@agent-plan/pi-adapter 0.2.21 → 0.2.22-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/README.md +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +252 -73
- package/dist/ui/paginate.d.ts +29 -0
- package/dist/ui/paginate.d.ts.map +1 -0
- package/dist/ui/paginate.js +73 -0
- package/package.json +3 -3
- package/web-ui-dist/assets/index-B2Nv9Ykv.css +1 -0
- package/web-ui-dist/assets/index-MvITvgH7.js +298 -0
- package/web-ui-dist/index.html +2 -2
- package/web-ui-dist/assets/index-B97B4wii.js +0 -298
- package/web-ui-dist/assets/index-Bi9ehfxU.css +0 -1
package/dist/index.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* - inject context into prompts
|
|
10
10
|
*/
|
|
11
11
|
import { Type } from "typebox";
|
|
12
|
-
import {
|
|
12
|
+
import { paginatedSelect, paginatedNotify } from "./ui/paginate.js";
|
|
13
|
+
import { ExportService, PlanStore, setWriteBusyHook, setWriteNotifyHook, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, buildRecap, addChecklistItem, removeChecklistItem, toggleChecklistItem, buildPhaseContextBlock, checkExplicitTaskStart, recommendNextTask, buildResumeRequiredProposal, packageVersionFromModule, resolvedPackageVersion, markFeatureRead, markPhaseRead, markTaskRead, hasReadParents, parentReadAdvisory, hasReadRequirements, requirementReadAdvisory, markRequirementRead, invalidateReads } from "@agent-plan/core";
|
|
13
14
|
import { createChecklistItemId, createFeatureId, createPhaseId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef, featureNumberOfPhase, validateResolvedTarget } from "@agent-plan/core/naming";
|
|
14
15
|
import { join, dirname } from "node:path";
|
|
15
16
|
import { homedir } from "node:os";
|
|
@@ -21,6 +22,9 @@ import { serve } from "@agent-plan/server/serve";
|
|
|
21
22
|
// ─── State (module-level) ───────────────────────────────────────────────
|
|
22
23
|
const PI_CONFIG_DIR_NAME = ".pi";
|
|
23
24
|
const PLAN_DIR_NAME = ".planner";
|
|
25
|
+
const PI_ADAPTER_PACKAGE = packageVersionFromModule(import.meta.url, "@agent-plan/pi-adapter");
|
|
26
|
+
const CORE_PACKAGE = resolvedPackageVersion("@agent-plan/core", import.meta.url);
|
|
27
|
+
const SERVER_PACKAGE = resolvedPackageVersion("@agent-plan/server", import.meta.url);
|
|
24
28
|
let capturedPi = null;
|
|
25
29
|
let store = null;
|
|
26
30
|
let server = null;
|
|
@@ -104,6 +108,7 @@ const healedStatusRoots = new Set();
|
|
|
104
108
|
const PLANNER_COMMAND_COMPLETIONS = [
|
|
105
109
|
{ value: "init", label: "init", description: "Initialize planner in this project" },
|
|
106
110
|
{ value: "show", label: "show", description: "Show planner overview" },
|
|
111
|
+
{ value: "version", label: "version", description: "Show loaded Agent Plan package versions" },
|
|
107
112
|
{ value: "repair", label: "repair", description: "Repair planner integrity" },
|
|
108
113
|
{ value: "cleanup-orphans", label: "cleanup-orphans", description: "List and remove orphan phase files" },
|
|
109
114
|
{ value: "project discuss", label: "project discuss", description: "Run project discovery" },
|
|
@@ -239,10 +244,11 @@ async function getPlannerExecutionGuard(st) {
|
|
|
239
244
|
? allTasks.find(({ task }) => task.id === resume.inProgressTaskIds[0])
|
|
240
245
|
: undefined;
|
|
241
246
|
const focusFromCurrentPhase = resume?.currentPhaseId
|
|
242
|
-
? allTasks.find(({ phase, task }) => phase.id === resume.currentPhaseId && task.status !== "done" && task.status !== "canceled")
|
|
247
|
+
? allTasks.find(({ phase, task }) => phase.id === resume.currentPhaseId && task.status !== "done" && task.status !== "canceled" && task.status !== "rejected")
|
|
243
248
|
: undefined;
|
|
244
|
-
const fallbackFocus = allTasks.find(({ task }) => task.
|
|
245
|
-
?? allTasks.find(({ task }) => task.status
|
|
249
|
+
const fallbackFocus = allTasks.find(({ task }) => task.pauseSnapshot)
|
|
250
|
+
?? allTasks.find(({ task }) => task.status === "planned" || task.status === "blocked" || task.status === "waiting")
|
|
251
|
+
?? allTasks.find(({ task }) => task.status !== "done" && task.status !== "canceled" && task.status !== "rejected");
|
|
246
252
|
const focus = focusFromResume ?? focusFromCurrentPhase ?? fallbackFocus;
|
|
247
253
|
return {
|
|
248
254
|
totalTasks,
|
|
@@ -313,7 +319,8 @@ async function buildHandoffMarkdown(st, reason, overrides) {
|
|
|
313
319
|
?? null;
|
|
314
320
|
const currentTask = currentPhase?.tasks.find((task) => resume.inProgressTaskIds.includes(task.id))
|
|
315
321
|
?? currentPhase?.tasks.find((task) => task.status === "in-progress")
|
|
316
|
-
??
|
|
322
|
+
?? currentPhase?.tasks.find((task) => task.pauseSnapshot)
|
|
323
|
+
?? [...(currentPhase?.tasks ?? [])].filter((task) => task.status !== "done" && task.status !== "canceled" && task.status !== "rejected").sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0]
|
|
317
324
|
?? latestTaskUpdate?.task
|
|
318
325
|
?? null;
|
|
319
326
|
const currentFeature = currentPhase?.featureId
|
|
@@ -551,6 +558,7 @@ function statusIcon(status) {
|
|
|
551
558
|
const icons = {
|
|
552
559
|
draft: "📄", discovery: "🔍", planned: "📋",
|
|
553
560
|
"in-progress": "🚧", done: "✅", blocked: "🚫", canceled: "❌",
|
|
561
|
+
rejected: "❌", deferred: "⏳", waiting: "⏱️",
|
|
554
562
|
};
|
|
555
563
|
return icons[status] ?? "❓";
|
|
556
564
|
}
|
|
@@ -622,20 +630,40 @@ function resolveStaticDir() {
|
|
|
622
630
|
const adapterDir = dirname(adapterFile);
|
|
623
631
|
// When installed as Pi package: node_modules/@agent-plan/pi-adapter/web-ui-dist/
|
|
624
632
|
const pkgWebUi = join(adapterDir, "..", "web-ui-dist");
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
633
|
+
// Monorepo dev: packages/plan-web-ui/dist/ (rebuilt by `pnpm build:web-ui`)
|
|
634
|
+
const devWebUi = join(adapterDir, "..", "..", "plan-web-ui", "dist");
|
|
635
|
+
const candidates = [pkgWebUi, devWebUi].filter((d) => {
|
|
636
|
+
try {
|
|
637
|
+
return existsSync(join(d, "index.html"));
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
if (candidates.length === 0)
|
|
644
|
+
return undefined;
|
|
645
|
+
if (candidates.length === 1)
|
|
646
|
+
return candidates[0];
|
|
647
|
+
// Prefer the most recently built bundle so a fresh `pnpm build:web-ui`
|
|
648
|
+
// (or copy-web-ui.sh) is picked up in dev instead of a stale vendored
|
|
649
|
+
// snapshot. Deterministic: compare index.html mtimes, newest wins.
|
|
650
|
+
const mtime = (d) => {
|
|
651
|
+
try {
|
|
652
|
+
return statSync(join(d, "index.html")).mtimeMs;
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
return 0;
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
return [...candidates].sort((a, b) => mtime(b) - mtime(a))[0];
|
|
629
659
|
}
|
|
630
660
|
catch {
|
|
631
661
|
return undefined;
|
|
632
662
|
}
|
|
633
663
|
}
|
|
634
664
|
async function maybeStartWeb(ctx) {
|
|
635
|
-
if (server)
|
|
636
|
-
plannerSessionEnabled = true;
|
|
665
|
+
if (server)
|
|
637
666
|
return;
|
|
638
|
-
}
|
|
639
667
|
ctx.ui.notify("Starting web server (LAN)…", "info");
|
|
640
668
|
try {
|
|
641
669
|
await startServer(ctx, undefined, "lan");
|
|
@@ -657,7 +685,6 @@ function normalizeVisibility(input) {
|
|
|
657
685
|
return undefined;
|
|
658
686
|
}
|
|
659
687
|
async function startServer(ctx, requestedPort, visibility = "lan") {
|
|
660
|
-
plannerSessionEnabled = true;
|
|
661
688
|
if (server)
|
|
662
689
|
return;
|
|
663
690
|
const host = visibility === "lan" ? "0.0.0.0" : "127.0.0.1";
|
|
@@ -729,6 +756,10 @@ async function startServer(ctx, requestedPort, visibility = "lan") {
|
|
|
729
756
|
catch { }
|
|
730
757
|
}
|
|
731
758
|
}
|
|
759
|
+
function enablePlannerSession() {
|
|
760
|
+
plannerSessionEnabled = true;
|
|
761
|
+
contextBlockDirty = true;
|
|
762
|
+
}
|
|
732
763
|
function disablePlannerSession() {
|
|
733
764
|
plannerSessionEnabled = false;
|
|
734
765
|
startupResumePromptPending = false;
|
|
@@ -1041,6 +1072,7 @@ export default function planPiExtension(pi) {
|
|
|
1041
1072
|
const PLANNER_MENU_ACTIONS = [
|
|
1042
1073
|
"init",
|
|
1043
1074
|
"show",
|
|
1075
|
+
"version",
|
|
1044
1076
|
"repair",
|
|
1045
1077
|
"cleanup-orphans",
|
|
1046
1078
|
"project discuss",
|
|
@@ -1076,7 +1108,7 @@ export default function planPiExtension(pi) {
|
|
|
1076
1108
|
"load",
|
|
1077
1109
|
"stop",
|
|
1078
1110
|
];
|
|
1079
|
-
const SUB_HELP = "Available: init, show, repair, cleanup-orphans, project, feature, phase, task, discuss, handoff, web, export, export-full, bypass, clear-bypass, load, stop\n" +
|
|
1111
|
+
const SUB_HELP = "Available: init, show, version, repair, cleanup-orphans, project, feature, phase, task, discuss, handoff, web, export, export-full, bypass, clear-bypass, load, stop\n" +
|
|
1080
1112
|
"Try: /planner <TAB> | /planner feature list | /planner task start | /planner cleanup-orphans\n" +
|
|
1081
1113
|
"Handoff actions: /planner handoff list | show P00x | write P00x | clear P00x | prepare (prepare proposes a target and asks for confirmation)";
|
|
1082
1114
|
if (!a) {
|
|
@@ -1088,6 +1120,15 @@ export default function planPiExtension(pi) {
|
|
|
1088
1120
|
await runPlanner(action, ctx);
|
|
1089
1121
|
return;
|
|
1090
1122
|
}
|
|
1123
|
+
if (a === "version") {
|
|
1124
|
+
ctx.ui.notify([
|
|
1125
|
+
"Agent Plan runtime versions",
|
|
1126
|
+
`${PI_ADAPTER_PACKAGE.name}: ${PI_ADAPTER_PACKAGE.version}`,
|
|
1127
|
+
`${CORE_PACKAGE.name}: ${CORE_PACKAGE.version}`,
|
|
1128
|
+
`${SERVER_PACKAGE.name}: ${SERVER_PACKAGE.version}`,
|
|
1129
|
+
].join("\n"), "info");
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1091
1132
|
const st = ensureStore(ctx);
|
|
1092
1133
|
// ── init ──
|
|
1093
1134
|
if (a === "init") {
|
|
@@ -1120,6 +1161,7 @@ export default function planPiExtension(pi) {
|
|
|
1120
1161
|
await st.writeGenerated();
|
|
1121
1162
|
await scanCodebase();
|
|
1122
1163
|
ctx.ui.notify(`.planner/ initialized for "${name.trim()}". Starting project discuss…`, "info");
|
|
1164
|
+
enablePlannerSession();
|
|
1123
1165
|
await maybeStartWeb(ctx);
|
|
1124
1166
|
await handlePlanner("project discuss", ctx);
|
|
1125
1167
|
return;
|
|
@@ -1137,6 +1179,7 @@ export default function planPiExtension(pi) {
|
|
|
1137
1179
|
await st.writeGenerated();
|
|
1138
1180
|
await scanCodebase();
|
|
1139
1181
|
ctx.ui.notify(`.planner/ initialized for "${name.trim()}". Starting project discuss…`, "info");
|
|
1182
|
+
enablePlannerSession();
|
|
1140
1183
|
await maybeStartWeb(ctx);
|
|
1141
1184
|
await handlePlanner("project discuss", ctx);
|
|
1142
1185
|
return;
|
|
@@ -1168,12 +1211,12 @@ export default function planPiExtension(pi) {
|
|
|
1168
1211
|
.sort((a, b) => a.number - b.number);
|
|
1169
1212
|
if (features.length === 0)
|
|
1170
1213
|
return null;
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1214
|
+
return paginatedSelect(ctx, {
|
|
1215
|
+
title: "Pick a feature",
|
|
1216
|
+
items: features,
|
|
1217
|
+
render: (f) => ` ${formatFeatureRef(f.number)} ${statusIcon(f.status)} ${f.name}${f.shortId ? ` · ${f.shortId}` : ""}`,
|
|
1218
|
+
pageSize: 10,
|
|
1219
|
+
});
|
|
1177
1220
|
}
|
|
1178
1221
|
// ═══════════════════════════════════════════════════════════════
|
|
1179
1222
|
// Helper: pick a phase interactively
|
|
@@ -1184,12 +1227,12 @@ export default function planPiExtension(pi) {
|
|
|
1184
1227
|
return null;
|
|
1185
1228
|
}
|
|
1186
1229
|
const features = (await st.loadFeatures().catch(() => ({ features: [] }))).features;
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1230
|
+
return paginatedSelect(ctx, {
|
|
1231
|
+
title: "Pick a phase",
|
|
1232
|
+
items: phases,
|
|
1233
|
+
render: (p) => ` ${formatPhaseRef(p.number, featureNumberOfPhase(p, features))} ${statusIcon(p.status)} ${p.title}${p.shortId ? ` · ${p.shortId}` : ""}`,
|
|
1234
|
+
pageSize: 10,
|
|
1235
|
+
});
|
|
1193
1236
|
}
|
|
1194
1237
|
// ═══════════════════════════════════════════════════════════════
|
|
1195
1238
|
// Helper: pick a task interactively from a phase
|
|
@@ -1198,12 +1241,12 @@ export default function planPiExtension(pi) {
|
|
|
1198
1241
|
if (phase.tasks.length === 0)
|
|
1199
1242
|
return null;
|
|
1200
1243
|
const tasks = phase.tasks.slice().sort((a, b) => a.number - b.number);
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1244
|
+
return paginatedSelect(ctx, {
|
|
1245
|
+
title: `Pick a task from "${phase.title}"`,
|
|
1246
|
+
items: tasks,
|
|
1247
|
+
render: (t) => ` T${String(t.number).padStart(3, "0")} ${statusIcon(t.status)} ${t.title}${t.shortId ? ` · ${t.shortId}` : ""}`,
|
|
1248
|
+
pageSize: 10,
|
|
1249
|
+
});
|
|
1207
1250
|
}
|
|
1208
1251
|
function parseMultilineList(value) {
|
|
1209
1252
|
return (value ?? "")
|
|
@@ -1447,11 +1490,12 @@ export default function planPiExtension(pi) {
|
|
|
1447
1490
|
return;
|
|
1448
1491
|
}
|
|
1449
1492
|
const phases = await st.loadAllPhases();
|
|
1450
|
-
|
|
1493
|
+
const lines = features.map((feature) => {
|
|
1451
1494
|
const featurePhases = phases.filter((phase) => phase.featureId === feature.id);
|
|
1452
1495
|
const taskCount = featurePhases.reduce((total, phase) => total + phase.tasks.length, 0);
|
|
1453
1496
|
return `${statusIcon(feature.status)} ${feature.name} (${feature.id}) — ${featurePhases.length} phases, ${taskCount} tasks`;
|
|
1454
|
-
})
|
|
1497
|
+
});
|
|
1498
|
+
await paginatedNotify(ctx, { title: "features", lines, pageSize: 10 });
|
|
1455
1499
|
return;
|
|
1456
1500
|
}
|
|
1457
1501
|
if (b === "add") {
|
|
@@ -2154,6 +2198,18 @@ export default function planPiExtension(pi) {
|
|
|
2154
2198
|
return;
|
|
2155
2199
|
}
|
|
2156
2200
|
}
|
|
2201
|
+
if (phase && !hasReadParents(phase.featureId, phase.id)) {
|
|
2202
|
+
ctx.ui.notify("⚠️ READ REQUIRED before proceeding: read the parent feature and phase (full=true) for this task before starting it.", "warning");
|
|
2203
|
+
}
|
|
2204
|
+
if (phase) {
|
|
2205
|
+
const linkedRequirementIds = [
|
|
2206
|
+
...(await st.linkedRequirementsForPhase(phase.id)).map((r) => r.id),
|
|
2207
|
+
...(phase.featureId ? (await st.linkedRequirementsForFeature(phase.featureId)).map((r) => r.id) : []),
|
|
2208
|
+
];
|
|
2209
|
+
if (linkedRequirementIds.length > 0 && !hasReadRequirements(linkedRequirementIds)) {
|
|
2210
|
+
ctx.ui.notify("⚠️ REQUIREMENTS READ REQUIRED: read the requirements linked to this phase (and feature) before starting the task.", "warning");
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2157
2213
|
if (task.status === "in-progress") {
|
|
2158
2214
|
ctx.ui.notify(`Task "${task.title}" is already in-progress.`, "info");
|
|
2159
2215
|
return;
|
|
@@ -2327,7 +2383,11 @@ export default function planPiExtension(pi) {
|
|
|
2327
2383
|
ctx.ui.notify("No phase handoffs set.", "info");
|
|
2328
2384
|
return;
|
|
2329
2385
|
}
|
|
2330
|
-
|
|
2386
|
+
await paginatedNotify(ctx, {
|
|
2387
|
+
title: "phase handoffs",
|
|
2388
|
+
lines: list.map((e) => `• ${e.compositeRef} — ${e.firstLine} (updated ${e.updatedAt})`),
|
|
2389
|
+
pageSize: 10,
|
|
2390
|
+
});
|
|
2331
2391
|
return;
|
|
2332
2392
|
}
|
|
2333
2393
|
if (action === "show") {
|
|
@@ -2500,7 +2560,7 @@ export default function planPiExtension(pi) {
|
|
|
2500
2560
|
return;
|
|
2501
2561
|
}
|
|
2502
2562
|
if (a === "load") {
|
|
2503
|
-
|
|
2563
|
+
enablePlannerSession();
|
|
2504
2564
|
if (!server) {
|
|
2505
2565
|
ctx.ui.notify("Starting web server (LAN) …", "info");
|
|
2506
2566
|
await startServer(ctx, undefined, "lan").catch(() => { });
|
|
@@ -2733,6 +2793,7 @@ export default function planPiExtension(pi) {
|
|
|
2733
2793
|
if (!st)
|
|
2734
2794
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
2735
2795
|
const requirements = await st.loadRequirements();
|
|
2796
|
+
requirements.requirements.forEach((req) => markRequirementRead(req.id));
|
|
2736
2797
|
return {
|
|
2737
2798
|
content: [{ type: "text", text: requirements.requirements.map((req) => `- ${req.id} — ${req.title} (${req.status})`).join("\n") || "No requirements" }],
|
|
2738
2799
|
details: requirements,
|
|
@@ -2753,15 +2814,21 @@ export default function planPiExtension(pi) {
|
|
|
2753
2814
|
const st = await requirePlan(ctx);
|
|
2754
2815
|
if (!st)
|
|
2755
2816
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
2817
|
+
const title = params.title?.trim();
|
|
2818
|
+
if (!title)
|
|
2819
|
+
return { content: [{ type: "text", text: "title required" }], details: {} };
|
|
2820
|
+
const links = await resolveRequirementPhaseRefs(st, params.linkedPhaseIds);
|
|
2821
|
+
if (!links.ok)
|
|
2822
|
+
return { content: [{ type: "text", text: links.error }], details: {} };
|
|
2756
2823
|
const requirements = await st.loadRequirements();
|
|
2757
2824
|
const now = nowISO();
|
|
2758
2825
|
const requirement = {
|
|
2759
2826
|
id: createRequirementId(),
|
|
2760
|
-
title
|
|
2827
|
+
title,
|
|
2761
2828
|
description: params.description?.trim() ?? "",
|
|
2762
2829
|
status: params.status?.trim() ?? "planned",
|
|
2763
2830
|
macroTasks: [],
|
|
2764
|
-
linkedPhaseIds:
|
|
2831
|
+
linkedPhaseIds: links.linkedPhaseIds,
|
|
2765
2832
|
createdAt: now,
|
|
2766
2833
|
updatedAt: now,
|
|
2767
2834
|
};
|
|
@@ -2790,14 +2857,22 @@ export default function planPiExtension(pi) {
|
|
|
2790
2857
|
const requirement = requirements.requirements.find((entry) => entry.id === params.requirementId);
|
|
2791
2858
|
if (!requirement)
|
|
2792
2859
|
return { content: [{ type: "text", text: `Requirement not found: ${params.requirementId}` }], details: {} };
|
|
2793
|
-
if (params.title !== undefined)
|
|
2794
|
-
|
|
2860
|
+
if (params.title !== undefined) {
|
|
2861
|
+
const title = params.title.trim();
|
|
2862
|
+
if (!title)
|
|
2863
|
+
return { content: [{ type: "text", text: "title required" }], details: {} };
|
|
2864
|
+
requirement.title = title;
|
|
2865
|
+
}
|
|
2795
2866
|
if (params.description !== undefined)
|
|
2796
2867
|
requirement.description = params.description.trim();
|
|
2797
2868
|
if (params.status !== undefined)
|
|
2798
2869
|
requirement.status = params.status.trim();
|
|
2799
|
-
if (params.linkedPhaseIds !== undefined)
|
|
2800
|
-
|
|
2870
|
+
if (params.linkedPhaseIds !== undefined) {
|
|
2871
|
+
const links = await resolveRequirementPhaseRefs(st, params.linkedPhaseIds);
|
|
2872
|
+
if (!links.ok)
|
|
2873
|
+
return { content: [{ type: "text", text: links.error }], details: {} };
|
|
2874
|
+
requirement.linkedPhaseIds = links.linkedPhaseIds;
|
|
2875
|
+
}
|
|
2801
2876
|
requirement.updatedAt = nowISO();
|
|
2802
2877
|
await st.saveRequirements(requirements);
|
|
2803
2878
|
await st.writeGenerated();
|
|
@@ -3007,6 +3082,23 @@ export default function planPiExtension(pi) {
|
|
|
3007
3082
|
const feat = phase.featureId ? features.find((f) => f.id === phase.featureId) : undefined;
|
|
3008
3083
|
return { ok: true, phase, compositeRef: formatPhaseRef(phase.number, feat?.number) };
|
|
3009
3084
|
}
|
|
3085
|
+
async function resolveRequirementPhaseRefs(st, refs) {
|
|
3086
|
+
if (!Array.isArray(refs) || refs.length === 0) {
|
|
3087
|
+
return { ok: false, error: "linkedPhaseIds must contain at least one phase" };
|
|
3088
|
+
}
|
|
3089
|
+
const trimmed = refs.map((entry) => entry.trim()).filter(Boolean);
|
|
3090
|
+
if (trimmed.length === 0) {
|
|
3091
|
+
return { ok: false, error: "linkedPhaseIds must contain non-empty phase references" };
|
|
3092
|
+
}
|
|
3093
|
+
const phases = await st.loadAllPhases();
|
|
3094
|
+
const features = (await st.loadFeatures()).features;
|
|
3095
|
+
const resolved = trimmed.map((ref) => findPhaseByRef(phases, features, ref));
|
|
3096
|
+
const missingIndex = resolved.findIndex((phase) => !phase);
|
|
3097
|
+
if (missingIndex !== -1) {
|
|
3098
|
+
return { ok: false, error: `linked phase not found: ${trimmed[missingIndex]}` };
|
|
3099
|
+
}
|
|
3100
|
+
return { ok: true, linkedPhaseIds: [...new Set(resolved.map((phase) => phase.id))] };
|
|
3101
|
+
}
|
|
3010
3102
|
pi.registerTool({
|
|
3011
3103
|
name: "handoff_list",
|
|
3012
3104
|
label: "Handoff List",
|
|
@@ -3063,11 +3155,13 @@ export default function planPiExtension(pi) {
|
|
|
3063
3155
|
// Derive the current first non-empty line (H1) of the supplied body.
|
|
3064
3156
|
const firstLine = body.split(/\r?\n/).find((l) => l.trim().length > 0) ?? "";
|
|
3065
3157
|
const firstHeadingText = firstLine.replace(/^#+\s*/, "").trim();
|
|
3066
|
-
const
|
|
3067
|
-
|
|
3158
|
+
const genericTitlePattern = /^(handoff|canonical handoff|session handoff)$/i;
|
|
3159
|
+
const effectiveHeadingText = (title && title.length > 0) ? title : firstHeadingText;
|
|
3160
|
+
const isGeneric = !effectiveHeadingText || genericTitlePattern.test(effectiveHeadingText);
|
|
3161
|
+
if (isGeneric) {
|
|
3068
3162
|
return {
|
|
3069
3163
|
content: [{ type: "text", text: "❌ Generic handoff title. Provide a meaningful `title` (or start your markdown with a descriptive H1) summarizing the work — e.g. 'P049 — featureId validation: tests + adapter wiring'. Generic titles like 'Handoff' or 'Canonical handoff' are not accepted." }],
|
|
3070
|
-
details: { error: "generic title", firstHeadingText },
|
|
3164
|
+
details: { error: "generic title", firstHeadingText: effectiveHeadingText || firstHeadingText },
|
|
3071
3165
|
};
|
|
3072
3166
|
}
|
|
3073
3167
|
// Normalize: if a title is provided, replace or prepend the H1 so the first line is the title.
|
|
@@ -3177,6 +3271,7 @@ export default function planPiExtension(pi) {
|
|
|
3177
3271
|
if (!resolvedFeature.ok)
|
|
3178
3272
|
return { content: [{ type: "text", text: resolvedFeature.error }], details: {} };
|
|
3179
3273
|
const feature = resolvedFeature.feature;
|
|
3274
|
+
markFeatureRead(feature.id);
|
|
3180
3275
|
const phases = (await st.loadAllPhases()).filter((p) => p.featureId === feature.id);
|
|
3181
3276
|
const linkedRequirements = await st.linkedRequirementsForFeature(feature.id);
|
|
3182
3277
|
const summary = `${feature.name} — ${formatFeatureRef(feature.number)}${feature.shortId ? ` · ${feature.shortId}` : ""} (${feature.status}; ${phases.length} phases${linkedRequirements.length ? `; ${linkedRequirements.length} linked requirement${linkedRequirements.length === 1 ? "" : "s"}` : ""})`;
|
|
@@ -3475,6 +3570,7 @@ export default function planPiExtension(pi) {
|
|
|
3475
3570
|
const phase = findPhaseByRef(await st.loadAllPhases(), features, params.phaseId.trim());
|
|
3476
3571
|
if (!phase)
|
|
3477
3572
|
return { content: [{ type: "text", text: `Phase not found: ${params.phaseId}` }], details: {} };
|
|
3573
|
+
markPhaseRead(phase.id, phase.featureId);
|
|
3478
3574
|
const linkedRequirements = await st.linkedRequirementsForPhase(phase.id);
|
|
3479
3575
|
const reqCount = linkedRequirements.length;
|
|
3480
3576
|
const summary = `${phase.title} — ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, features))}${phase.shortId ? ` · ${phase.shortId}` : ""} (${phase.status}; ${phase.tasks.length} tasks${reqCount ? `; ${reqCount} linked requirement${reqCount === 1 ? "" : "s"}` : ""})`;
|
|
@@ -3499,6 +3595,9 @@ export default function planPiExtension(pi) {
|
|
|
3499
3595
|
const st = await requirePlan(ctx);
|
|
3500
3596
|
if (!st)
|
|
3501
3597
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
3598
|
+
const title = params.title?.trim();
|
|
3599
|
+
if (!title)
|
|
3600
|
+
return { content: [{ type: "text", text: "title required" }], details: {} };
|
|
3502
3601
|
if (!params.featureId?.trim())
|
|
3503
3602
|
return { content: [{ type: "text", text: "featureId is required: a phase must belong to a feature." }], details: {} };
|
|
3504
3603
|
const createPhaseFeatures = (await st.loadFeatures()).features;
|
|
@@ -3519,7 +3618,7 @@ export default function planPiExtension(pi) {
|
|
|
3519
3618
|
const priority = await st.nextPriority("phase", featureId);
|
|
3520
3619
|
const now = nowISO();
|
|
3521
3620
|
phase = {
|
|
3522
|
-
id, number: identity.number, shortId: identity.shortId, priority, slug: normalizeSlug(
|
|
3621
|
+
id, number: identity.number, shortId: identity.shortId, priority, slug: normalizeSlug(title), title,
|
|
3523
3622
|
featureId,
|
|
3524
3623
|
status: params.status ?? "draft",
|
|
3525
3624
|
discussedAt: "",
|
|
@@ -3809,10 +3908,10 @@ export default function planPiExtension(pi) {
|
|
|
3809
3908
|
pi.registerTool({
|
|
3810
3909
|
name: "task_get",
|
|
3811
3910
|
label: "Task Get",
|
|
3812
|
-
description: "Show a task. Compact identity by default (saves tokens); pass full=true to include the description
|
|
3911
|
+
description: "Show a task. Compact identity by default (saves tokens); pass full=true to include the description, resume checkpoint/advisory, and statusLog.",
|
|
3813
3912
|
parameters: Type.Object({
|
|
3814
3913
|
taskId: Type.String({ description: "Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title" }),
|
|
3815
|
-
full: Type.Optional(Type.Boolean({ description: "If true, include the task description
|
|
3914
|
+
full: Type.Optional(Type.Boolean({ description: "If true, include the task description, resume checkpoint/advisory, and statusLog. Default: compact identity only." })),
|
|
3816
3915
|
}),
|
|
3817
3916
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3818
3917
|
const st = await requirePlan(ctx);
|
|
@@ -3822,11 +3921,36 @@ export default function planPiExtension(pi) {
|
|
|
3822
3921
|
const found = findTaskByRef(await st.loadAllPhases(), features, params.taskId.trim());
|
|
3823
3922
|
if (!found)
|
|
3824
3923
|
return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], details: {} };
|
|
3924
|
+
markTaskRead(found.task.id, found.phase.id, found.phase.featureId);
|
|
3825
3925
|
const summary = `${found.task.title} — ${formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features))}/T${pad(found.task.number)}${found.task.shortId ? ` · ${found.task.shortId}` : ""} (${found.task.status})`;
|
|
3826
3926
|
if (!params.full)
|
|
3827
3927
|
return { content: [{ type: "text", text: summary }], details: {} };
|
|
3928
|
+
const project = await st.loadProject();
|
|
3929
|
+
const pendingDeviation = found.task.status === "done" || found.task.status === "canceled" || found.task.status === "rejected"
|
|
3930
|
+
? undefined
|
|
3931
|
+
: project.workDeviations
|
|
3932
|
+
.filter((deviation) => deviation.resumeTaskId === found.task.id
|
|
3933
|
+
&& (deviation.state === "resume-required" || deviation.state === "resolved"))
|
|
3934
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
|
|
3935
|
+
const snapshot = found.task.pauseSnapshot ?? pendingDeviation?.snapshot ?? null;
|
|
3828
3936
|
const log = (found.task.statusLog ?? []).map((e) => ` - ${e.date.slice(0, 10)} ${e.title}`).join("\n");
|
|
3829
|
-
|
|
3937
|
+
const sections = [summary];
|
|
3938
|
+
if (found.task.description?.trim())
|
|
3939
|
+
sections.push(found.task.description.trim());
|
|
3940
|
+
if (snapshot || pendingDeviation) {
|
|
3941
|
+
sections.push([
|
|
3942
|
+
pendingDeviation
|
|
3943
|
+
? "Resume advisory: this task has a saved checkpoint that should be evaluated before selecting other work."
|
|
3944
|
+
: "Resume checkpoint:",
|
|
3945
|
+
snapshot ? `Checkpoint reason: ${snapshot.reason}` : "",
|
|
3946
|
+
snapshot ? `Work checkpoint: ${snapshot.whatWasBeingDone}` : "",
|
|
3947
|
+
snapshot ? `Resume from: ${snapshot.resumeLocation}` : "",
|
|
3948
|
+
snapshot ? `How to resume: ${snapshot.howToResume}` : "",
|
|
3949
|
+
].filter(Boolean).join("\n"));
|
|
3950
|
+
}
|
|
3951
|
+
if (log)
|
|
3952
|
+
sections.push(`Status log:\n${log}`);
|
|
3953
|
+
return { content: [{ type: "text", text: sections.join("\n\n") }], details: {} };
|
|
3830
3954
|
},
|
|
3831
3955
|
});
|
|
3832
3956
|
pi.registerTool({
|
|
@@ -3846,6 +3970,9 @@ export default function planPiExtension(pi) {
|
|
|
3846
3970
|
const st = await requirePlan(ctx);
|
|
3847
3971
|
if (!st)
|
|
3848
3972
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
3973
|
+
const title = params.title?.trim();
|
|
3974
|
+
if (!title)
|
|
3975
|
+
return { content: [{ type: "text", text: "title required" }], details: {} };
|
|
3849
3976
|
if (!params.featureId?.trim())
|
|
3850
3977
|
return { content: [{ type: "text", text: "featureId is required: a task must belong to a feature." }], details: {} };
|
|
3851
3978
|
const features = (await st.loadFeatures()).features;
|
|
@@ -3872,7 +3999,7 @@ export default function planPiExtension(pi) {
|
|
|
3872
3999
|
const existingPhase = await st.loadPhase(phaseId);
|
|
3873
4000
|
if (!existingPhase)
|
|
3874
4001
|
return { content: [{ type: "text", text: `Resolved phase ${phaseId} no longer exists. Refusing to create task.` }], details: {} };
|
|
3875
|
-
const shortName = clampSlug(params.shortName ??
|
|
4002
|
+
const shortName = clampSlug(params.shortName ?? title, 30, `task-${Date.now().toString(36)}`); // clamp+strip trailing dash; never empty
|
|
3876
4003
|
const taskId = createTaskId();
|
|
3877
4004
|
const identity = await st.allocateEntityIdentity("task", taskId);
|
|
3878
4005
|
const priority = await st.nextPriority("task", phaseId);
|
|
@@ -3883,7 +4010,7 @@ export default function planPiExtension(pi) {
|
|
|
3883
4010
|
}
|
|
3884
4011
|
const task = {
|
|
3885
4012
|
id: taskId, phaseId, number: identity.number, shortId: identity.shortId, priority, shortName,
|
|
3886
|
-
title
|
|
4013
|
+
title,
|
|
3887
4014
|
status: initialStatus,
|
|
3888
4015
|
description: params.description ?? "",
|
|
3889
4016
|
descriptionUpdatedAt: now,
|
|
@@ -3953,6 +4080,12 @@ export default function planPiExtension(pi) {
|
|
|
3953
4080
|
// Validate the motivation requirement for restrictive status transitions,
|
|
3954
4081
|
// mirroring the MCP adapter and the /planner task update command
|
|
3955
4082
|
// (AGENTS.md rule 8 — rejected writes must leave the files unchanged).
|
|
4083
|
+
if (found.task.pauseSnapshot && params.status === "in-progress" && params.status !== found.task.status) {
|
|
4084
|
+
return {
|
|
4085
|
+
content: [{ type: "text", text: "Resume checkpoint lifecycle transitions require task_start so the checkpoint and return stack remain consistent." }],
|
|
4086
|
+
details: {},
|
|
4087
|
+
};
|
|
4088
|
+
}
|
|
3956
4089
|
if (params.status !== undefined &&
|
|
3957
4090
|
params.status !== found.task.status &&
|
|
3958
4091
|
needsMotivation(found.task.status, params.status)) {
|
|
@@ -4131,7 +4264,8 @@ export default function planPiExtension(pi) {
|
|
|
4131
4264
|
resumeLocation: params.resume_location.trim(), howToResume: params.how_to_resume.trim(), relatedTaskId: "",
|
|
4132
4265
|
pausedAt: nowISO(), pausedBy: params.paused_by?.trim() ?? "",
|
|
4133
4266
|
};
|
|
4134
|
-
|
|
4267
|
+
invalidateReads();
|
|
4268
|
+
const checkpointed = await st.pauseTask(found.phase.id, found.task.id, snapshot);
|
|
4135
4269
|
await st.syncTaskStatusRollup(found.phase.id);
|
|
4136
4270
|
const activeDeviation = (await st.loadProject()).workDeviations
|
|
4137
4271
|
.filter((deviation) => (deviation.state === "approved" || deviation.state === "active")
|
|
@@ -4143,20 +4277,20 @@ export default function planPiExtension(pi) {
|
|
|
4143
4277
|
? findTaskByRef(await st.loadAllPhases(), features, activeDeviation.resumeTaskId)
|
|
4144
4278
|
: undefined;
|
|
4145
4279
|
await st.writeGenerated();
|
|
4146
|
-
const reference = `${formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features))}/T${String(
|
|
4280
|
+
const reference = `${formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features))}/T${String(checkpointed.number).padStart(3, "0")}`;
|
|
4147
4281
|
const resumeRef = resume
|
|
4148
4282
|
? `${formatPhaseRef(resume.phase.number, featureNumberOfPhase(resume.phase, features))}/T${String(resume.task.number).padStart(3, "0")}`
|
|
4149
4283
|
: "";
|
|
4150
4284
|
return {
|
|
4151
4285
|
content: [{ type: "text", text: [
|
|
4152
|
-
|
|
4286
|
+
`💾 Resume checkpoint saved: ${reference} — ${checkpointed.title}`,
|
|
4153
4287
|
`Why: ${snapshot.reason}`,
|
|
4154
4288
|
`Checkpoint: ${snapshot.whatWasBeingDone}`,
|
|
4155
4289
|
`Resume from: ${snapshot.resumeLocation}`,
|
|
4156
4290
|
`How to resume: ${snapshot.howToResume}`,
|
|
4157
4291
|
resume ? `↩️ RESUME REQUIRED: ${resumeRef} — ${resume.task.title}` : "",
|
|
4158
4292
|
].filter(Boolean).join("\n") }],
|
|
4159
|
-
details: { task:
|
|
4293
|
+
details: { task: checkpointed, snapshot, ...(resume ? { resumeRequired: { taskId: resume.task.id } } : {}) },
|
|
4160
4294
|
};
|
|
4161
4295
|
},
|
|
4162
4296
|
});
|
|
@@ -4187,8 +4321,8 @@ export default function planPiExtension(pi) {
|
|
|
4187
4321
|
return { content: [{ type: "text", text: `Task not found: ${params.to_task}` }], details: {} };
|
|
4188
4322
|
if (source.task.id === target.task.id)
|
|
4189
4323
|
return { content: [{ type: "text", text: "Source and temporary task must differ." }], details: {} };
|
|
4190
|
-
if (source.task.status !== "in-progress" && source.task.
|
|
4191
|
-
return { content: [{ type: "text", text: `Task switch denied: source is ${source.task.status}; only in-progress or
|
|
4324
|
+
if (source.task.status !== "in-progress" && !source.task.pauseSnapshot) {
|
|
4325
|
+
return { content: [{ type: "text", text: `Task switch denied: source is ${source.task.status}; only in-progress or checkpointed work can be switched.` }], details: {} };
|
|
4192
4326
|
}
|
|
4193
4327
|
const eligibility = checkExplicitTaskStart(features, phases, target.task.id, project.workDeviations);
|
|
4194
4328
|
if (!eligibility.eligible)
|
|
@@ -4218,7 +4352,7 @@ export default function planPiExtension(pi) {
|
|
|
4218
4352
|
await st.updatePhase(source.phase.id, (phase) => {
|
|
4219
4353
|
const task = phase.tasks.find((entry) => entry.id === source.task.id);
|
|
4220
4354
|
if (!task)
|
|
4221
|
-
throw new Error(`
|
|
4355
|
+
throw new Error(`Checkpointed source task disappeared: ${params.from_task}`);
|
|
4222
4356
|
task.pauseSnapshot = snapshot;
|
|
4223
4357
|
task.pauseHistory = [...task.pauseHistory, snapshot];
|
|
4224
4358
|
task.updatedAt = timestamp;
|
|
@@ -4226,7 +4360,7 @@ export default function planPiExtension(pi) {
|
|
|
4226
4360
|
});
|
|
4227
4361
|
}
|
|
4228
4362
|
sourceCheckpointed = true;
|
|
4229
|
-
if (target.task.
|
|
4363
|
+
if (target.task.pauseSnapshot) {
|
|
4230
4364
|
await st.resumeTask(target.phase.id, target.task.id, timestamp);
|
|
4231
4365
|
}
|
|
4232
4366
|
else {
|
|
@@ -4248,7 +4382,7 @@ export default function planPiExtension(pi) {
|
|
|
4248
4382
|
}
|
|
4249
4383
|
targetStarted = true;
|
|
4250
4384
|
await st.addWorkDeviation(record);
|
|
4251
|
-
if (target.task.
|
|
4385
|
+
if (target.task.pauseSnapshot) {
|
|
4252
4386
|
const resumedDeviation = project.workDeviations
|
|
4253
4387
|
.filter((deviation) => deviation.resumeTaskId === target.task.id
|
|
4254
4388
|
&& (deviation.state === "resume-required" || deviation.state === "resolved"))
|
|
@@ -4281,21 +4415,28 @@ export default function planPiExtension(pi) {
|
|
|
4281
4415
|
await st.writeGenerated();
|
|
4282
4416
|
const sourceRef = `${formatPhaseRef(source.phase.number, featureNumberOfPhase(source.phase, features))}/T${String(source.task.number).padStart(3, "0")}`;
|
|
4283
4417
|
const targetRef = `${formatPhaseRef(target.phase.number, featureNumberOfPhase(target.phase, features))}/T${String(target.task.number).padStart(3, "0")}`;
|
|
4418
|
+
const targetReadAdvisory = parentReadAdvisory(target.phase.featureId, target.phase.id);
|
|
4419
|
+
const targetRequirementAdvisory = requirementReadAdvisory([
|
|
4420
|
+
...(await st.linkedRequirementsForPhase(target.phase.id)).map((r) => r.id),
|
|
4421
|
+
...(target.phase.featureId ? (await st.linkedRequirementsForFeature(target.phase.featureId)).map((r) => r.id) : []),
|
|
4422
|
+
]);
|
|
4284
4423
|
return {
|
|
4285
4424
|
content: [{ type: "text", text: [
|
|
4286
4425
|
`🔀 Task switched: ${sourceRef} → ${targetRef}`,
|
|
4287
|
-
`
|
|
4426
|
+
`Resume checkpoint: ${snapshot.whatWasBeingDone}`,
|
|
4288
4427
|
`Return target: ${sourceRef} from ${snapshot.resumeLocation}`,
|
|
4289
4428
|
"Normal priority was deliberately overridden; completing the temporary task will emit RESUME REQUIRED.",
|
|
4429
|
+
...(targetReadAdvisory ? [targetReadAdvisory.trimStart()] : []),
|
|
4430
|
+
...(targetRequirementAdvisory ? [targetRequirementAdvisory.trimStart()] : []),
|
|
4290
4431
|
].join("\n") }],
|
|
4291
|
-
details: { deviation: record, snapshot, resumeTaskId: source.task.id, temporaryTaskId: target.task.id },
|
|
4432
|
+
details: { deviation: record, snapshot, resumeTaskId: source.task.id, temporaryTaskId: target.task.id, ...(targetReadAdvisory || targetRequirementAdvisory ? { readRequired: true } : {}) },
|
|
4292
4433
|
};
|
|
4293
4434
|
},
|
|
4294
4435
|
});
|
|
4295
4436
|
pi.registerTool({
|
|
4296
4437
|
name: "task_start",
|
|
4297
4438
|
label: "Task Start",
|
|
4298
|
-
description: "Set a task to in-progress or resume
|
|
4439
|
+
description: "Set a task to in-progress or resume checkpointed work. BEFORE calling, read feature_get(full=true), phase_get(full=true), then task_get(full=true). A different active task must first be suspended through task_switch. Otherwise follow active/pending-resume/feature → phase → task priority guidance.",
|
|
4299
4440
|
parameters: Type.Object({
|
|
4300
4441
|
taskId: Type.String({ description: "Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title to start" }),
|
|
4301
4442
|
}),
|
|
@@ -4325,9 +4466,27 @@ export default function planPiExtension(pi) {
|
|
|
4325
4466
|
const activeRef = `${formatPhaseRef(selection.candidate.phase.number, featureNumberOfPhase(selection.candidate.phase, features))}/T${String(selection.candidate.task.number).padStart(3, "0")}`;
|
|
4326
4467
|
return { content: [{ type: "text", text: `Task start denied: ${activeRef} is active. Use task_switch to snapshot and pause it before starting another task.` }], details: selection };
|
|
4327
4468
|
}
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4469
|
+
let resumeProposal;
|
|
4470
|
+
if (selection.kind === "resume" && selection.candidate && selection.candidate.task.id !== task.id) {
|
|
4471
|
+
// Advisory only: a pending resume must be surfaced loudly, but it must
|
|
4472
|
+
// not hard-block an explicit start of a different task. Capture a
|
|
4473
|
+
// structured proposal so the resume-first guidance is explicit and
|
|
4474
|
+
// machine-readable for the host/agent.
|
|
4475
|
+
const resumeTask = selection.candidate;
|
|
4476
|
+
const resumeRef = `${formatPhaseRef(resumeTask.phase.number, featureNumberOfPhase(resumeTask.phase, features))}/T${String(resumeTask.task.number).padStart(3, "0")}`;
|
|
4477
|
+
const resumeSnapshot = resumeTask.task.pauseSnapshot
|
|
4478
|
+
?? project.workDeviations.find((deviation) => deviation.resumeTaskId === resumeTask.task.id
|
|
4479
|
+
&& (deviation.state === "resume-required" || deviation.state === "resolved"))?.snapshot
|
|
4480
|
+
?? null;
|
|
4481
|
+
resumeProposal = buildResumeRequiredProposal({
|
|
4482
|
+
ref: resumeRef,
|
|
4483
|
+
title: resumeTask.task.title,
|
|
4484
|
+
taskId: resumeTask.task.id,
|
|
4485
|
+
phaseId: resumeTask.phase.id,
|
|
4486
|
+
snapshot: resumeSnapshot
|
|
4487
|
+
? { reason: resumeSnapshot.reason, resumeLocation: resumeSnapshot.resumeLocation, howToResume: resumeSnapshot.howToResume }
|
|
4488
|
+
: null,
|
|
4489
|
+
});
|
|
4331
4490
|
}
|
|
4332
4491
|
// Assemble the mandatory parent context before changing task lifecycle state.
|
|
4333
4492
|
// The output order is feature + its requirements, then phase + its requirements.
|
|
@@ -4338,11 +4497,13 @@ export default function planPiExtension(pi) {
|
|
|
4338
4497
|
]);
|
|
4339
4498
|
const phaseContext = buildPhaseContextBlock(phaseWithRequirements, parentFeature, phaseWithRequirements.linkedRequirements ?? [], featureRequirements);
|
|
4340
4499
|
const advisory = selection.candidate && selection.candidate.task.id !== task.id
|
|
4341
|
-
?
|
|
4500
|
+
? selection.kind === "resume"
|
|
4501
|
+
? `\n\n${resumeProposal ? resumeProposal.text : `⚠️ Resume advisory: ${formatPhaseRef(selection.candidate.phase.number, featureNumberOfPhase(selection.candidate.phase, features))}/T${String(selection.candidate.task.number).padStart(3, "0")} has a saved checkpoint. Evaluate its resume context before continuing with this explicit task request. Explicit task request honored.`}`
|
|
4502
|
+
: `\n\n⚠️ Priority advisory: ${formatPhaseRef(selection.candidate.phase.number, featureNumberOfPhase(selection.candidate.phase, features))}/T${String(selection.candidate.task.number).padStart(3, "0")} is the automatic recommendation. Explicit task request honored.`
|
|
4342
4503
|
: "";
|
|
4343
4504
|
const now = nowISO();
|
|
4344
4505
|
let startedTask;
|
|
4345
|
-
if (task.
|
|
4506
|
+
if (task.pauseSnapshot) {
|
|
4346
4507
|
startedTask = await st.resumeTask(found.phase.id, found.task.id, now);
|
|
4347
4508
|
}
|
|
4348
4509
|
else {
|
|
@@ -4376,7 +4537,17 @@ export default function planPiExtension(pi) {
|
|
|
4376
4537
|
await st.writeGenerated();
|
|
4377
4538
|
if (!startedTask)
|
|
4378
4539
|
return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], details: {} };
|
|
4379
|
-
|
|
4540
|
+
const readAdvisory = parentReadAdvisory(parentFeature?.id, found.phase.id);
|
|
4541
|
+
const requirementAdvisory = requirementReadAdvisory([
|
|
4542
|
+
...(phaseWithRequirements.linkedRequirements ?? []).map((r) => r.id),
|
|
4543
|
+
...featureRequirements.map((r) => r.id),
|
|
4544
|
+
]);
|
|
4545
|
+
return {
|
|
4546
|
+
content: [{ type: "text", text: `✅ Task started: ${formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features))}/T${String(startedTask.number).padStart(3, "0")} — ${startedTask.title} (in-progress)${startedTask.shortId ? ` · ${startedTask.shortId}` : ""}${phaseContext}${advisory}${readAdvisory}${requirementAdvisory}` }],
|
|
4547
|
+
details: startedTask,
|
|
4548
|
+
...(resumeProposal ? { resumeRequired: resumeProposal.structured } : {}),
|
|
4549
|
+
...(readAdvisory || requirementAdvisory ? { readRequired: true } : {}),
|
|
4550
|
+
};
|
|
4380
4551
|
},
|
|
4381
4552
|
});
|
|
4382
4553
|
pi.registerTool({
|
|
@@ -4399,8 +4570,8 @@ export default function planPiExtension(pi) {
|
|
|
4399
4570
|
const task = found.task;
|
|
4400
4571
|
if (task.status === "done")
|
|
4401
4572
|
return { content: [{ type: "text", text: `Task ${task.id} is already done.` }], details: task };
|
|
4402
|
-
if (task.
|
|
4403
|
-
return { content: [{ type: "text", text: "Task completion denied: resume
|
|
4573
|
+
if (task.pauseSnapshot)
|
|
4574
|
+
return { content: [{ type: "text", text: "Task completion denied: resume checkpointed work with task_start before completing it, or cancel it explicitly with motivation." }], details: task };
|
|
4404
4575
|
const unchecked = task.checklist.filter((item) => !item.checked);
|
|
4405
4576
|
if (unchecked.length > 0 && !params.force) {
|
|
4406
4577
|
return {
|
|
@@ -4449,7 +4620,7 @@ export default function planPiExtension(pi) {
|
|
|
4449
4620
|
resumeNotice = [
|
|
4450
4621
|
"",
|
|
4451
4622
|
`↩️ RESUME REQUIRED: ${resumeRef} — ${resume.task.title}`,
|
|
4452
|
-
snapshot ? `
|
|
4623
|
+
snapshot ? `Checkpoint reason: ${snapshot.reason}` : "Return to the preserved task before selecting new priority work.",
|
|
4453
4624
|
snapshot ? `Resume from: ${snapshot.resumeLocation}` : "",
|
|
4454
4625
|
snapshot ? `How to resume: ${snapshot.howToResume}` : "",
|
|
4455
4626
|
`Next action: task_start ${resumeRef}`,
|
|
@@ -4636,7 +4807,7 @@ export default function planPiExtension(pi) {
|
|
|
4636
4807
|
const st = await requirePlan(ctx);
|
|
4637
4808
|
if (!st)
|
|
4638
4809
|
return { content: [{ type: "text", text: "No .planner/ found. Run plan_init first." }], details: { enabled: false, running: false } };
|
|
4639
|
-
|
|
4810
|
+
enablePlannerSession();
|
|
4640
4811
|
if (!server) {
|
|
4641
4812
|
await startServer(ctx, undefined, "lan").catch(() => { });
|
|
4642
4813
|
}
|
|
@@ -4708,6 +4879,7 @@ export default function planPiExtension(pi) {
|
|
|
4708
4879
|
const activity = await st.loadActivityLog();
|
|
4709
4880
|
const recentActivity = activity.entries.slice(-3).reverse();
|
|
4710
4881
|
const handoffs = await st.listHandoffs();
|
|
4882
|
+
const extensionRules = await st.extensionRules();
|
|
4711
4883
|
const featureById = new Map(plan.features.features.map((feature) => [feature.id, feature]));
|
|
4712
4884
|
const phaseById = new Map(plan.phases.map((phase) => [phase.id, phase]));
|
|
4713
4885
|
const orderedPhases = [
|
|
@@ -4828,6 +5000,13 @@ export default function planPiExtension(pi) {
|
|
|
4828
5000
|
"- Planner ops (status/handoff/planner metadata) are NOT code edits; they are always allowed.",
|
|
4829
5001
|
"- Prioritize work: continue an in-progress task; otherwise choose ready work feature → phase → task by ascending priority, respecting dependencies and blocked/waiting states. Prefer shortId or F00x/P00x/T00x refs. Find via feature_list / phase_list / task_list; read one entity via *_get(full=true).",
|
|
4830
5002
|
"- If edit/write guard blocks you, start the right task or use an explicit bypass.",
|
|
5003
|
+
...(extensionRules.length > 0
|
|
5004
|
+
? [
|
|
5005
|
+
"",
|
|
5006
|
+
"Planner rules (extension; agent-only):",
|
|
5007
|
+
...extensionRules.map((rule, index) => `${index + 1}. ${rule}`),
|
|
5008
|
+
]
|
|
5009
|
+
: []),
|
|
4831
5010
|
"",
|
|
4832
5011
|
`Goal: ${project.goal || "(not set)"}`,
|
|
4833
5012
|
project.description ? `Description: ${project.description}` : "",
|