@agent-plan/pi-adapter 0.2.19-next.16 → 0.2.19-next.17
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 +331 -130
- package/package.json +3 -3
- package/web-ui-dist/assets/index-DG2XF_Sy.css +1 -0
- package/web-ui-dist/assets/index-DS7Qrq0d.js +293 -0
- package/web-ui-dist/index.html +3 -2
- package/web-ui-dist/assets/index-CZs-bw22.js +0 -263
- package/web-ui-dist/assets/index-zZFIH6DD.css +0 -1
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { Type } from "typebox";
|
|
12
12
|
import { ExportService, PlanStore, setWriteBusyHook, setWriteNotifyHook, migrateToUuids, migrateToGlobalSequence, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, buildRecap, addChecklistItem, removeChecklistItem, toggleChecklistItem } from "@agent-plan/core";
|
|
13
|
-
import { createChecklistItemId, createFeatureId, createPhaseId, createShortId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef, featureNumberOfPhase } from "@agent-plan/core/naming";
|
|
13
|
+
import { createChecklistItemId, createFeatureId, createPhaseId, createShortId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef, featureNumberOfPhase, validateResolvedTarget } 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";
|
|
@@ -105,11 +105,13 @@ const PLANNER_COMMAND_COMPLETIONS = [
|
|
|
105
105
|
{ value: "init", label: "init", description: "Initialize planner in this project" },
|
|
106
106
|
{ value: "show", label: "show", description: "Show planner overview" },
|
|
107
107
|
{ value: "repair", label: "repair", description: "Repair planner integrity" },
|
|
108
|
+
{ value: "cleanup-orphans", label: "cleanup-orphans", description: "List and remove orphan phase files" },
|
|
108
109
|
{ value: "project discuss", label: "project discuss", description: "Run project discovery" },
|
|
109
110
|
{ value: "project language", label: "project language", description: "Set persistent language preferences" },
|
|
110
111
|
{ value: "feature list", label: "feature list", description: "List features" },
|
|
111
112
|
{ value: "feature add", label: "feature add", description: "Create a feature" },
|
|
112
113
|
{ value: "feature show", label: "feature show", description: "Show a feature" },
|
|
114
|
+
{ value: "feature discuss", label: "feature discuss", description: "Discuss a feature" },
|
|
113
115
|
{ value: "feature update", label: "feature update", description: "Update a feature" },
|
|
114
116
|
{ value: "feature delete", label: "feature delete", description: "Delete a feature" },
|
|
115
117
|
{ value: "phase add", label: "phase add", description: "Add a phase" },
|
|
@@ -253,6 +255,34 @@ function phaseLabel(phase) {
|
|
|
253
255
|
function taskLabel(task) {
|
|
254
256
|
return `T${formatSequence(task.number)} — ${task.title}`;
|
|
255
257
|
}
|
|
258
|
+
function resolveFeatureRefStrict(features, ref) {
|
|
259
|
+
const raw = ref.trim();
|
|
260
|
+
if (!raw)
|
|
261
|
+
return { ok: false, error: "Feature ref is required." };
|
|
262
|
+
const normalized = raw.toLowerCase();
|
|
263
|
+
const byNumber = normalized.match(/^f(\d+)$/)
|
|
264
|
+
? features.find((feature) => feature.number === parseInt(normalized.slice(1), 10))
|
|
265
|
+
: undefined;
|
|
266
|
+
if (byNumber)
|
|
267
|
+
return { ok: true, feature: byNumber };
|
|
268
|
+
const byShortId = features.find((feature) => feature.shortId?.toLowerCase() === normalized);
|
|
269
|
+
if (byShortId)
|
|
270
|
+
return { ok: true, feature: byShortId };
|
|
271
|
+
const byId = features.find((feature) => feature.id.toLowerCase() === normalized);
|
|
272
|
+
if (byId)
|
|
273
|
+
return { ok: true, feature: byId };
|
|
274
|
+
const exactName = features.filter((feature) => feature.name.toLowerCase() === normalized);
|
|
275
|
+
if (exactName.length === 1)
|
|
276
|
+
return { ok: true, feature: exactName[0] };
|
|
277
|
+
if (exactName.length > 1)
|
|
278
|
+
return { ok: false, error: `Ambiguous feature ref: ${raw}. Multiple features have that exact name; use F00x, shortId, or UUID.` };
|
|
279
|
+
const partialName = features.filter((feature) => feature.name.toLowerCase().includes(normalized));
|
|
280
|
+
if (partialName.length === 1)
|
|
281
|
+
return { ok: true, feature: partialName[0] };
|
|
282
|
+
if (partialName.length > 1)
|
|
283
|
+
return { ok: false, error: `Ambiguous feature ref: ${raw}. Matches: ${partialName.map((feature) => formatFeatureRef(feature.number)).join(", ")}. Use a specific F00x, shortId, or UUID.` };
|
|
284
|
+
return { ok: false, error: `Feature not found: ${raw}` };
|
|
285
|
+
}
|
|
256
286
|
async function buildHandoffMarkdown(st, reason, overrides) {
|
|
257
287
|
const [plan, resume, activity] = await Promise.all([
|
|
258
288
|
st.loadAll(),
|
|
@@ -750,9 +780,10 @@ export default function planPiExtension(pi) {
|
|
|
750
780
|
// ── Step 1: Enable gating ───────────────────────────────────────
|
|
751
781
|
// No blocking prompt in session_start: ctx.ui.input()/select() do not
|
|
752
782
|
// render for NPM-loaded ESM extensions during session_start (they hang
|
|
753
|
-
// until the safety-net timeout).
|
|
754
|
-
//
|
|
755
|
-
//
|
|
783
|
+
// until the safety-net timeout). Tracked upstream via planner task
|
|
784
|
+
// T151 · EBRST. The planner is always DISABLED at startup; the user
|
|
785
|
+
// enables it with '/planner load' (which also starts the web UI on LAN
|
|
786
|
+
// and triggers the resume summary showing the URL).
|
|
756
787
|
let enablePlanner = false;
|
|
757
788
|
if (exists) {
|
|
758
789
|
ctx.ui.notify("Planner detected in this project. Run '/planner load' to enable the planner and start the web UI (LAN).", "info");
|
|
@@ -945,6 +976,7 @@ export default function planPiExtension(pi) {
|
|
|
945
976
|
"init",
|
|
946
977
|
"show",
|
|
947
978
|
"repair",
|
|
979
|
+
"cleanup-orphans",
|
|
948
980
|
"project discuss",
|
|
949
981
|
"project language",
|
|
950
982
|
"feature list",
|
|
@@ -978,8 +1010,8 @@ export default function planPiExtension(pi) {
|
|
|
978
1010
|
"load",
|
|
979
1011
|
"stop",
|
|
980
1012
|
];
|
|
981
|
-
const SUB_HELP = "Available: init, show, repair, project, feature, phase, task, discuss, handoff, web, export, export-full, bypass, clear-bypass, load, stop\n" +
|
|
982
|
-
"Try: /planner <TAB> | /planner feature list | /planner task start | /planner
|
|
1013
|
+
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" +
|
|
1014
|
+
"Try: /planner <TAB> | /planner feature list | /planner task start | /planner cleanup-orphans\n" +
|
|
983
1015
|
"Handoff actions: /planner handoff list | show [P00x] | write [P00x] | clear [P00x] | prepare";
|
|
984
1016
|
if (!a) {
|
|
985
1017
|
const action = await ctx.ui.select("Planner action", PLANNER_MENU_ACTIONS);
|
|
@@ -1135,6 +1167,34 @@ export default function planPiExtension(pi) {
|
|
|
1135
1167
|
?? features.find((feature) => feature.name.toLowerCase().includes(normalized))
|
|
1136
1168
|
?? null;
|
|
1137
1169
|
}
|
|
1170
|
+
function resolveFeatureRef(features, ref) {
|
|
1171
|
+
const raw = ref.trim();
|
|
1172
|
+
if (!raw)
|
|
1173
|
+
return { ok: false, error: "Feature ref is required." };
|
|
1174
|
+
const normalized = raw.toLowerCase();
|
|
1175
|
+
const byNumber = normalized.match(/^f(\d+)$/)
|
|
1176
|
+
? features.find((feature) => feature.number === parseInt(normalized.slice(1), 10))
|
|
1177
|
+
: undefined;
|
|
1178
|
+
if (byNumber)
|
|
1179
|
+
return { ok: true, feature: byNumber };
|
|
1180
|
+
const byShortId = features.find((feature) => feature.shortId?.toLowerCase() === normalized);
|
|
1181
|
+
if (byShortId)
|
|
1182
|
+
return { ok: true, feature: byShortId };
|
|
1183
|
+
const byId = features.find((feature) => feature.id.toLowerCase() === normalized);
|
|
1184
|
+
if (byId)
|
|
1185
|
+
return { ok: true, feature: byId };
|
|
1186
|
+
const exactName = features.filter((feature) => feature.name.toLowerCase() === normalized);
|
|
1187
|
+
if (exactName.length == 1)
|
|
1188
|
+
return { ok: true, feature: exactName[0] };
|
|
1189
|
+
if (exactName.length > 1)
|
|
1190
|
+
return { ok: false, error: `Ambiguous feature ref: ${raw}. Multiple features have that exact name; use F00x, shortId, or UUID.` };
|
|
1191
|
+
const partialName = features.filter((feature) => feature.name.toLowerCase().includes(normalized));
|
|
1192
|
+
if (partialName.length == 1)
|
|
1193
|
+
return { ok: true, feature: partialName[0] };
|
|
1194
|
+
if (partialName.length > 1)
|
|
1195
|
+
return { ok: false, error: `Ambiguous feature ref: ${raw}. Matches: ${partialName.map((feature) => formatFeatureRef(feature.number)).join(", ")}. Use a specific F00x, shortId, or UUID.` };
|
|
1196
|
+
return { ok: false, error: `Feature not found: ${raw}` };
|
|
1197
|
+
}
|
|
1138
1198
|
function findPhaseByRef(phases, ref) {
|
|
1139
1199
|
const normalized = ref.trim().toLowerCase();
|
|
1140
1200
|
if (!normalized)
|
|
@@ -1324,7 +1384,7 @@ export default function planPiExtension(pi) {
|
|
|
1324
1384
|
// ═══════════════════════════════════════════════════════════════
|
|
1325
1385
|
if (a === "feature") {
|
|
1326
1386
|
if (!b) {
|
|
1327
|
-
ctx.ui.notify("feature actions: list | add [name] | show [id|name] | update [id|name] | delete [id|name]", "info");
|
|
1387
|
+
ctx.ui.notify("feature actions: list | add [name] | show [id|name] | discuss [id|name] | update [id|name] | delete [id|name]", "info");
|
|
1328
1388
|
return;
|
|
1329
1389
|
}
|
|
1330
1390
|
if (b === "list") {
|
|
@@ -1415,6 +1475,44 @@ export default function planPiExtension(pi) {
|
|
|
1415
1475
|
].filter(Boolean).join("\n"), "info");
|
|
1416
1476
|
return;
|
|
1417
1477
|
}
|
|
1478
|
+
if (b === "discuss") {
|
|
1479
|
+
const featuresDoc = await st.loadFeatures();
|
|
1480
|
+
let feature = subArgs.trim() ? findFeatureByRef(featuresDoc.features, subArgs.trim()) : null;
|
|
1481
|
+
if (!feature) {
|
|
1482
|
+
feature = await pickFeature();
|
|
1483
|
+
if (!feature) {
|
|
1484
|
+
ctx.ui.notify("No features to discuss. Create one first.", "warning");
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
const description = await ctx.ui.editor("Feature description / scope / current implementation state", feature.description || "");
|
|
1489
|
+
const workDone = await ctx.ui.editor("Work done / already decided", feature.workDone || "");
|
|
1490
|
+
const workRemaining = await ctx.ui.editor("Work remaining / next work", feature.workRemaining || "");
|
|
1491
|
+
const dependencies = await ctx.ui.input(`Dependencies (comma-separated) [${feature.dependsOn.join(", ") || ""}]`);
|
|
1492
|
+
const featureId = feature.id;
|
|
1493
|
+
const updatedDoc = await st.updateFeatures((doc) => {
|
|
1494
|
+
const target = doc.features.find((entry) => entry.id === featureId);
|
|
1495
|
+
if (!target)
|
|
1496
|
+
return doc;
|
|
1497
|
+
if (description !== undefined)
|
|
1498
|
+
target.description = description.trim();
|
|
1499
|
+
if (workDone !== undefined)
|
|
1500
|
+
target.workDone = workDone.trim();
|
|
1501
|
+
if (workRemaining !== undefined)
|
|
1502
|
+
target.workRemaining = workRemaining.trim();
|
|
1503
|
+
if (dependencies !== undefined)
|
|
1504
|
+
target.dependsOn = splitCsv(dependencies);
|
|
1505
|
+
target.discussedAt = nowISO();
|
|
1506
|
+
target.contextReady = true;
|
|
1507
|
+
target.contextReadyReason = "Updated through /planner feature discuss.";
|
|
1508
|
+
target.updatedAt = nowISO();
|
|
1509
|
+
return doc;
|
|
1510
|
+
});
|
|
1511
|
+
feature = updatedDoc.features.find((entry) => entry.id === featureId) ?? feature;
|
|
1512
|
+
await st.writeGenerated();
|
|
1513
|
+
ctx.ui.notify(`Feature discussed/updated: ${formatFeatureRef(feature.number)}${feature.shortId ? ` +MI+ ${feature.shortId}` : ""} +EM+ ${feature.name}`, "info");
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1418
1516
|
if (b === "update") {
|
|
1419
1517
|
const featuresDoc = await st.loadFeatures();
|
|
1420
1518
|
let feature = subArgs.trim() ? findFeatureByRef(featuresDoc.features, subArgs.trim()) : null;
|
|
@@ -1513,7 +1611,7 @@ export default function planPiExtension(pi) {
|
|
|
1513
1611
|
ctx.ui.notify(`Feature deleted: ${featureId}${phases.length > 0 ? cascade ? `; deleted ${affectedPhases} phase(s)` : `; unlinked ${affectedPhases} phase(s)` : ""}`, "info");
|
|
1514
1612
|
return;
|
|
1515
1613
|
}
|
|
1516
|
-
ctx.ui.notify(`Unknown feature action "${b}". Try: list, add, show, update, delete`, "warning");
|
|
1614
|
+
ctx.ui.notify(`Unknown feature action "${b}". Try: list, add, show, discuss, update, delete`, "warning");
|
|
1517
1615
|
return;
|
|
1518
1616
|
}
|
|
1519
1617
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -2299,6 +2397,25 @@ export default function planPiExtension(pi) {
|
|
|
2299
2397
|
ctx.ui.notify(`Repair done: renamed ${m.renamed}, repaired ${m.repaired} refs, inferred ${m.inferred}. Integrity: ${dup} duplicate, ${dang} dangling.`, "info");
|
|
2300
2398
|
return;
|
|
2301
2399
|
}
|
|
2400
|
+
if (a === "cleanup-orphans") {
|
|
2401
|
+
const found = await st.listOrphanPhases();
|
|
2402
|
+
if (found.length === 0) {
|
|
2403
|
+
ctx.ui.notify("No orphan phases found.", "info");
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
const lines = [
|
|
2407
|
+
`Found ${found.length} orphan phase${found.length === 1 ? "" : "s"}:`,
|
|
2408
|
+
...found.map((phase) => `- ${phase.compositeRef}${phase.shortId ? ` · ${phase.shortId}` : ""} — ${phase.title} (${phase.reason})`),
|
|
2409
|
+
];
|
|
2410
|
+
const confirmed = await ctx.ui.confirm(lines.join("\n\n"), "Delete orphan phase files?");
|
|
2411
|
+
if (!confirmed) {
|
|
2412
|
+
ctx.ui.notify("Cleanup aborted.", "warning");
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
const report = await st.cleanupOrphanPhases();
|
|
2416
|
+
ctx.ui.notify(`Removed ${report.removed.length} orphan phase${report.removed.length === 1 ? "" : "s"}.`, "info");
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2302
2419
|
// ── export ──
|
|
2303
2420
|
if (a === "export" || a === "export-full") {
|
|
2304
2421
|
const isFull = a === "export-full" || subArgs.includes("--full");
|
|
@@ -2703,6 +2820,38 @@ export default function planPiExtension(pi) {
|
|
|
2703
2820
|
return { content: [{ type: "text", text: lines.join("\n") }], details: report };
|
|
2704
2821
|
},
|
|
2705
2822
|
});
|
|
2823
|
+
pi.registerTool({
|
|
2824
|
+
name: "plan_cleanup_orphan_phases",
|
|
2825
|
+
label: "Plan Cleanup Orphan Phases",
|
|
2826
|
+
description: "Discover phase files that no longer resolve to a valid owning feature, and optionally delete them. Run once with confirm=false (default) to inspect, then rerun with confirm=true to remove them.",
|
|
2827
|
+
parameters: Type.Object({
|
|
2828
|
+
confirm: Type.Optional(Type.Boolean({ description: "Set true to actually delete the discovered orphan phase files. Default: false (dry-run/list only)." })),
|
|
2829
|
+
}),
|
|
2830
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
2831
|
+
const st = await requirePlan(ctx);
|
|
2832
|
+
if (!st)
|
|
2833
|
+
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
2834
|
+
const found = await st.listOrphanPhases();
|
|
2835
|
+
if (!params.confirm) {
|
|
2836
|
+
if (found.length === 0)
|
|
2837
|
+
return { content: [{ type: "text", text: "No orphan phases found." }], details: { found: [] } };
|
|
2838
|
+
const lines = [
|
|
2839
|
+
`Found ${found.length} orphan phase${found.length === 1 ? "" : "s"}.`,
|
|
2840
|
+
...found.map((phase) => `- ${phase.compositeRef}${phase.shortId ? ` · ${phase.shortId}` : ""} — ${phase.title} (${phase.reason})`),
|
|
2841
|
+
"Rerun with confirm=true to delete these orphan phase files.",
|
|
2842
|
+
];
|
|
2843
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { found, confirmRequired: true } };
|
|
2844
|
+
}
|
|
2845
|
+
const report = await st.cleanupOrphanPhases();
|
|
2846
|
+
if (report.removed.length === 0)
|
|
2847
|
+
return { content: [{ type: "text", text: "No orphan phases found." }], details: report };
|
|
2848
|
+
const lines = [
|
|
2849
|
+
`Removed ${report.removed.length} orphan phase${report.removed.length === 1 ? "" : "s"}.`,
|
|
2850
|
+
...report.removed.map((phase) => `- ${phase.compositeRef}${phase.shortId ? ` · ${phase.shortId}` : ""} — ${phase.title}`),
|
|
2851
|
+
];
|
|
2852
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: report };
|
|
2853
|
+
},
|
|
2854
|
+
});
|
|
2706
2855
|
pi.registerTool({
|
|
2707
2856
|
name: "plan_get_handoff",
|
|
2708
2857
|
label: "Plan Get Handoff",
|
|
@@ -2991,6 +3140,51 @@ export default function planPiExtension(pi) {
|
|
|
2991
3140
|
return { content: [{ type: "text", text: `✅ Feature created: ${formatFeatureRef(feature.number)} — ${feature.name}${feature.shortId ? ` · ${feature.shortId}` : ""}` }], details: feature };
|
|
2992
3141
|
},
|
|
2993
3142
|
});
|
|
3143
|
+
pi.registerTool({
|
|
3144
|
+
name: "feature_discuss",
|
|
3145
|
+
label: "Feature Discuss",
|
|
3146
|
+
description: "Persist feature discovery/governance fields so the feature context is ready before detailed implementation planning.",
|
|
3147
|
+
parameters: Type.Object({
|
|
3148
|
+
featureId: Type.String({ description: "Feature ref: F00x, shortId, UUID, or name" }),
|
|
3149
|
+
description: Type.Optional(Type.String({ description: "Current implementation state, scope, and goals for this feature" })),
|
|
3150
|
+
workDone: Type.Optional(Type.String({ description: "What is already implemented / decided" })),
|
|
3151
|
+
workRemaining: Type.Optional(Type.String({ description: "What still needs to be done" })),
|
|
3152
|
+
dependencies: Type.Optional(Type.Array(Type.String(), { description: "Cross-feature or external dependencies" })),
|
|
3153
|
+
}),
|
|
3154
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3155
|
+
const st = await requirePlan(ctx);
|
|
3156
|
+
if (!st)
|
|
3157
|
+
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
3158
|
+
const features = (await st.loadFeatures()).features;
|
|
3159
|
+
const resolvedFeature = resolveFeatureRefStrict(features, params.featureId);
|
|
3160
|
+
if (!resolvedFeature.ok)
|
|
3161
|
+
return { content: [{ type: "text", text: resolvedFeature.error }], details: {} };
|
|
3162
|
+
const featureId = resolvedFeature.feature.id;
|
|
3163
|
+
const updatedDoc = await st.updateFeatures((doc) => {
|
|
3164
|
+
const feature = doc.features.find((f) => f.id === featureId);
|
|
3165
|
+
if (!feature)
|
|
3166
|
+
return doc;
|
|
3167
|
+
if (params.description !== undefined)
|
|
3168
|
+
feature.description = params.description;
|
|
3169
|
+
if (params.workDone !== undefined)
|
|
3170
|
+
feature.workDone = params.workDone;
|
|
3171
|
+
if (params.workRemaining !== undefined)
|
|
3172
|
+
feature.workRemaining = params.workRemaining;
|
|
3173
|
+
if (params.dependencies !== undefined)
|
|
3174
|
+
feature.dependsOn = params.dependencies.map((item) => item.trim()).filter(Boolean);
|
|
3175
|
+
feature.discussedAt = nowISO();
|
|
3176
|
+
feature.contextReady = true;
|
|
3177
|
+
feature.contextReadyReason = "Updated through feature_discuss tool.";
|
|
3178
|
+
feature.updatedAt = nowISO();
|
|
3179
|
+
return doc;
|
|
3180
|
+
});
|
|
3181
|
+
const feature = updatedDoc.features.find((f) => f.id === featureId);
|
|
3182
|
+
if (!feature)
|
|
3183
|
+
return { content: [{ type: "text", text: `Feature not found: ${params.featureId}` }], details: {} };
|
|
3184
|
+
await st.writeGenerated();
|
|
3185
|
+
return { content: [{ type: "text", text: `✅ Feature discussed/updated: ${formatFeatureRef(feature.number)} — ${feature.name}${feature.shortId ? ` · ${feature.shortId}` : ""}` }], details: feature };
|
|
3186
|
+
},
|
|
3187
|
+
});
|
|
2994
3188
|
pi.registerTool({
|
|
2995
3189
|
name: "feature_update",
|
|
2996
3190
|
label: "Feature Update",
|
|
@@ -3019,10 +3213,10 @@ export default function planPiExtension(pi) {
|
|
|
3019
3213
|
if (!st)
|
|
3020
3214
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
3021
3215
|
const features = (await st.loadFeatures()).features;
|
|
3022
|
-
const resolvedFeature = features
|
|
3023
|
-
if (!resolvedFeature)
|
|
3024
|
-
return { content: [{ type: "text", text:
|
|
3025
|
-
const featureId = resolvedFeature.id;
|
|
3216
|
+
const resolvedFeature = resolveFeatureRefStrict(features, params.featureId);
|
|
3217
|
+
if (!resolvedFeature.ok)
|
|
3218
|
+
return { content: [{ type: "text", text: resolvedFeature.error }], details: {} };
|
|
3219
|
+
const featureId = resolvedFeature.feature.id;
|
|
3026
3220
|
let foundDoc;
|
|
3027
3221
|
try {
|
|
3028
3222
|
foundDoc = await st.updateFeatures((doc) => {
|
|
@@ -3084,10 +3278,10 @@ export default function planPiExtension(pi) {
|
|
|
3084
3278
|
if (!st)
|
|
3085
3279
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
3086
3280
|
const features = (await st.loadFeatures()).features;
|
|
3087
|
-
const resolvedFeature = features
|
|
3088
|
-
if (!resolvedFeature)
|
|
3089
|
-
return { content: [{ type: "text", text:
|
|
3090
|
-
const featureId = resolvedFeature.id;
|
|
3281
|
+
const resolvedFeature = resolveFeatureRefStrict(features, params.featureId);
|
|
3282
|
+
if (!resolvedFeature.ok)
|
|
3283
|
+
return { content: [{ type: "text", text: resolvedFeature.error }], details: {} };
|
|
3284
|
+
const featureId = resolvedFeature.feature.id;
|
|
3091
3285
|
let deleted = false;
|
|
3092
3286
|
await st.updateFeatures((doc) => {
|
|
3093
3287
|
const before = doc.features.length;
|
|
@@ -3143,7 +3337,7 @@ export default function planPiExtension(pi) {
|
|
|
3143
3337
|
pi.registerTool({
|
|
3144
3338
|
name: "phase_get",
|
|
3145
3339
|
label: "Phase Get",
|
|
3146
|
-
description: "Show a phase. Compact identity by default (saves tokens); pass full=true to include the description.",
|
|
3340
|
+
description: "Show a phase. Compact identity by default (saves tokens); pass full=true to include the description and derived linked requirements.",
|
|
3147
3341
|
parameters: Type.Object({
|
|
3148
3342
|
phaseId: Type.String({ description: "Phase ref: F00x/P00x, bare P00x (global), shortId, UUID, or title" }),
|
|
3149
3343
|
full: Type.Optional(Type.Boolean({ description: "If true, include the phase description. Default: compact identity only." })),
|
|
@@ -3156,8 +3350,13 @@ export default function planPiExtension(pi) {
|
|
|
3156
3350
|
const phase = findPhaseByRef(await st.loadAllPhases(), features, params.phaseId.trim());
|
|
3157
3351
|
if (!phase)
|
|
3158
3352
|
return { content: [{ type: "text", text: `Phase not found: ${params.phaseId}` }], details: {} };
|
|
3159
|
-
const
|
|
3160
|
-
|
|
3353
|
+
const linkedRequirements = await st.linkedRequirementsForPhase(phase.id);
|
|
3354
|
+
const reqCount = linkedRequirements.length;
|
|
3355
|
+
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"}` : ""})`;
|
|
3356
|
+
const requirementsBlock = reqCount > 0
|
|
3357
|
+
? `\n\nLinked requirements:\n${linkedRequirements.map((requirement) => `- ${requirement.title} (${requirement.status})`).join("\n")}`
|
|
3358
|
+
: "";
|
|
3359
|
+
return { content: [{ type: "text", text: params.full ? `${summary}\n\n${phase.description || ""}${requirementsBlock}` : summary }], details: { linkedRequirements } };
|
|
3161
3360
|
},
|
|
3162
3361
|
});
|
|
3163
3362
|
pi.registerTool({
|
|
@@ -3177,18 +3376,27 @@ export default function planPiExtension(pi) {
|
|
|
3177
3376
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
3178
3377
|
if (!params.featureId?.trim())
|
|
3179
3378
|
return { content: [{ type: "text", text: "featureId is required: a phase must belong to a feature." }], details: {} };
|
|
3379
|
+
const createPhaseFeatures = (await st.loadFeatures()).features;
|
|
3380
|
+
const resolvedFeature = resolveFeatureRefStrict(createPhaseFeatures, params.featureId);
|
|
3381
|
+
if (!resolvedFeature.ok)
|
|
3382
|
+
return { content: [{ type: "text", text: resolvedFeature.error }], details: {} };
|
|
3383
|
+
const featureId = resolvedFeature.feature.id;
|
|
3384
|
+
const featureValidation = await validateResolvedTarget("feature", featureId, () => st.loadFeatures().then((doc) => doc.features.find((f) => f.id === featureId)).catch(() => undefined));
|
|
3385
|
+
if (!featureValidation.ok)
|
|
3386
|
+
return { content: [{ type: "text", text: featureValidation.error }], details: {} };
|
|
3387
|
+
const existingFeature = (await st.loadFeatures()).features.find((f) => f.id === featureId);
|
|
3388
|
+
if (!existingFeature)
|
|
3389
|
+
return { content: [{ type: "text", text: `Resolved feature ${featureId} no longer exists. Refusing to create phase.` }], details: {} };
|
|
3180
3390
|
let phase;
|
|
3181
|
-
await withFeatureLock(
|
|
3182
|
-
const phases = await st.loadAllPhases();
|
|
3183
|
-
const featurePhases = phases.filter((p) => p.featureId === params.featureId);
|
|
3391
|
+
await withFeatureLock(featureId, async () => {
|
|
3184
3392
|
const number = await st.allocPhaseNumber();
|
|
3185
3393
|
const id = createPhaseId();
|
|
3186
3394
|
const shortId = createShortId(await st.assignedShortIds());
|
|
3187
|
-
const priority = await st.nextPriority("phase",
|
|
3395
|
+
const priority = await st.nextPriority("phase", featureId);
|
|
3188
3396
|
const now = nowISO();
|
|
3189
3397
|
phase = {
|
|
3190
3398
|
id, number, shortId, priority, slug: normalizeSlug(params.title), title: params.title,
|
|
3191
|
-
featureId
|
|
3399
|
+
featureId,
|
|
3192
3400
|
status: params.status ?? "draft",
|
|
3193
3401
|
discussedAt: "",
|
|
3194
3402
|
contextReady: false,
|
|
@@ -3205,7 +3413,7 @@ export default function planPiExtension(pi) {
|
|
|
3205
3413
|
await st.savePhase(phase);
|
|
3206
3414
|
// Atomic: serialize concurrent phase_create linking to the same feature.
|
|
3207
3415
|
await st.updateFeatures((doc) => {
|
|
3208
|
-
const feature = doc.features.find((f) => f.id ===
|
|
3416
|
+
const feature = doc.features.find((f) => f.id === featureId);
|
|
3209
3417
|
if (feature && !feature.phaseIds.includes(phase.id)) {
|
|
3210
3418
|
feature.phaseIds.push(phase.id);
|
|
3211
3419
|
feature.updatedAt = now;
|
|
@@ -3265,6 +3473,7 @@ export default function planPiExtension(pi) {
|
|
|
3265
3473
|
return { content: [{ type: "text", text: `Phase not found: ${params.phaseId}` }], details: {} };
|
|
3266
3474
|
}
|
|
3267
3475
|
const prevFeatureId = phase.featureId;
|
|
3476
|
+
let nextFeatureIdForUpdate = prevFeatureId;
|
|
3268
3477
|
if (params.title !== undefined) {
|
|
3269
3478
|
phase.title = params.title;
|
|
3270
3479
|
phase.slug = normalizeSlug(params.title);
|
|
@@ -3294,14 +3503,22 @@ export default function planPiExtension(pi) {
|
|
|
3294
3503
|
if (params.completionCriteria !== undefined)
|
|
3295
3504
|
phase.completionCriteria = params.completionCriteria;
|
|
3296
3505
|
if (params.featureId !== undefined) {
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3506
|
+
if (params.featureId === "") {
|
|
3507
|
+
nextFeatureIdForUpdate = undefined;
|
|
3508
|
+
}
|
|
3509
|
+
else {
|
|
3510
|
+
const resolvedFeature = resolveFeatureRefStrict(features, params.featureId);
|
|
3511
|
+
if (!resolvedFeature.ok)
|
|
3512
|
+
return { content: [{ type: "text", text: resolvedFeature.error }], details: {} };
|
|
3513
|
+
nextFeatureIdForUpdate = resolvedFeature.feature.id;
|
|
3514
|
+
}
|
|
3515
|
+
phase.featureId = nextFeatureIdForUpdate;
|
|
3516
|
+
if (nextFeatureIdForUpdate !== prevFeatureId) {
|
|
3300
3517
|
await st.updateFeatures((doc) => {
|
|
3301
3518
|
for (const f of doc.features) {
|
|
3302
3519
|
if (f.id === prevFeatureId)
|
|
3303
3520
|
f.phaseIds = f.phaseIds.filter((pid) => pid !== phase.id);
|
|
3304
|
-
if (f.id ===
|
|
3521
|
+
if (f.id === nextFeatureIdForUpdate && !f.phaseIds.includes(phase.id))
|
|
3305
3522
|
f.phaseIds.push(phase.id);
|
|
3306
3523
|
}
|
|
3307
3524
|
return doc;
|
|
@@ -3439,13 +3656,18 @@ export default function planPiExtension(pi) {
|
|
|
3439
3656
|
if (!resolvedPhase)
|
|
3440
3657
|
return { content: [{ type: "text", text: `Phase not found: ${params.phaseId}` }], details: {} };
|
|
3441
3658
|
const phaseId = resolvedPhase.id;
|
|
3659
|
+
const phaseValidation = await validateResolvedTarget("phase", phaseId, () => st.loadPhase(phaseId).catch(() => undefined));
|
|
3660
|
+
if (!phaseValidation.ok)
|
|
3661
|
+
return { content: [{ type: "text", text: phaseValidation.error }], details: {} };
|
|
3662
|
+
const existingPhase = await st.loadPhase(phaseId);
|
|
3663
|
+
if (!existingPhase)
|
|
3664
|
+
return { content: [{ type: "text", text: `Resolved phase ${phaseId} no longer exists. Refusing to create task.` }], details: {} };
|
|
3442
3665
|
const shortName = clampSlug(params.shortName ?? params.title, 30, `task-${Date.now().toString(36)}`); // clamp+strip trailing dash; never empty
|
|
3443
3666
|
const taskId = createTaskId();
|
|
3444
3667
|
const shortId = createShortId(await st.assignedShortIds());
|
|
3445
3668
|
const priority = await st.nextPriority("task", phaseId);
|
|
3446
3669
|
const now = nowISO();
|
|
3447
3670
|
const initialStatus = params.status ?? "planned";
|
|
3448
|
-
const existingPhase = await st.loadPhase(phaseId);
|
|
3449
3671
|
const task = {
|
|
3450
3672
|
id: taskId, phaseId, number: await st.allocTaskNumber(), shortId, priority, shortName,
|
|
3451
3673
|
title: params.title,
|
|
@@ -3926,8 +4148,9 @@ export default function planPiExtension(pi) {
|
|
|
3926
4148
|
const profile = await st.loadCodebaseProfile();
|
|
3927
4149
|
const resume = await st.loadResume().catch(() => null) ?? await st.refreshResume();
|
|
3928
4150
|
const activity = await st.loadActivityLog();
|
|
3929
|
-
const recentActivity = activity.entries.slice(-
|
|
4151
|
+
const recentActivity = activity.entries.slice(-3).reverse();
|
|
3930
4152
|
const handoffs = await st.listHandoffs();
|
|
4153
|
+
const featureById = new Map(plan.features.features.map((feature) => [feature.id, feature]));
|
|
3931
4154
|
const phaseById = new Map(plan.phases.map((phase) => [phase.id, phase]));
|
|
3932
4155
|
const orderedPhases = [
|
|
3933
4156
|
...plan.features.features.flatMap((feature) => {
|
|
@@ -3940,21 +4163,9 @@ export default function planPiExtension(pi) {
|
|
|
3940
4163
|
...plan.phases.filter((phase) => !phase.featureId)
|
|
3941
4164
|
.sort((left, right) => left.number - right.number || left.createdAt.localeCompare(right.createdAt)),
|
|
3942
4165
|
];
|
|
3943
|
-
const
|
|
3944
|
-
.
|
|
3945
|
-
.
|
|
3946
|
-
const active = orderedPhases
|
|
3947
|
-
.filter((p) => p.status === "in-progress")
|
|
3948
|
-
.map((p) => `\`${p.id}\``)
|
|
3949
|
-
.join(", ");
|
|
3950
|
-
const doneFeaturesCount = plan.features.features.filter((f) => f.status === "done").length;
|
|
3951
|
-
const activeFeaturesCount = plan.features.features.filter((f) => f.status === "in-progress").length;
|
|
3952
|
-
const donePhasesCount = plan.phases.filter((p) => p.status === "done").length;
|
|
3953
|
-
const activePhasesCount = plan.phases.filter((p) => p.status === "in-progress" || p.status === "discovery").length;
|
|
3954
|
-
const totalTasksCount = orderedPhases.reduce((total, phase) => total + phase.tasks.length, 0);
|
|
3955
|
-
const doneTasksCount = orderedPhases.reduce((total, phase) => total + phase.tasks.filter((task) => task.status === "done").length, 0);
|
|
3956
|
-
const activeTasksCount = orderedPhases.reduce((total, phase) => total + phase.tasks.filter((task) => task.status === "in-progress").length, 0);
|
|
3957
|
-
const hasActiveWork = activeFeaturesCount > 0 || activePhasesCount > 0 || activeTasksCount > 0;
|
|
4166
|
+
const hasActiveWork = plan.features.features.some((feature) => feature.status === "in-progress")
|
|
4167
|
+
|| plan.phases.some((phase) => phase.status === "in-progress" || phase.status === "discovery")
|
|
4168
|
+
|| orderedPhases.some((phase) => phase.tasks.some((task) => task.status === "in-progress"));
|
|
3958
4169
|
const currentPhase = hasActiveWork
|
|
3959
4170
|
? orderedPhases.find((phase) => phase.id === resume.currentPhaseId && (phase.status === "in-progress" || phase.tasks.some((task) => resume.inProgressTaskIds.includes(task.id))))
|
|
3960
4171
|
?? orderedPhases.find((phase) => phase.tasks.some((task) => resume.inProgressTaskIds.includes(task.id)))
|
|
@@ -3971,16 +4182,51 @@ export default function planPiExtension(pi) {
|
|
|
3971
4182
|
?? null)
|
|
3972
4183
|
: null;
|
|
3973
4184
|
const currentFeature = hasActiveWork && currentPhase?.featureId
|
|
3974
|
-
?
|
|
4185
|
+
? featureById.get(currentPhase.featureId) ?? null
|
|
3975
4186
|
: hasActiveWork
|
|
3976
4187
|
? plan.features.features.find((feature) => feature.status === "in-progress") ?? null
|
|
3977
4188
|
: null;
|
|
3978
|
-
const
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
4189
|
+
const currentFeatureRef = currentFeature ? formatFeatureRef(currentFeature.number) : "";
|
|
4190
|
+
const currentPhaseRef = currentPhase ? formatPhaseRef(currentPhase.number, currentFeature?.number) : "";
|
|
4191
|
+
const currentTaskRef = currentTask && currentPhase
|
|
4192
|
+
? `${formatPhaseRef(currentPhase.number, currentFeature?.number)}/T${pad(currentTask.number)}`
|
|
4193
|
+
: "";
|
|
4194
|
+
const phaseStatusOrder = ["draft", "discovery", "planned", "in-progress", "blocked", "done", "canceled"];
|
|
4195
|
+
const phaseStatusCounts = new Map(phaseStatusOrder.map((status) => [status, 0]));
|
|
4196
|
+
for (const phase of plan.phases) {
|
|
4197
|
+
phaseStatusCounts.set(phase.status, (phaseStatusCounts.get(phase.status) ?? 0) + 1);
|
|
4198
|
+
}
|
|
4199
|
+
const phaseCountsSummary = phaseStatusOrder
|
|
4200
|
+
.map((status) => `${status}=${phaseStatusCounts.get(status) ?? 0}`)
|
|
4201
|
+
.join(" ");
|
|
4202
|
+
const activeTaskMap = new Map();
|
|
4203
|
+
for (const phase of orderedPhases) {
|
|
4204
|
+
const featureNumber = phase.featureId ? featureById.get(phase.featureId)?.number : undefined;
|
|
4205
|
+
for (const task of phase.tasks) {
|
|
4206
|
+
if (!resume.inProgressTaskIds.includes(task.id) && task.status !== "in-progress")
|
|
4207
|
+
continue;
|
|
4208
|
+
activeTaskMap.set(task.id, { phase, task, featureNumber });
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
const inProgressTaskLines = [...activeTaskMap.values()]
|
|
4212
|
+
.sort((left, right) => left.task.number - right.task.number || left.task.createdAt.localeCompare(right.task.createdAt))
|
|
4213
|
+
.map(({ phase, task, featureNumber }) => `${formatPhaseRef(phase.number, featureNumber)}/T${pad(task.number)} — ${task.title}`);
|
|
4214
|
+
const otherActiveOrBlockedPhases = orderedPhases
|
|
4215
|
+
.filter((phase) => phase.id !== currentPhase?.id && (phase.status === "in-progress" || phase.status === "discovery" || phase.status === "blocked"))
|
|
4216
|
+
.slice(0, 6)
|
|
4217
|
+
.map((phase) => {
|
|
4218
|
+
const featureNumber = phase.featureId ? featureById.get(phase.featureId)?.number : undefined;
|
|
4219
|
+
return `${statusIcon(phase.status)} ${formatPhaseRef(phase.number, featureNumber)} — ${phase.title} (${phase.status})`;
|
|
4220
|
+
});
|
|
4221
|
+
const openQuestions = plan.phases
|
|
4222
|
+
.flatMap((phase) => phase.openQuestions.map((question) => `[${formatPhaseRef(phase.number, featureNumberOfPhase(phase, plan.features.features))}] ${question}`))
|
|
4223
|
+
.slice(0, 8);
|
|
4224
|
+
const acceptedDecisions = [
|
|
4225
|
+
...project.acceptedDecisions.map((decision) => `[project] ${decision.title}`),
|
|
4226
|
+
...plan.features.features.flatMap((feature) => feature.acceptedDecisions.map((decision) => `[feature ${formatFeatureRef(feature.number)}] ${decision.title}`)),
|
|
4227
|
+
...plan.phases.flatMap((phase) => phase.acceptedDecisions.map((decision) => `[phase ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, plan.features.features))}] ${decision.title}`)),
|
|
4228
|
+
...plan.phases.flatMap((phase) => phase.tasks.flatMap((task) => task.acceptedDecisions.map((decision) => `[task ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, plan.features.features))}/T${pad(task.number)}] ${decision.title}`))),
|
|
4229
|
+
].slice(0, 20);
|
|
3984
4230
|
const webServer = server;
|
|
3985
4231
|
const webLocalUrl = webServer?.localUrl ?? webServer?.url ?? "";
|
|
3986
4232
|
const webLanUrl = webServer?.lanUrl ?? "";
|
|
@@ -3993,15 +4239,6 @@ export default function planPiExtension(pi) {
|
|
|
3993
4239
|
catch { }
|
|
3994
4240
|
const webUrl = webLocalUrl;
|
|
3995
4241
|
const webUrlFull = webLanUrl ? `${webLocalUrl} (LAN: ${webLanUrl})` : webLocalUrl;
|
|
3996
|
-
const openQuestions = plan.phases
|
|
3997
|
-
.flatMap((p) => p.openQuestions.map((q) => `[${p.id}] ${q}`))
|
|
3998
|
-
.slice(0, 12);
|
|
3999
|
-
const acceptedDecisions = [
|
|
4000
|
-
...project.acceptedDecisions.map((d) => `[project] ${d.title}`),
|
|
4001
|
-
...plan.features.features.flatMap((f) => f.acceptedDecisions.map((d) => `[feature ${f.id}] ${d.title}`)),
|
|
4002
|
-
...plan.phases.flatMap((p) => p.acceptedDecisions.map((d) => `[phase ${p.id}] ${d.title}`)),
|
|
4003
|
-
...plan.phases.flatMap((p) => p.tasks.flatMap((t) => t.acceptedDecisions.map((d) => `[task ${t.id}] ${d.title}`))),
|
|
4004
|
-
].slice(0, 20);
|
|
4005
4242
|
const pkgSummary = profile?.packageJson
|
|
4006
4243
|
? `name=${profile.packageJson.name ?? "?"} pm=${profile.packageJson.packageManager ?? "npm"} scripts=${Object.keys(profile.packageJson.scripts).join(",") || "none"} deps=${Object.keys(profile.packageJson.dependencies).length} devDeps=${Object.keys(profile.packageJson.devDependencies).length}`
|
|
4007
4244
|
: "(no package.json)";
|
|
@@ -4023,96 +4260,60 @@ export default function planPiExtension(pi) {
|
|
|
4023
4260
|
// Build/refresh the context block (slow path: cache miss).
|
|
4024
4261
|
const contextBlock = [
|
|
4025
4262
|
`[Plan Context — ${project.name}]`,
|
|
4026
|
-
// ── Web UI status (prominent — agent must surface URL in its summary) ──
|
|
4027
4263
|
webUrl ? `🌐 WEB UI RUNNING: ${webUrlFull}${webPort ? ` (port ${webPort})` : ""}. Include this address+port at the end of your resume summary.` : `Web UI: not running. Start it with '/planner web start'.`,
|
|
4028
4264
|
"",
|
|
4029
|
-
|
|
4030
|
-
"",
|
|
4031
|
-
"
|
|
4032
|
-
"
|
|
4033
|
-
"
|
|
4034
|
-
"
|
|
4035
|
-
"
|
|
4036
|
-
"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.",
|
|
4037
|
-
"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.",
|
|
4038
|
-
"5. NO SHORTCUTS: If a tool blocks you, follow the protocol. Bypasses are for emergencies only, not for convenience.",
|
|
4039
|
-
"6. ENTITY REFERENCES & FAST LOOKUP: Feature/Phase/Task numbers are GLOBALLY unique (one P003, one T007 in the whole plan) — bare P00x/T00x is a valid unambiguous ref. PREFER the 5-char shortId (e.g. UUXD1) or composite F00x/P00x/T00x in chat; NEVER a raw UUID. To FIND an entity: call feature_list / phase_list / task_list (compact, ~2K tokens for the whole plan) — do NOT read .planner/*.json files or plan_get full=true (~32K tokens) to locate an entity. To ACT: pass the ref directly to the action tool (task_start/complete/update/phase_update accept F00x/P00x/T00x, bare P00x/T00x, shortId, UUID, title). To read a description: call task_get / phase_get / feature_get with full=true for that ONE entity. Tool responses already show shortId + compositeRef; reuse them.",
|
|
4040
|
-
"═══════════════════════════════════════════════════════════════",
|
|
4265
|
+
"Operational rules:",
|
|
4266
|
+
"- Handoffs are context, not locks. Read the relevant handoff with handoff show <ref> before resuming.",
|
|
4267
|
+
"- BEFORE any code edit/write: task_start. AFTER finishing a task: task_complete.",
|
|
4268
|
+
"- Use task_update with motivation for blocked/canceled/rejected/deferred/waiting/planned(from non-planned).",
|
|
4269
|
+
"- Planner ops (status/handoff/planner metadata) are NOT code edits; they are always allowed.",
|
|
4270
|
+
"- Prefer shortId or F00x/P00x/T00x refs. Find via feature_list / phase_list / task_list; read one entity via *_get(full=true).",
|
|
4271
|
+
"- If edit/write guard blocks you, start the right task or use an explicit bypass.",
|
|
4041
4272
|
"",
|
|
4042
|
-
// ── Project details ──
|
|
4043
4273
|
`Goal: ${project.goal || "(not set)"}`,
|
|
4044
4274
|
project.description ? `Description: ${project.description}` : "",
|
|
4045
4275
|
`Stack: ${[...project.technologies, ...project.tools].join(", ") || "(not set)"}`,
|
|
4046
4276
|
`Codebase: ${pkgSummary} | dirs=${profile?.directories.join(",") || "none"} | scanned=${profile?.scannedAt ?? "never"}`,
|
|
4047
4277
|
`Ambient: ${ambientSummary}`,
|
|
4048
4278
|
"",
|
|
4049
|
-
"Global rules
|
|
4050
|
-
project.globalRules.length ? project.globalRules.map((
|
|
4051
|
-
"",
|
|
4052
|
-
"Workflow rules:",
|
|
4053
|
-
` before phase start: ${project.workflowRules.beforePhaseStart.join(" | ") || "(none)"}`,
|
|
4054
|
-
` before task start: ${project.workflowRules.beforeTaskStart.join(" | ") || "(none)"}`,
|
|
4055
|
-
` after phase complete: ${project.workflowRules.afterPhaseComplete.join(" | ") || "(none)"}`,
|
|
4279
|
+
"Global rules:",
|
|
4280
|
+
project.globalRules.length ? project.globalRules.map((rule) => `- ${rule}`).join("\n") : "- (none)",
|
|
4056
4281
|
"",
|
|
4282
|
+
`Workflow: before phase=${project.workflowRules.beforePhaseStart.join(" | ") || "(none)"}; before task=${project.workflowRules.beforeTaskStart.join(" | ") || "(none)"}; after phase=${project.workflowRules.afterPhaseComplete.join(" | ") || "(none)"}`,
|
|
4057
4283
|
`Scope: ${project.scope.join(", ") || "(none)"}`,
|
|
4058
4284
|
`Out of scope: ${project.outOfScope.join(", ") || "(none)"}`,
|
|
4059
4285
|
"",
|
|
4060
4286
|
acceptedDecisions.length ? "Accepted decisions (do not re-litigate):" : "",
|
|
4061
|
-
...acceptedDecisions.map((
|
|
4287
|
+
...acceptedDecisions.map((decision) => `- ${decision}`),
|
|
4062
4288
|
openQuestions.length ? "" : "",
|
|
4063
4289
|
openQuestions.length ? "Open questions:" : "",
|
|
4064
|
-
...openQuestions.map((
|
|
4290
|
+
...openQuestions.map((question) => `- ${question}`),
|
|
4065
4291
|
"",
|
|
4066
|
-
"Resume focus
|
|
4067
|
-
|
|
4068
|
-
|
|
4292
|
+
"Resume focus:",
|
|
4293
|
+
currentFeatureRef ? ` current feature: ${currentFeatureRef} — ${currentFeature?.name}` : " current feature: (none)",
|
|
4294
|
+
currentPhaseRef ? ` current phase: ${currentPhaseRef} — ${currentPhase?.title}` : " current phase: (none)",
|
|
4295
|
+
currentTaskRef ? ` current task: ${currentTaskRef} — ${currentTask?.title}` : " current task: (none)",
|
|
4296
|
+
inProgressTaskLines.length ? ` in-progress tasks: ${inProgressTaskLines.join("; ")}` : " in-progress tasks: (none)",
|
|
4069
4297
|
resume.blockers.length ? ` blockers: ${resume.blockers.join("; ")}` : " blockers: (none)",
|
|
4070
|
-
|
|
4071
|
-
|
|
4298
|
+
handoffs.length === 0 && resume.nextSteps.length
|
|
4299
|
+
? ` next step: ${resume.nextSteps[0]}${resume.nextStepsUpdatedAt ? ` (updated ${resume.nextStepsUpdatedAt})` : ""}`
|
|
4300
|
+
: "",
|
|
4072
4301
|
"",
|
|
4073
|
-
|
|
4074
|
-
|
|
4302
|
+
"Phase overview:",
|
|
4303
|
+
` counts: ${phaseCountsSummary}`,
|
|
4304
|
+
currentPhaseRef ? ` current: ${currentPhaseRef} — ${currentPhase?.title} (${currentPhase?.status})` : " current: (none)",
|
|
4305
|
+
otherActiveOrBlockedPhases.length ? " other active/blocked:" : " other active/blocked: (none)",
|
|
4306
|
+
...otherActiveOrBlockedPhases.map((line) => ` - ${line}`),
|
|
4075
4307
|
"",
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
phaseLines || " _none_",
|
|
4079
|
-
active ? `Active: ${active}` : "",
|
|
4308
|
+
recentActivity.length ? "Recent activity:" : "",
|
|
4309
|
+
...recentActivity.map((entry) => `- ${entry.at} [${entry.type}] ${entry.ref}: ${entry.summary}`),
|
|
4080
4310
|
"",
|
|
4081
|
-
"Primary command UX:",
|
|
4082
|
-
"- Use `/planner <TAB>` for grouped subcommand suggestions.",
|
|
4083
|
-
"- Use `/planner` with no args to open the navigable planner menu.",
|
|
4084
|
-
"- Feature commands: planner feature list|add|show|update|delete.",
|
|
4085
|
-
"- Phase commands: planner phase add|show|discuss|update|delete.",
|
|
4086
|
-
"- Task commands: planner task add|show|discuss|update|delete|start|complete.",
|
|
4087
|
-
"- Handoff commands: planner handoff prepare|show|write|clear.",
|
|
4088
|
-
"- Web/session commands: planner web start|stop|status, planner export|export-full, planner load, planner disable, planner repair.",
|
|
4089
|
-
"Legacy flat aliases may also exist for backward compatibility, but prefer grouped `/planner ...` forms.",
|
|
4090
|
-
"Planner discuss mode is Agent Plan only: ignore GSD workflows/skills/commands unless the user explicitly asks for GSD.",
|
|
4091
|
-
`Language preferences: plan content=${project.contentLanguage || "(not set)"}; chat=${project.chatLanguage || "(not set)"}.`,
|
|
4092
|
-
"If these language preferences are already set, follow them automatically and do NOT ask again in later sessions.",
|
|
4093
|
-
"Ask for language only if BOTH preferences are unset.",
|
|
4094
|
-
"If one preference is set and the other is unset, infer the missing one from the saved value and persist it immediately with project_set_language_preferences without asking the user again.",
|
|
4095
|
-
"Persist accepted decisions into project data, not only chat. Use acceptedDecisions on project/feature/phase/task when the user accepts a decision or solution.",
|
|
4096
|
-
"If you are creating a planner from an existing plan or document, do NOT leave project title, project description, project goal, or top-level requirements empty when the source material provides them.",
|
|
4097
|
-
"When deriving a project title from an existing document, prefer a concise human project title; if the candidate title is long or compound, ask the user to confirm before using it.",
|
|
4098
|
-
"Before starting any phase/task, emit a recap of current state and ask the user if requirements/dependencies changed before planning tasks.",
|
|
4099
|
-
"",
|
|
4100
|
-
"TASK STATUS RULES (strict — do not skip):",
|
|
4101
|
-
"- BEFORE you start implementation work on ANY task, you MUST call task_start to set it to in-progress. This sets startedAt.",
|
|
4102
|
-
"- AFTER you finish implementation work on ANY task, you MUST call task_complete to set it to done. This sets completedAt.",
|
|
4103
|
-
"- Phase and feature statuses are DERIVED from task statuses. They are not the primary source of truth.",
|
|
4104
|
-
"- Therefore, if not strictly necessary, do NOT change phase.status or feature.status directly. Move the task statuses instead and let rollup derive the parent statuses.",
|
|
4105
|
-
"- The extension blocks edit/write when no task is in-progress (bash stays free so git pull/build/test work). If you hit that block, either call task_start, OR ask the user to authorize a one-time bypass (they can run /planner bypass, or you can call plan_authorize_bypass) and then retry.",
|
|
4106
|
-
"- If you forget these task status calls, the derived rollup breaks: phase/feature can stay stale, the dashboard shows wrong active work, and resume focus is wrong.",
|
|
4107
|
-
"- Before your final answer after implementation work, either call task_complete if the task is finished, or explicitly tell the user the task remains in-progress.",
|
|
4108
|
-
"- Use task_complete with force=true only if you have a good reason to skip the checklist check.",
|
|
4109
|
-
"- To reopen a completed task, use task_start (recommended) or task_update.",
|
|
4110
4311
|
handoffs.length > 0 ? `Pending phase handoffs (${handoffs.length}):` : "",
|
|
4111
|
-
...handoffs.map((
|
|
4112
|
-
handoffs.length > 0 ? "
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
"
|
|
4312
|
+
...handoffs.map((handoff, index) => ` [${index + 1}] ${handoff.compositeRef} — ${handoff.updatedAt} — "${handoff.firstLine}"`),
|
|
4313
|
+
handoffs.length > 0 ? "Use handoff show <ref>; if no task is active, treat the handoff target as a candidate to validate, not the current focus." : "",
|
|
4314
|
+
"",
|
|
4315
|
+
`Language: content=${project.contentLanguage || "(not set)"}; chat=${project.chatLanguage || "(not set)"}`,
|
|
4316
|
+
"Planner discuss mode is Agent Plan only: ignore GSD workflows unless the user explicitly asks for GSD.",
|
|
4116
4317
|
].filter(Boolean).join("\n");
|
|
4117
4318
|
contextBlockCache = contextBlock;
|
|
4118
4319
|
contextBlockDirty = false;
|