@agent-plan/pi-adapter 0.2.19-next.1 → 0.2.19-next.11
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 +198 -126
- package/package.json +3 -3
- package/web-ui-dist/assets/index-CCi6Ok6y.css +1 -0
- package/web-ui-dist/assets/index-Cqm9jOC0.js +216 -0
- package/web-ui-dist/index.html +2 -2
- package/web-ui-dist/assets/index-CZ27vQH6.js +0 -211
- package/web-ui-dist/assets/index-Jy8Yy6I4.css +0 -1
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - inject context into prompts
|
|
10
10
|
*/
|
|
11
11
|
import { Type } from "typebox";
|
|
12
|
-
import { ExportService, PlanStore, setWriteBusyHook, setWriteNotifyHook, migrateToUuids, withFeatureLock, needsMotivation, findPhaseByRef, buildRecap } from "@agent-plan/core";
|
|
12
|
+
import { ExportService, PlanStore, setWriteBusyHook, setWriteNotifyHook, migrateToUuids, migrateToGlobalSequence, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, buildRecap } from "@agent-plan/core";
|
|
13
13
|
import { createChecklistItemId, createFeatureId, createPhaseId, createShortId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef, featureNumberOfPhase } from "@agent-plan/core/naming";
|
|
14
14
|
import { join, dirname } from "node:path";
|
|
15
15
|
import { homedir } from "node:os";
|
|
@@ -68,6 +68,29 @@ setWriteNotifyHook(() => {
|
|
|
68
68
|
let previousContextPercent = 0;
|
|
69
69
|
let autoHandoffTriggered = false;
|
|
70
70
|
let plannerSessionEnabled = false;
|
|
71
|
+
// In-process guard bypass (per session, NOT persisted/shared). The edit/write
|
|
72
|
+
// guard is advisory in Pi (warns "NO ACTIVE TASK" but does not block). When a
|
|
73
|
+
// bypass is active the warning is silenced for the window. Lives in memory only:
|
|
74
|
+
// not on disk, not in git, not shared across agents/sessions on the same folder.
|
|
75
|
+
let guardBypassUntil = "";
|
|
76
|
+
function authorizeGuardBypass(durationMinutes = 15) {
|
|
77
|
+
const until = new Date(Date.now() + durationMinutes * 60_000).toISOString();
|
|
78
|
+
guardBypassUntil = until;
|
|
79
|
+
return until;
|
|
80
|
+
}
|
|
81
|
+
function clearGuardBypass() { guardBypassUntil = ""; }
|
|
82
|
+
function isGuardBypassed() {
|
|
83
|
+
if (!guardBypassUntil)
|
|
84
|
+
return false;
|
|
85
|
+
const until = Date.parse(guardBypassUntil);
|
|
86
|
+
if (!Number.isFinite(until))
|
|
87
|
+
return false;
|
|
88
|
+
if (until <= Date.now()) {
|
|
89
|
+
guardBypassUntil = "";
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
71
94
|
let startupResumePromptPending = false;
|
|
72
95
|
let startupResumeSummaryPending = false;
|
|
73
96
|
let plannerHeavyInitDone = false; // runs migrate/heal/refreshResume once per session, not every turn
|
|
@@ -393,7 +416,7 @@ async function buildHandoffMarkdown(st, reason, overrides) {
|
|
|
393
416
|
...recentActivityLines,
|
|
394
417
|
"",
|
|
395
418
|
"## Reminder",
|
|
396
|
-
"-
|
|
419
|
+
"- This handoff is KEPT until a task in this phase starts (then auto-archived to .planner/handoff-archive/) or the phase completes. No need to clear it manually.",
|
|
397
420
|
].join("\n");
|
|
398
421
|
}
|
|
399
422
|
async function writeProjectHandoff(st, reason, overrides) {
|
|
@@ -489,6 +512,9 @@ function statusIcon(status) {
|
|
|
489
512
|
};
|
|
490
513
|
return icons[status] ?? "❓";
|
|
491
514
|
}
|
|
515
|
+
function pad(n) {
|
|
516
|
+
return String(n).padStart(3, "0");
|
|
517
|
+
}
|
|
492
518
|
function applyTaskLifecycleDates(task, nextStatus, now) {
|
|
493
519
|
const previousStatus = task.status;
|
|
494
520
|
if (nextStatus === "in-progress" && !task.startedAt) {
|
|
@@ -734,6 +760,10 @@ export default function planPiExtension(pi) {
|
|
|
734
760
|
const handoffs = await st.listHandoffs().catch(() => []);
|
|
735
761
|
if (handoffs.length > 0) {
|
|
736
762
|
ctx.ui.notify(`ℹ️ ${handoffs.length} phase handoff(s) pending — review with /planner handoff list.`, "info");
|
|
763
|
+
// Mark each handoff as read (recap presented). Do NOT clear — content is
|
|
764
|
+
// kept until a task in the phase starts (auto-archived) or phase completes.
|
|
765
|
+
for (const h of handoffs)
|
|
766
|
+
await st.markHandoffRead(h.phaseId).catch(() => { });
|
|
737
767
|
}
|
|
738
768
|
}
|
|
739
769
|
// If the user declined enablement, skip EVERYTHING (no web prompt, no migration, no summary).
|
|
@@ -862,6 +892,8 @@ export default function planPiExtension(pi) {
|
|
|
862
892
|
return; // nothing to enforce yet
|
|
863
893
|
if (guard.inProgressTaskIds.length > 0)
|
|
864
894
|
return; // a task is open → we're good
|
|
895
|
+
if (isGuardBypassed())
|
|
896
|
+
return; // user authorized proceeding without a task
|
|
865
897
|
const focusHint = guard.focusTaskId
|
|
866
898
|
? `Il task più probabile è ${guard.focusTaskId} — ${guard.focusTaskTitle}. Avvialo con: \`/planner task start ${guard.focusTaskId}\``
|
|
867
899
|
: `Scegli un task dal piano e avvialo con \`/planner task start <taskId>\`.`;
|
|
@@ -1117,19 +1149,6 @@ export default function planPiExtension(pi) {
|
|
|
1117
1149
|
?? phases.find((phase) => phase.title.toLowerCase().includes(normalized))
|
|
1118
1150
|
?? null;
|
|
1119
1151
|
}
|
|
1120
|
-
function findTaskByRef(phases, ref) {
|
|
1121
|
-
const normalized = ref.trim().toLowerCase();
|
|
1122
|
-
if (!normalized)
|
|
1123
|
-
return null;
|
|
1124
|
-
for (const phase of phases) {
|
|
1125
|
-
const task = phase.tasks.find((entryTask) => entryTask.id.toLowerCase() === normalized
|
|
1126
|
-
|| entryTask.title.toLowerCase() === normalized
|
|
1127
|
-
|| entryTask.title.toLowerCase().includes(normalized));
|
|
1128
|
-
if (task)
|
|
1129
|
-
return { phase, task };
|
|
1130
|
-
}
|
|
1131
|
-
return null;
|
|
1132
|
-
}
|
|
1133
1152
|
function profile_packageManager(pkg, lockfile) {
|
|
1134
1153
|
if (pkg?.packageManager)
|
|
1135
1154
|
return pkg.packageManager;
|
|
@@ -1334,7 +1353,7 @@ export default function planPiExtension(pi) {
|
|
|
1334
1353
|
return;
|
|
1335
1354
|
}
|
|
1336
1355
|
const now = nowISO();
|
|
1337
|
-
const featureNumber =
|
|
1356
|
+
const featureNumber = await st.allocFeatureNumber();
|
|
1338
1357
|
const shortId = createShortId(await st.assignedShortIds());
|
|
1339
1358
|
const priority = await st.nextPriority("feature");
|
|
1340
1359
|
const feature = {
|
|
@@ -1363,7 +1382,7 @@ export default function planPiExtension(pi) {
|
|
|
1363
1382
|
return doc;
|
|
1364
1383
|
});
|
|
1365
1384
|
await st.writeGenerated();
|
|
1366
|
-
ctx.ui.notify(`Feature created: ${feature.
|
|
1385
|
+
ctx.ui.notify(`Feature created: ${formatFeatureRef(feature.number)}${feature.shortId ? ` +MI+ ${feature.shortId}` : ""} +EM+ ${feature.name}`, "info");
|
|
1367
1386
|
return;
|
|
1368
1387
|
}
|
|
1369
1388
|
if (b === "show") {
|
|
@@ -1444,7 +1463,7 @@ export default function planPiExtension(pi) {
|
|
|
1444
1463
|
});
|
|
1445
1464
|
feature = updatedDoc.features.find((entry) => entry.id === featureId) ?? feature;
|
|
1446
1465
|
await st.writeGenerated();
|
|
1447
|
-
ctx.ui.notify(`Feature updated: ${feature.
|
|
1466
|
+
ctx.ui.notify(`Feature updated: ${formatFeatureRef(feature.number)}${feature.shortId ? ` +MI+ ${feature.shortId}` : ""} +EM+ ${feature.name} (${feature.status})`, "info");
|
|
1448
1467
|
return;
|
|
1449
1468
|
}
|
|
1450
1469
|
if (b === "delete") {
|
|
@@ -1521,7 +1540,7 @@ export default function planPiExtension(pi) {
|
|
|
1521
1540
|
await withFeatureLock(feature.id, async () => {
|
|
1522
1541
|
const phases = await st.loadAllPhases();
|
|
1523
1542
|
const featurePhases = phases.filter((p) => p.featureId === feature.id);
|
|
1524
|
-
const number =
|
|
1543
|
+
const number = await st.allocPhaseNumber();
|
|
1525
1544
|
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1526
1545
|
const id = createPhaseId();
|
|
1527
1546
|
const shortId = createShortId(await st.assignedShortIds());
|
|
@@ -1532,6 +1551,8 @@ export default function planPiExtension(pi) {
|
|
|
1532
1551
|
openQuestions: [], decisions: [], acceptedDecisions: [], completionCriteria: [], taskIds: [], tasks: [],
|
|
1533
1552
|
createdAt: nowISO(), updatedAt: nowISO(),
|
|
1534
1553
|
handoff: "", handoffUpdatedAt: "",
|
|
1554
|
+
handoffReadAt: "",
|
|
1555
|
+
handoffHistory: [],
|
|
1535
1556
|
};
|
|
1536
1557
|
await st.savePhase(phase);
|
|
1537
1558
|
await st.updateFeatures((doc) => {
|
|
@@ -1546,7 +1567,7 @@ export default function planPiExtension(pi) {
|
|
|
1546
1567
|
});
|
|
1547
1568
|
if (!phase)
|
|
1548
1569
|
return;
|
|
1549
|
-
ctx.ui.notify(`Phase created: ${phase.
|
|
1570
|
+
ctx.ui.notify(`Phase created: ${formatPhaseRef(phase.number, feature.number)} — ${title}${phase.shortId ? ` · ${phase.shortId}` : ""} (feature ${feature.name})`, "info");
|
|
1550
1571
|
const startDiscuss = await ctx.ui.input("Start phase discuss now? Type yes to continue");
|
|
1551
1572
|
if (isYes(startDiscuss)) {
|
|
1552
1573
|
await handlePlanner(`phase discuss ${phase.id}`, ctx);
|
|
@@ -1571,7 +1592,7 @@ export default function planPiExtension(pi) {
|
|
|
1571
1592
|
phase.status = "discovery";
|
|
1572
1593
|
phase.updatedAt = nowISO();
|
|
1573
1594
|
await st.savePhase(phase);
|
|
1574
|
-
ctx.ui.notify(`Discovery for ${phase.
|
|
1595
|
+
ctx.ui.notify(`Discovery for ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, (await st.loadFeatures()).features))}…`, "info");
|
|
1575
1596
|
// ── Recap: give full context before planning tasks ──
|
|
1576
1597
|
const recapPlan = await st.loadAll();
|
|
1577
1598
|
const recapProject = recapPlan.project;
|
|
@@ -1636,7 +1657,7 @@ export default function planPiExtension(pi) {
|
|
|
1636
1657
|
phase.updatedAt = nowISO();
|
|
1637
1658
|
await st.savePhase(phase);
|
|
1638
1659
|
await st.writeGenerated();
|
|
1639
|
-
ctx.ui.notify(`Phase ${phase.
|
|
1660
|
+
ctx.ui.notify(`Phase ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, (await st.loadFeatures()).features))} is now **planned**.${phase.shortId ? ` · ${phase.shortId}` : ""}`, "info");
|
|
1640
1661
|
return;
|
|
1641
1662
|
}
|
|
1642
1663
|
if (b === "show") {
|
|
@@ -1684,7 +1705,7 @@ export default function planPiExtension(pi) {
|
|
|
1684
1705
|
}
|
|
1685
1706
|
await st.deletePhase(phase.id);
|
|
1686
1707
|
await st.writeGenerated();
|
|
1687
|
-
ctx.ui.notify(`Phase deleted: ${phase.
|
|
1708
|
+
ctx.ui.notify(`Phase deleted: ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, (await st.loadFeatures()).features))}${phase.shortId ? ` · ${phase.shortId}` : ""}`, "info");
|
|
1688
1709
|
return;
|
|
1689
1710
|
}
|
|
1690
1711
|
if (b === "update") {
|
|
@@ -1736,7 +1757,7 @@ export default function planPiExtension(pi) {
|
|
|
1736
1757
|
phase.updatedAt = nowISO();
|
|
1737
1758
|
await st.savePhase(phase);
|
|
1738
1759
|
await st.writeGenerated();
|
|
1739
|
-
ctx.ui.notify(`Phase updated: ${phase.
|
|
1760
|
+
ctx.ui.notify(`Phase updated: ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, (await st.loadFeatures()).features))} — ${phase.title} (${phase.status})${phase.shortId ? ` · ${phase.shortId}` : ""}`, "info");
|
|
1740
1761
|
return;
|
|
1741
1762
|
}
|
|
1742
1763
|
ctx.ui.notify(`Unknown phase action "${b}". Try: add, show, discuss, delete, update`, "warning");
|
|
@@ -1762,7 +1783,7 @@ export default function planPiExtension(pi) {
|
|
|
1762
1783
|
return;
|
|
1763
1784
|
}
|
|
1764
1785
|
const shortName = clampSlug(title, 30, `task-${Date.now().toString(36)}`);
|
|
1765
|
-
const taskNum =
|
|
1786
|
+
const taskNum = await st.allocTaskNumber();
|
|
1766
1787
|
const taskId = createTaskId();
|
|
1767
1788
|
const shortId = createShortId(await st.assignedShortIds());
|
|
1768
1789
|
const priority = await st.nextPriority("task", phase.id);
|
|
@@ -1794,7 +1815,7 @@ export default function planPiExtension(pi) {
|
|
|
1794
1815
|
}
|
|
1795
1816
|
if (b === "discuss") {
|
|
1796
1817
|
const phases = await st.loadAllPhases();
|
|
1797
|
-
let resolved = subArgs.trim() ? findTaskByRef(phases, subArgs.trim()) : null;
|
|
1818
|
+
let resolved = subArgs.trim() ? findTaskByRef(phases, (await st.loadFeatures()).features, subArgs.trim()) : null;
|
|
1798
1819
|
if (!resolved) {
|
|
1799
1820
|
const phase = await pickPhase();
|
|
1800
1821
|
if (!phase) {
|
|
@@ -1828,14 +1849,14 @@ export default function planPiExtension(pi) {
|
|
|
1828
1849
|
phase.updatedAt = nowISO();
|
|
1829
1850
|
await st.savePhase(phase);
|
|
1830
1851
|
await st.writeGenerated();
|
|
1831
|
-
ctx.ui.notify(`Task ${task.
|
|
1852
|
+
ctx.ui.notify(`Task ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, (await st.loadFeatures()).features))}/T${String(task.number).padStart(3, "0")} discussed and updated.`, "info");
|
|
1832
1853
|
return;
|
|
1833
1854
|
}
|
|
1834
1855
|
if (b === "show") {
|
|
1835
1856
|
let phase = null;
|
|
1836
1857
|
let task = null;
|
|
1837
1858
|
if (subArgs.trim()) {
|
|
1838
|
-
const resolved = findTaskByRef(await st.loadAllPhases(), subArgs.trim());
|
|
1859
|
+
const resolved = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, subArgs.trim());
|
|
1839
1860
|
phase = resolved?.phase ?? null;
|
|
1840
1861
|
task = resolved?.task ?? null;
|
|
1841
1862
|
}
|
|
@@ -1897,7 +1918,7 @@ export default function planPiExtension(pi) {
|
|
|
1897
1918
|
phase.updatedAt = nowISO();
|
|
1898
1919
|
await st.savePhase(phase);
|
|
1899
1920
|
await st.writeGenerated();
|
|
1900
|
-
ctx.ui.notify(`Task deleted: ${task.
|
|
1921
|
+
ctx.ui.notify(`Task deleted: ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, (await st.loadFeatures()).features))}/T${String(task.number).padStart(3, "0")}${task.shortId ? ` · ${task.shortId}` : ""}`, "info");
|
|
1901
1922
|
return;
|
|
1902
1923
|
}
|
|
1903
1924
|
if (b === "update") {
|
|
@@ -1946,14 +1967,14 @@ export default function planPiExtension(pi) {
|
|
|
1946
1967
|
phase.updatedAt = now;
|
|
1947
1968
|
await st.savePhase(phase);
|
|
1948
1969
|
await st.writeGenerated();
|
|
1949
|
-
ctx.ui.notify(`Task updated: ${task.
|
|
1970
|
+
ctx.ui.notify(`Task updated: ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, (await st.loadFeatures()).features))}/T${String(task.number).padStart(3, "0")} — ${task.title} (${task.status})${task.shortId ? ` · ${task.shortId}` : ""}`, "info");
|
|
1950
1971
|
return;
|
|
1951
1972
|
}
|
|
1952
1973
|
if (b === "start") {
|
|
1953
1974
|
let phase = null;
|
|
1954
1975
|
let task = null;
|
|
1955
1976
|
if (subArgs.trim()) {
|
|
1956
|
-
const resolved = findTaskByRef(await st.loadAllPhases(), subArgs.trim());
|
|
1977
|
+
const resolved = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, subArgs.trim());
|
|
1957
1978
|
phase = resolved?.phase ?? null;
|
|
1958
1979
|
task = resolved?.task ?? null;
|
|
1959
1980
|
}
|
|
@@ -1977,12 +1998,16 @@ export default function planPiExtension(pi) {
|
|
|
1977
1998
|
ctx.ui.notify(`Task "${task.title}" is already in-progress.`, "info");
|
|
1978
1999
|
return;
|
|
1979
2000
|
}
|
|
1980
|
-
//
|
|
1981
|
-
//
|
|
1982
|
-
//
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
ctx.ui.notify("
|
|
2001
|
+
// Auto-archive the handoff on THIS task's phase (reason: task-started) —
|
|
2002
|
+
// the agent is now actively working, so the captured context is consumed.
|
|
2003
|
+
// Other phases' handoffs are left untouched (informational, non-blocking).
|
|
2004
|
+
if ((phase.handoff ?? "") !== "") {
|
|
2005
|
+
await st.clearPhaseHandoff(phase.id, "task-started").catch(() => { });
|
|
2006
|
+
ctx.ui.notify("📦 Archived handoff for this phase (task started) — recover via /planner handoff list + .planner/handoff-archive/.", "info");
|
|
2007
|
+
}
|
|
2008
|
+
const _otherHandoffs = (await st.listHandoffs()).filter((h) => h.phaseId !== phase.id);
|
|
2009
|
+
if (_otherHandoffs.length > 0) {
|
|
2010
|
+
ctx.ui.notify(`ℹ️ ${_otherHandoffs.length} other phase handoff(s) pending — review with /planner handoff list if relevant.`, "info");
|
|
1986
2011
|
}
|
|
1987
2012
|
const now = nowISO();
|
|
1988
2013
|
// Record status change in the incremental statusLog.
|
|
@@ -2001,14 +2026,14 @@ export default function planPiExtension(pi) {
|
|
|
2001
2026
|
phase.updatedAt = now;
|
|
2002
2027
|
await st.savePhase(phase);
|
|
2003
2028
|
await st.writeGenerated();
|
|
2004
|
-
ctx.ui.notify(`✅ Task started: ${task.
|
|
2029
|
+
ctx.ui.notify(`✅ Task started: ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, (await st.loadFeatures()).features))}/T${String(task.number).padStart(3, "0")} — ${task.title} (in-progress)${task.shortId ? ` · ${task.shortId}` : ""}`, "info");
|
|
2005
2030
|
return;
|
|
2006
2031
|
}
|
|
2007
2032
|
if (b === "complete") {
|
|
2008
2033
|
let phase = null;
|
|
2009
2034
|
let task = null;
|
|
2010
2035
|
if (subArgs.trim()) {
|
|
2011
|
-
const resolved = findTaskByRef(await st.loadAllPhases(), subArgs.trim());
|
|
2036
|
+
const resolved = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, subArgs.trim());
|
|
2012
2037
|
phase = resolved?.phase ?? null;
|
|
2013
2038
|
task = resolved?.task ?? null;
|
|
2014
2039
|
}
|
|
@@ -2056,7 +2081,7 @@ export default function planPiExtension(pi) {
|
|
|
2056
2081
|
phase.updatedAt = now;
|
|
2057
2082
|
await st.savePhase(phase);
|
|
2058
2083
|
await st.writeGenerated();
|
|
2059
|
-
ctx.ui.notify(`✅ Task completed: ${task.
|
|
2084
|
+
ctx.ui.notify(`✅ Task completed: ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, (await st.loadFeatures()).features))}/T${String(task.number).padStart(3, "0")} — ${task.title} (done)${task.shortId ? ` · ${task.shortId}` : ""}`, "info");
|
|
2060
2085
|
return;
|
|
2061
2086
|
}
|
|
2062
2087
|
ctx.ui.notify(`Unknown task action "${b}". Try: add, show, discuss, delete, update, start, complete`, "warning");
|
|
@@ -2139,7 +2164,7 @@ export default function planPiExtension(pi) {
|
|
|
2139
2164
|
ctx.ui.notify(r.error, "warning");
|
|
2140
2165
|
return;
|
|
2141
2166
|
}
|
|
2142
|
-
await st.clearPhaseHandoff(r.phase.id);
|
|
2167
|
+
await st.clearPhaseHandoff(r.phase.id, "manual");
|
|
2143
2168
|
ctx.ui.notify(`Cleared handoff on ${r.compositeRef}`, "info");
|
|
2144
2169
|
return;
|
|
2145
2170
|
}
|
|
@@ -2221,12 +2246,12 @@ export default function planPiExtension(pi) {
|
|
|
2221
2246
|
try {
|
|
2222
2247
|
if (a === "bypass") {
|
|
2223
2248
|
const mins = parseInt(subArgs.trim(), 10);
|
|
2224
|
-
const until =
|
|
2225
|
-
ctx.ui.notify(`Guard bypass authorized until ${until}. edit/write
|
|
2249
|
+
const until = authorizeGuardBypass(Number.isFinite(mins) && mins > 0 ? mins : 15);
|
|
2250
|
+
ctx.ui.notify(`Guard bypass authorized until ${until}. edit/write advisory warnings silenced for that window (per-session, not shared).`, "info");
|
|
2226
2251
|
}
|
|
2227
2252
|
else {
|
|
2228
|
-
|
|
2229
|
-
ctx.ui.notify("Guard bypass revoked. edit/write
|
|
2253
|
+
clearGuardBypass();
|
|
2254
|
+
ctx.ui.notify("Guard bypass revoked. edit/write advisory warnings re-enabled.", "info");
|
|
2230
2255
|
}
|
|
2231
2256
|
}
|
|
2232
2257
|
catch (e) {
|
|
@@ -2583,7 +2608,7 @@ export default function planPiExtension(pi) {
|
|
|
2583
2608
|
pi.registerTool({
|
|
2584
2609
|
name: "plan_repair",
|
|
2585
2610
|
label: "Plan Repair",
|
|
2586
|
-
description: "Repair dangling feature→phase references and report plan integrity (duplicate phase ids, dangling phase refs). Safe to run anytime; run if the planner reports ENOENT/phase not found or after manual edits to .planner/.",
|
|
2611
|
+
description: "Repair dangling feature→phase references, rebuild phase containment from each task's own phaseId (heals the migrateToGlobalSequence task-shuffle bug), and report plan integrity (duplicate phase ids, dangling phase refs). Safe to run anytime; run if the planner reports ENOENT/phase not found, tasks appear in the wrong phase, or after manual edits to .planner/.",
|
|
2587
2612
|
parameters: Type.Object({}),
|
|
2588
2613
|
async execute(_id, _params, _signal, _onUpdate, ctx) {
|
|
2589
2614
|
const st = await requirePlan(ctx);
|
|
@@ -2591,9 +2616,11 @@ export default function planPiExtension(pi) {
|
|
|
2591
2616
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
2592
2617
|
const report = await st.repair();
|
|
2593
2618
|
const m = report.migrated;
|
|
2619
|
+
const c = report.containment;
|
|
2594
2620
|
const lines = [
|
|
2595
2621
|
`Repair complete.`,
|
|
2596
2622
|
`Migration: renamed=${m.renamed}, repaired=${m.repaired} refs, inferred=${m.inferred}.`,
|
|
2623
|
+
`Containment: ${c.changed} phase file${c.changed === 1 ? "" : "s"} rewritten (tasks regrouped by their own phaseId), ${c.tasks} tasks scanned, ${c.orphan} orphan.`,
|
|
2597
2624
|
`Integrity: duplicatePhaseIds=${report.integrity.duplicatePhaseIds.length}, danglingPhaseIds=${report.integrity.danglingPhaseIds.length}.`,
|
|
2598
2625
|
];
|
|
2599
2626
|
if (report.integrity.danglingPhaseIds.length)
|
|
@@ -2666,7 +2693,7 @@ export default function planPiExtension(pi) {
|
|
|
2666
2693
|
const r = await resolvePhaseForHandoff(st, undefined);
|
|
2667
2694
|
if (!r.ok)
|
|
2668
2695
|
return { content: [{ type: "text", text: `⚠️ plan_delete_handoff is deprecated — use handoff_clear. ${r.error}` }], details: { deprecated: true, error: r.error } };
|
|
2669
|
-
await st.clearPhaseHandoff(r.phase.id);
|
|
2696
|
+
await st.clearPhaseHandoff(r.phase.id, "manual");
|
|
2670
2697
|
return { content: [{ type: "text", text: `⚠️ plan_delete_handoff is deprecated — cleared handoff on ${r.compositeRef} (phase.handoff). Prefer handoff_clear.` }], details: { deprecated: true, phaseRef: r.compositeRef, phaseId: r.phase.id } };
|
|
2671
2698
|
},
|
|
2672
2699
|
});
|
|
@@ -2764,36 +2791,36 @@ export default function planPiExtension(pi) {
|
|
|
2764
2791
|
const r = await resolvePhaseForHandoff(st, params.phaseRef);
|
|
2765
2792
|
if (!r.ok)
|
|
2766
2793
|
return { content: [{ type: "text", text: `❌ ${r.error}` }], details: { error: r.error } };
|
|
2767
|
-
await st.clearPhaseHandoff(r.phase.id);
|
|
2768
|
-
return { content: [{ type: "text", text: `✅ Cleared handoff on ${r.compositeRef}
|
|
2794
|
+
await st.clearPhaseHandoff(r.phase.id, "manual");
|
|
2795
|
+
return { content: [{ type: "text", text: `✅ Cleared handoff on ${r.compositeRef} (handoffUpdatedAt preserved as audit; archived to .planner/handoff-archive/).` }], details: { phaseRef: r.compositeRef, phaseId: r.phase.id } };
|
|
2769
2796
|
},
|
|
2770
2797
|
});
|
|
2771
2798
|
pi.registerTool({
|
|
2772
2799
|
name: "plan_authorize_bypass",
|
|
2773
2800
|
label: "Plan Authorize Guard Bypass",
|
|
2774
|
-
description: "Authorize a temporary guard bypass (default 15 minutes) so edit/write
|
|
2801
|
+
description: "Authorize a temporary guard bypass (default 15 minutes) so the edit/write advisory guard stops warning when no task is in-progress. Per-session in-memory: not persisted, not shared across agents. Use ONLY after the user explicitly authorizes proceeding without a task.",
|
|
2775
2802
|
parameters: Type.Object({
|
|
2776
2803
|
durationMinutes: Type.Optional(Type.Number({ description: "Bypass window in minutes. Default 15." })),
|
|
2777
2804
|
}),
|
|
2778
2805
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
2779
|
-
const
|
|
2780
|
-
if (!
|
|
2806
|
+
const ok = await requirePlan(ctx);
|
|
2807
|
+
if (!ok)
|
|
2781
2808
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
2782
2809
|
const mins = params.durationMinutes ?? 15;
|
|
2783
|
-
const until =
|
|
2784
|
-
return { content: [{ type: "text", text: `Guard bypass authorized until ${until}. edit/write
|
|
2810
|
+
const until = authorizeGuardBypass(mins);
|
|
2811
|
+
return { content: [{ type: "text", text: `Guard bypass authorized until ${until}. Advisory edit/write warnings silenced for ${mins} minutes (per-session, not shared).` }], details: { until } };
|
|
2785
2812
|
},
|
|
2786
2813
|
});
|
|
2787
2814
|
pi.registerTool({
|
|
2788
2815
|
name: "plan_clear_bypass",
|
|
2789
2816
|
label: "Plan Clear Guard Bypass",
|
|
2790
|
-
description: "Revoke
|
|
2817
|
+
description: "Revoke the per-session guard bypass so the edit/write advisory warnings resume.",
|
|
2791
2818
|
parameters: Type.Object({}),
|
|
2792
2819
|
async execute(_id, _params, _signal, _onUpdate, ctx) {
|
|
2793
|
-
const
|
|
2794
|
-
if (!
|
|
2820
|
+
const ok = await requirePlan(ctx);
|
|
2821
|
+
if (!ok)
|
|
2795
2822
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
2796
|
-
|
|
2823
|
+
clearGuardBypass();
|
|
2797
2824
|
return { content: [{ type: "text", text: "Guard bypass revoked." }], details: { cleared: true } };
|
|
2798
2825
|
},
|
|
2799
2826
|
});
|
|
@@ -2801,38 +2828,44 @@ export default function planPiExtension(pi) {
|
|
|
2801
2828
|
pi.registerTool({
|
|
2802
2829
|
name: "feature_list",
|
|
2803
2830
|
label: "Feature List",
|
|
2804
|
-
description: "List
|
|
2805
|
-
parameters: Type.Object({}),
|
|
2806
|
-
async execute(_id,
|
|
2831
|
+
description: "List features (compact: F00x · shortId — name (status; N phases, M tasks)). Use this to discover refs cheaply — do NOT read .planner/ files or plan_get full=true to find entities.",
|
|
2832
|
+
parameters: Type.Object({ featureRef: Type.Optional(Type.String({ description: "Optional: filter to one feature (F00x/shortId/UUID/name)" })) }),
|
|
2833
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
2807
2834
|
const st = await requirePlan(ctx);
|
|
2808
2835
|
if (!st)
|
|
2809
2836
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
2810
|
-
const
|
|
2837
|
+
const all = (await st.loadFeatures()).features;
|
|
2838
|
+
const ref = params.featureRef?.trim();
|
|
2839
|
+
const matchF = (f) => !ref || formatFeatureRef(f.number) === ref || f.shortId === ref || f.id === ref || f.name.toLowerCase() === ref.toLowerCase();
|
|
2840
|
+
const features = all.filter(matchF);
|
|
2811
2841
|
const phases = await st.loadAllPhases();
|
|
2812
2842
|
const summary = features.map((f) => {
|
|
2813
2843
|
const fp = phases.filter((p) => p.featureId === f.id);
|
|
2814
|
-
return `- ${
|
|
2844
|
+
return `- ${formatFeatureRef(f.number)}${f.shortId ? ` · ${f.shortId}` : ""} — ${f.name} (${f.status}; ${fp.length} phases, ${fp.reduce((t, p) => t + p.tasks.length, 0)} tasks)`;
|
|
2815
2845
|
}).join("\n");
|
|
2816
|
-
return { content: [{ type: "text", text: summary || "No features" }], details: {
|
|
2846
|
+
return { content: [{ type: "text", text: summary || "No features" }], details: {} };
|
|
2817
2847
|
},
|
|
2818
2848
|
});
|
|
2819
2849
|
pi.registerTool({
|
|
2820
2850
|
name: "feature_get",
|
|
2821
2851
|
label: "Feature Get",
|
|
2822
|
-
description: "
|
|
2852
|
+
description: "Show a feature. Compact identity by default (saves tokens); pass full=true to include the description.",
|
|
2823
2853
|
parameters: Type.Object({
|
|
2824
|
-
featureId: Type.String({ description: "Feature
|
|
2854
|
+
featureId: Type.String({ description: "Feature ref: F00x, shortId, UUID, or name" }),
|
|
2855
|
+
full: Type.Optional(Type.Boolean({ description: "If true, include the feature description. Default: compact identity only." })),
|
|
2825
2856
|
}),
|
|
2826
2857
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
2827
2858
|
const st = await requirePlan(ctx);
|
|
2828
2859
|
if (!st)
|
|
2829
2860
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
2830
2861
|
const features = (await st.loadFeatures()).features;
|
|
2831
|
-
const
|
|
2862
|
+
const ref = params.featureId.trim();
|
|
2863
|
+
const feature = features.find((f) => formatFeatureRef(f.number) === ref || f.shortId === ref || f.id === ref || f.name.toLowerCase() === ref.toLowerCase());
|
|
2832
2864
|
if (!feature)
|
|
2833
|
-
return { content: [{ type: "text", text: `Feature not found: ${
|
|
2865
|
+
return { content: [{ type: "text", text: `Feature not found: ${ref}` }], details: {} };
|
|
2834
2866
|
const phases = (await st.loadAllPhases()).filter((p) => p.featureId === feature.id);
|
|
2835
|
-
|
|
2867
|
+
const summary = `${feature.name} — ${formatFeatureRef(feature.number)}${feature.shortId ? ` · ${feature.shortId}` : ""} (${feature.status}; ${phases.length} phases)`;
|
|
2868
|
+
return { content: [{ type: "text", text: params.full ? `${summary}\n\n${feature.description || ""}` : summary }], details: {} };
|
|
2836
2869
|
},
|
|
2837
2870
|
});
|
|
2838
2871
|
pi.registerTool({
|
|
@@ -2855,7 +2888,7 @@ export default function planPiExtension(pi) {
|
|
|
2855
2888
|
const priority = await st.nextPriority("feature");
|
|
2856
2889
|
const feature = {
|
|
2857
2890
|
id: createFeatureId(),
|
|
2858
|
-
number:
|
|
2891
|
+
number: await st.allocFeatureNumber(),
|
|
2859
2892
|
shortId,
|
|
2860
2893
|
priority,
|
|
2861
2894
|
name: params.name,
|
|
@@ -2881,7 +2914,7 @@ export default function planPiExtension(pi) {
|
|
|
2881
2914
|
return doc;
|
|
2882
2915
|
});
|
|
2883
2916
|
await st.writeGenerated();
|
|
2884
|
-
return { content: [{ type: "text", text:
|
|
2917
|
+
return { content: [{ type: "text", text: `✅ Feature created: ${formatFeatureRef(feature.number)} — ${feature.name}${feature.shortId ? ` · ${feature.shortId}` : ""}` }], details: feature };
|
|
2885
2918
|
},
|
|
2886
2919
|
});
|
|
2887
2920
|
pi.registerTool({
|
|
@@ -2889,7 +2922,7 @@ export default function planPiExtension(pi) {
|
|
|
2889
2922
|
label: "Feature Update",
|
|
2890
2923
|
description: "Update one or more fields of a feature. Only provided fields are changed. IMPORTANT: feature status is derived from child phases/tasks, so do not update feature.status directly unless truly necessary.",
|
|
2891
2924
|
parameters: Type.Object({
|
|
2892
|
-
featureId: Type.String({ description: "Feature
|
|
2925
|
+
featureId: Type.String({ description: "Feature ref: F00x, shortId, UUID, or name" }),
|
|
2893
2926
|
name: Type.Optional(Type.String({ description: "New name" })),
|
|
2894
2927
|
description: Type.Optional(Type.String({ description: "New description" })),
|
|
2895
2928
|
status: Type.Optional(Type.String({ description: "New status: planned|in-progress|done|blocked|canceled. Avoid setting this directly unless you truly need an override: feature status is derived from child phases/tasks." })),
|
|
@@ -2956,7 +2989,7 @@ export default function planPiExtension(pi) {
|
|
|
2956
2989
|
if (!feature)
|
|
2957
2990
|
return { content: [{ type: "text", text: `Feature not found: ${params.featureId}` }], details: {} };
|
|
2958
2991
|
await st.writeGenerated();
|
|
2959
|
-
return { content: [{ type: "text", text:
|
|
2992
|
+
return { content: [{ type: "text", text: `✅ Feature updated: ${formatFeatureRef(feature.number)} — ${feature.name}${feature.shortId ? ` · ${feature.shortId}` : ""}` }], details: feature };
|
|
2960
2993
|
},
|
|
2961
2994
|
});
|
|
2962
2995
|
pi.registerTool({
|
|
@@ -2964,7 +2997,7 @@ export default function planPiExtension(pi) {
|
|
|
2964
2997
|
label: "Feature Delete",
|
|
2965
2998
|
description: "Delete a feature. Its phases are unlinked (featureId cleared) but NOT deleted unless cascade=true.",
|
|
2966
2999
|
parameters: Type.Object({
|
|
2967
|
-
featureId: Type.String({ description: "Feature
|
|
3000
|
+
featureId: Type.String({ description: "Feature ref: F00x, shortId, UUID, or name to delete" }),
|
|
2968
3001
|
cascade: Type.Optional(Type.Boolean({ description: "If true, also delete all phases belonging to this feature. Default: false" })),
|
|
2969
3002
|
}),
|
|
2970
3003
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
@@ -2998,40 +3031,52 @@ export default function planPiExtension(pi) {
|
|
|
2998
3031
|
pi.registerTool({
|
|
2999
3032
|
name: "phase_list",
|
|
3000
3033
|
label: "Phase List",
|
|
3001
|
-
description: "List
|
|
3034
|
+
description: "List phases (compact: F00x/P00x · shortId — title (status; N tasks) [F00x]). Filters: featureRef, status. Cheap discovery — do NOT read .planner/ files or plan_get full=true.",
|
|
3002
3035
|
parameters: Type.Object({
|
|
3003
|
-
|
|
3036
|
+
featureRef: Type.Optional(Type.String({ description: "Optional: filter to one feature (F00x/shortId/UUID/name)" })),
|
|
3037
|
+
status: Type.Optional(Type.String({ description: "Optional: filter by status name" })),
|
|
3004
3038
|
}),
|
|
3005
3039
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3006
3040
|
const st = await requirePlan(ctx);
|
|
3007
3041
|
if (!st)
|
|
3008
3042
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
3043
|
+
const features = (await st.loadFeatures()).features;
|
|
3009
3044
|
let phases = await st.loadAllPhases();
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3045
|
+
const fref = params.featureRef?.trim();
|
|
3046
|
+
if (fref) {
|
|
3047
|
+
const f = features.find((x) => formatFeatureRef(x.number) === fref || x.shortId === fref || x.id === fref || x.name.toLowerCase() === fref.toLowerCase());
|
|
3048
|
+
if (!f)
|
|
3049
|
+
return { content: [{ type: "text", text: `Feature not found: ${fref}` }], details: {} };
|
|
3050
|
+
phases = phases.filter((p) => p.featureId === f.id);
|
|
3051
|
+
}
|
|
3052
|
+
if (params.status)
|
|
3053
|
+
phases = phases.filter((p) => p.status === params.status);
|
|
3054
|
+
const summary = phases.map((p) => {
|
|
3055
|
+
const fNum = featureNumberOfPhase(p, features);
|
|
3056
|
+
const fTag = fNum !== undefined ? ` [F${pad(fNum)}]` : "";
|
|
3057
|
+
return `- ${formatPhaseRef(p.number, fNum)}${p.shortId ? ` · ${p.shortId}` : ""} — ${p.title} (${p.status}; ${p.tasks.length} tasks)${fTag}`;
|
|
3058
|
+
}).join("\n");
|
|
3059
|
+
return { content: [{ type: "text", text: summary || "No phases" }], details: {} };
|
|
3014
3060
|
},
|
|
3015
3061
|
});
|
|
3016
3062
|
pi.registerTool({
|
|
3017
3063
|
name: "phase_get",
|
|
3018
3064
|
label: "Phase Get",
|
|
3019
|
-
description: "
|
|
3065
|
+
description: "Show a phase. Compact identity by default (saves tokens); pass full=true to include the description.",
|
|
3020
3066
|
parameters: Type.Object({
|
|
3021
|
-
phaseId: Type.String({ description: "Phase
|
|
3067
|
+
phaseId: Type.String({ description: "Phase ref: F00x/P00x, bare P00x (global), shortId, UUID, or title" }),
|
|
3068
|
+
full: Type.Optional(Type.Boolean({ description: "If true, include the phase description. Default: compact identity only." })),
|
|
3022
3069
|
}),
|
|
3023
3070
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3024
3071
|
const st = await requirePlan(ctx);
|
|
3025
3072
|
if (!st)
|
|
3026
3073
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
}
|
|
3031
|
-
catch {
|
|
3074
|
+
const features = (await st.loadFeatures()).features;
|
|
3075
|
+
const phase = findPhaseByRef(await st.loadAllPhases(), features, params.phaseId.trim());
|
|
3076
|
+
if (!phase)
|
|
3032
3077
|
return { content: [{ type: "text", text: `Phase not found: ${params.phaseId}` }], details: {} };
|
|
3033
|
-
}
|
|
3034
|
-
return { content: [{ type: "text", text:
|
|
3078
|
+
const summary = `${phase.title} — ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, features))}${phase.shortId ? ` · ${phase.shortId}` : ""} (${phase.status}; ${phase.tasks.length} tasks)`;
|
|
3079
|
+
return { content: [{ type: "text", text: params.full ? `${summary}\n\n${phase.description || ""}` : summary }], details: {} };
|
|
3035
3080
|
},
|
|
3036
3081
|
});
|
|
3037
3082
|
pi.registerTool({
|
|
@@ -3055,7 +3100,7 @@ export default function planPiExtension(pi) {
|
|
|
3055
3100
|
await withFeatureLock(params.featureId, async () => {
|
|
3056
3101
|
const phases = await st.loadAllPhases();
|
|
3057
3102
|
const featurePhases = phases.filter((p) => p.featureId === params.featureId);
|
|
3058
|
-
const number =
|
|
3103
|
+
const number = await st.allocPhaseNumber();
|
|
3059
3104
|
const id = createPhaseId();
|
|
3060
3105
|
const shortId = createShortId(await st.assignedShortIds());
|
|
3061
3106
|
const priority = await st.nextPriority("phase", params.featureId);
|
|
@@ -3072,6 +3117,8 @@ export default function planPiExtension(pi) {
|
|
|
3072
3117
|
openQuestions: [], decisions: [], acceptedDecisions: [], completionCriteria: [], taskIds: [], tasks: [],
|
|
3073
3118
|
createdAt: now, updatedAt: now,
|
|
3074
3119
|
handoff: "", handoffUpdatedAt: "",
|
|
3120
|
+
handoffReadAt: "",
|
|
3121
|
+
handoffHistory: [],
|
|
3075
3122
|
};
|
|
3076
3123
|
await st.savePhase(phase);
|
|
3077
3124
|
// Atomic: serialize concurrent phase_create linking to the same feature.
|
|
@@ -3087,7 +3134,8 @@ export default function planPiExtension(pi) {
|
|
|
3087
3134
|
});
|
|
3088
3135
|
if (!phase)
|
|
3089
3136
|
return { content: [{ type: "text", text: "Phase creation failed." }], details: {} };
|
|
3090
|
-
|
|
3137
|
+
const phaseCreateFeatures = (await st.loadFeatures()).features;
|
|
3138
|
+
return { content: [{ type: "text", text: `✅ Phase created: ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, phaseCreateFeatures))} — ${phase.title}${phase.shortId ? ` · ${phase.shortId}` : ""}` }], details: phase };
|
|
3091
3139
|
},
|
|
3092
3140
|
});
|
|
3093
3141
|
pi.registerTool({
|
|
@@ -3095,7 +3143,7 @@ export default function planPiExtension(pi) {
|
|
|
3095
3143
|
label: "Phase Update",
|
|
3096
3144
|
description: "Update one or more fields of a phase. Only provided fields are changed. Supports re-linking to a feature via featureId. IMPORTANT: phase status is derived from task statuses, so do not update phase.status directly unless truly necessary.",
|
|
3097
3145
|
parameters: Type.Object({
|
|
3098
|
-
phaseId: Type.String({ description: "Phase
|
|
3146
|
+
phaseId: Type.String({ description: "Phase ref: F00x/P00x, bare P00x (global), shortId, UUID, or title" }),
|
|
3099
3147
|
title: Type.Optional(Type.String({ description: "New title" })),
|
|
3100
3148
|
status: Type.Optional(Type.String({ description: "New status: draft|discovery|planned|in-progress|done|blocked|canceled. Avoid setting this directly unless you truly need an override: phase status is derived from task statuses." })),
|
|
3101
3149
|
summary: Type.Optional(Type.String({ description: "New summary" })),
|
|
@@ -3176,7 +3224,8 @@ export default function planPiExtension(pi) {
|
|
|
3176
3224
|
phase.updatedAt = nowISO();
|
|
3177
3225
|
await st.savePhase(phase);
|
|
3178
3226
|
await st.writeGenerated();
|
|
3179
|
-
|
|
3227
|
+
const phaseUpdateFeatures = (await st.loadFeatures()).features;
|
|
3228
|
+
return { content: [{ type: "text", text: `✅ Phase updated: ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, phaseUpdateFeatures))} — ${phase.title}${phase.shortId ? ` · ${phase.shortId}` : ""}` }], details: phase };
|
|
3180
3229
|
},
|
|
3181
3230
|
});
|
|
3182
3231
|
pi.registerTool({
|
|
@@ -3184,7 +3233,7 @@ export default function planPiExtension(pi) {
|
|
|
3184
3233
|
label: "Phase Delete",
|
|
3185
3234
|
description: "Delete a phase. Its tasks are deleted with it (cascade). Unlinks the phase from its feature.",
|
|
3186
3235
|
parameters: Type.Object({
|
|
3187
|
-
phaseId: Type.String({ description: "Phase
|
|
3236
|
+
phaseId: Type.String({ description: "Phase ref: F00x/P00x, bare P00x (global), shortId, UUID, or title to delete" }),
|
|
3188
3237
|
}),
|
|
3189
3238
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3190
3239
|
const st = await requirePlan(ctx);
|
|
@@ -3216,42 +3265,64 @@ export default function planPiExtension(pi) {
|
|
|
3216
3265
|
pi.registerTool({
|
|
3217
3266
|
name: "task_list",
|
|
3218
3267
|
label: "Task List",
|
|
3219
|
-
description: "List tasks
|
|
3268
|
+
description: "List tasks (compact: F00x/P00x/T00x · shortId — title (status)). Filters: featureRef, phaseRef, status. Omit all to list every task. Cheap discovery — do NOT read .planner/ files or plan_get full=true.",
|
|
3220
3269
|
parameters: Type.Object({
|
|
3221
|
-
|
|
3270
|
+
featureRef: Type.Optional(Type.String({})),
|
|
3271
|
+
phaseRef: Type.Optional(Type.String({})),
|
|
3272
|
+
status: Type.Optional(Type.String({ description: "Optional: filter by status name" })),
|
|
3222
3273
|
}),
|
|
3223
3274
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3224
3275
|
const st = await requirePlan(ctx);
|
|
3225
3276
|
if (!st)
|
|
3226
3277
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3278
|
+
const features = (await st.loadFeatures()).features;
|
|
3279
|
+
let phases = await st.loadAllPhases();
|
|
3280
|
+
const fref = params.featureRef?.trim();
|
|
3281
|
+
if (fref) {
|
|
3282
|
+
const f = features.find((x) => formatFeatureRef(x.number) === fref || x.shortId === fref || x.id === fref || x.name.toLowerCase() === fref.toLowerCase());
|
|
3283
|
+
if (!f)
|
|
3284
|
+
return { content: [{ type: "text", text: `Feature not found: ${fref}` }], details: {} };
|
|
3285
|
+
phases = phases.filter((p) => p.featureId === f.id);
|
|
3286
|
+
}
|
|
3287
|
+
const pref = params.phaseRef?.trim();
|
|
3288
|
+
if (pref) {
|
|
3289
|
+
const p = findPhaseByRef(phases, features, pref);
|
|
3290
|
+
if (!p)
|
|
3291
|
+
return { content: [{ type: "text", text: `Phase not found: ${pref}` }], details: {} };
|
|
3292
|
+
phases = [p];
|
|
3293
|
+
}
|
|
3294
|
+
const out = [];
|
|
3295
|
+
for (const phase of phases) {
|
|
3296
|
+
for (const task of phase.tasks) {
|
|
3297
|
+
if (params.status && task.status !== params.status)
|
|
3298
|
+
continue;
|
|
3299
|
+
out.push(`- ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, features))}/T${pad(task.number)}${task.shortId ? ` · ${task.shortId}` : ""} — ${task.title} (${task.status})`);
|
|
3300
|
+
}
|
|
3233
3301
|
}
|
|
3234
|
-
|
|
3235
|
-
return { content: [{ type: "text", text: summary || "No tasks" }], details: { tasks: phase.tasks } };
|
|
3302
|
+
return { content: [{ type: "text", text: out.join("\n") || "No tasks" }], details: {} };
|
|
3236
3303
|
},
|
|
3237
3304
|
});
|
|
3238
3305
|
pi.registerTool({
|
|
3239
3306
|
name: "task_get",
|
|
3240
3307
|
label: "Task Get",
|
|
3241
|
-
description: "
|
|
3308
|
+
description: "Show a task. Compact identity by default (saves tokens); pass full=true to include the description + statusLog.",
|
|
3242
3309
|
parameters: Type.Object({
|
|
3243
|
-
taskId: Type.String({ description: "Task
|
|
3310
|
+
taskId: Type.String({ description: "Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title" }),
|
|
3311
|
+
full: Type.Optional(Type.Boolean({ description: "If true, include the task description + statusLog. Default: compact identity only." })),
|
|
3244
3312
|
}),
|
|
3245
3313
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3246
3314
|
const st = await requirePlan(ctx);
|
|
3247
3315
|
if (!st)
|
|
3248
3316
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
3249
|
-
const
|
|
3250
|
-
const
|
|
3251
|
-
|
|
3252
|
-
if (!task || !phase)
|
|
3317
|
+
const features = (await st.loadFeatures()).features;
|
|
3318
|
+
const found = findTaskByRef(await st.loadAllPhases(), features, params.taskId.trim());
|
|
3319
|
+
if (!found)
|
|
3253
3320
|
return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], details: {} };
|
|
3254
|
-
|
|
3321
|
+
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})`;
|
|
3322
|
+
if (!params.full)
|
|
3323
|
+
return { content: [{ type: "text", text: summary }], details: {} };
|
|
3324
|
+
const log = (found.task.statusLog ?? []).map((e) => ` - ${e.date.slice(0, 10)} ${e.title}`).join("\n");
|
|
3325
|
+
return { content: [{ type: "text", text: `${summary}\n\n${found.task.description || ""}${log ? `\n\nStatus log:\n${log}` : ""}` }], details: {} };
|
|
3255
3326
|
},
|
|
3256
3327
|
});
|
|
3257
3328
|
pi.registerTool({
|
|
@@ -3284,7 +3355,7 @@ export default function planPiExtension(pi) {
|
|
|
3284
3355
|
const initialStatus = params.status ?? "planned";
|
|
3285
3356
|
const existingPhase = await st.loadPhase(params.phaseId);
|
|
3286
3357
|
const task = {
|
|
3287
|
-
id: taskId, phaseId: params.phaseId, number:
|
|
3358
|
+
id: taskId, phaseId: params.phaseId, number: await st.allocTaskNumber(), shortId, priority, shortName,
|
|
3288
3359
|
title: params.title,
|
|
3289
3360
|
status: initialStatus,
|
|
3290
3361
|
description: params.description ?? "",
|
|
@@ -3315,7 +3386,7 @@ export default function planPiExtension(pi) {
|
|
|
3315
3386
|
label: "Task Update",
|
|
3316
3387
|
description: "Update one or more fields of a task. Only provided fields are changed. Do NOT use this tool to start or complete work; use task_start and task_complete for lifecycle transitions so startedAt/completedAt stay correct.",
|
|
3317
3388
|
parameters: Type.Object({
|
|
3318
|
-
taskId: Type.String({ description: "Task
|
|
3389
|
+
taskId: Type.String({ description: "Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title" }),
|
|
3319
3390
|
title: Type.Optional(Type.String({ description: "New title" })),
|
|
3320
3391
|
status: Type.Optional(Type.String({ description: "New status: planned|in-progress|done|blocked|canceled|rejected|deferred|waiting" })),
|
|
3321
3392
|
description: Type.Optional(Type.String({ description: "New description" })),
|
|
@@ -3403,7 +3474,7 @@ export default function planPiExtension(pi) {
|
|
|
3403
3474
|
label: "Task Delete",
|
|
3404
3475
|
description: "Delete a task from its phase.",
|
|
3405
3476
|
parameters: Type.Object({
|
|
3406
|
-
taskId: Type.String({ description: "Task
|
|
3477
|
+
taskId: Type.String({ description: "Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title to delete" }),
|
|
3407
3478
|
}),
|
|
3408
3479
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3409
3480
|
const st = await requirePlan(ctx);
|
|
@@ -3429,7 +3500,7 @@ export default function planPiExtension(pi) {
|
|
|
3429
3500
|
label: "Task Start",
|
|
3430
3501
|
description: "Set a task to in-progress. Use this BEFORE starting implementation work on any task. Sets startedAt timestamp automatically.",
|
|
3431
3502
|
parameters: Type.Object({
|
|
3432
|
-
taskId: Type.String({ description: "Task
|
|
3503
|
+
taskId: Type.String({ description: "Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title to start" }),
|
|
3433
3504
|
}),
|
|
3434
3505
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3435
3506
|
const st = await requirePlan(ctx);
|
|
@@ -3467,7 +3538,7 @@ export default function planPiExtension(pi) {
|
|
|
3467
3538
|
label: "Task Complete",
|
|
3468
3539
|
description: "Mark a task as done. Sets completedAt and startedAt (if missing) automatically. Checks for unchecked checklist items and warns unless force=true.",
|
|
3469
3540
|
parameters: Type.Object({
|
|
3470
|
-
taskId: Type.String({ description: "Task
|
|
3541
|
+
taskId: Type.String({ description: "Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title to complete" }),
|
|
3471
3542
|
force: Type.Optional(Type.Boolean({ description: "Skip checklist completion check. Default: false" })),
|
|
3472
3543
|
description_update: Type.Optional(Type.String({ description: "Post-hoc summary: commit hash(s), files touched, decisions made, updated code references with new line numbers. Keeps the planner alive and traceable." })),
|
|
3473
3544
|
}),
|
|
@@ -3607,6 +3678,7 @@ export default function planPiExtension(pi) {
|
|
|
3607
3678
|
// add seconds of latency on every turn if run unconditionally.
|
|
3608
3679
|
if (!plannerHeavyInitDone) {
|
|
3609
3680
|
await migrateToUuids(st);
|
|
3681
|
+
await migrateToGlobalSequence(st).catch(() => null);
|
|
3610
3682
|
await st.ensureShortIdsAndPriority().catch(() => null);
|
|
3611
3683
|
await ensureProjectLanguagePreferences(st).catch(() => null);
|
|
3612
3684
|
await maybeHealStatuses(st, ctx);
|
|
@@ -3688,12 +3760,12 @@ export default function planPiExtension(pi) {
|
|
|
3688
3760
|
: hasActiveWork
|
|
3689
3761
|
? plan.features.features.find((feature) => feature.status === "in-progress") ?? null
|
|
3690
3762
|
: null;
|
|
3691
|
-
const nextActivity =
|
|
3692
|
-
? (resume.nextSteps
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
: "Review the current plan and choose the next task."
|
|
3763
|
+
const nextActivity = handoffs.length > 0
|
|
3764
|
+
? "Read the most recent phase handoff (handoff show <ref>), validate it against current plan ordering and dependencies, then pick the next task. (Legacy resume.json nextSteps may be stale — the handoff is authoritative.)"
|
|
3765
|
+
: hasActiveWork
|
|
3766
|
+
? (resume.nextSteps[0]
|
|
3767
|
+
?? (currentTask ? `Resume task ${currentTask.id} — ${taskLabel(currentTask)}` : currentPhase ? `Resume phase ${currentPhase.id} — ${phaseLabel(currentPhase)}` : "Resume the active work stream"))
|
|
3768
|
+
: "Review the current plan and choose the next task.";
|
|
3697
3769
|
const webServer = server;
|
|
3698
3770
|
const webLocalUrl = webServer?.localUrl ?? webServer?.url ?? "";
|
|
3699
3771
|
const webLanUrl = webServer?.lanUrl ?? "";
|
|
@@ -3744,12 +3816,12 @@ export default function planPiExtension(pi) {
|
|
|
3744
3816
|
"═══════════════════════════════════════════════════════════════",
|
|
3745
3817
|
"MANDATORY OPERATIONAL PROTOCOL (violation = execution failure):",
|
|
3746
3818
|
"═══════════════════════════════════════════════════════════════",
|
|
3747
|
-
"1. HANDOFF HYGIENE: If one or more phases have a pending handoff (entity-scoped, phase.handoff), present the list, read the relevant one with handoff show <ref
|
|
3819
|
+
"1. HANDOFF HYGIENE: If one or more phases have a pending handoff (entity-scoped, phase.handoff), present the list, read the relevant one with handoff show <ref> for context. Handoffs are KEPT until a task in the phase starts (auto-archived to .planner/handoff-archive/) or the phase completes — no manual clear needed. They are non-blocking context artifacts, not locks — they never block task_start.",
|
|
3748
3820
|
"2. TASK LIFECYCLE: BEFORE coding ANY file: call task_start. AFTER finishing work on ANY task: call task_complete. No exceptions. No 'I'll do it later'. NOTE: Planner operations (plan_write_handoff / /planner handoff, task_start, task_complete, task_update, status reads) are NOT code edits. They are ALWAYS allowed regardless of task state — you may write/refresh the handoff even when no task is in-progress (e.g., all tasks done, or capturing state mid-flight). Never refuse to write a handoff because 'no task is open'; that is incorrect.",
|
|
3749
3821
|
"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.",
|
|
3750
3822
|
"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.",
|
|
3751
3823
|
"5. NO SHORTCUTS: If a tool blocks you, follow the protocol. Bypasses are for emergencies only, not for convenience.",
|
|
3752
|
-
"6. ENTITY REFERENCES
|
|
3824
|
+
"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.",
|
|
3753
3825
|
"═══════════════════════════════════════════════════════════════",
|
|
3754
3826
|
"",
|
|
3755
3827
|
// ── Project details ──
|
|
@@ -3780,7 +3852,7 @@ export default function planPiExtension(pi) {
|
|
|
3780
3852
|
resume.currentPhaseId ? ` current phase: ${resume.currentPhaseId}` : " current phase: (none)",
|
|
3781
3853
|
resume.inProgressTaskIds.length ? ` in-progress tasks: ${resume.inProgressTaskIds.join(", ")}` : " in-progress tasks: (none)",
|
|
3782
3854
|
resume.blockers.length ? ` blockers: ${resume.blockers.join("; ")}` : " blockers: (none)",
|
|
3783
|
-
resume.nextSteps.length ? ` next steps: ${resume.nextSteps.join("; ")}` : " next steps: (none)",
|
|
3855
|
+
resume.nextSteps.length ? ` next steps: ${resume.nextSteps.join("; ")}${handoffs.length > 0 ? " (⚠️ may be stale — handoff is authoritative)" : resume.nextStepsUpdatedAt ? ` (last updated ${resume.nextStepsUpdatedAt}, may be stale)` : ""}` : " next steps: (none)",
|
|
3784
3856
|
resume.lastSessionSummary ? ` last session: ${resume.lastSessionSummary}` : "",
|
|
3785
3857
|
"",
|
|
3786
3858
|
recentActivity.length ? "Recent activity (most recent first):" : "",
|
|
@@ -3824,7 +3896,7 @@ export default function planPiExtension(pi) {
|
|
|
3824
3896
|
...handoffs.map((h, i) => ` [${i + 1}] ${h.compositeRef} — ${h.updatedAt} — "${h.firstLine}"`),
|
|
3825
3897
|
handoffs.length > 0 ? "Read the relevant one with handoff show <ref> (or /planner handoff show <ref>) as previous-session context, but validate it against the current planner state, ordering, and dependencies before treating any target as the next task." : "",
|
|
3826
3898
|
handoffs.length > 0 ? "If planner state shows no task/phase in-progress, do NOT present a handoff target as the current focus; present it only as a candidate to validate." : "",
|
|
3827
|
-
handoffs.length > 0 ? "
|
|
3899
|
+
handoffs.length > 0 ? "Handoffs are KEPT until a task in the phase starts (auto-archived to .planner/handoff-archive/) or the phase completes — read with handoff show <ref> for context; no manual clear needed." : "",
|
|
3828
3900
|
"Plan tools available: project_set_language_preferences, plan_init, project_update, requirement_list, requirement_create, requirement_update, requirement_delete, plan_get, feature_list, feature_get, feature_create, feature_update, feature_delete, phase_list, phase_get, phase_create, phase_update, phase_delete, task_list, task_get, task_create, task_update, task_delete, task_start, task_complete, plan_render, plan_get_handoff, plan_write_handoff, plan_delete_handoff, handoff_list, handoff_show, handoff_write, handoff_clear, plan_authorize_bypass, plan_clear_bypass",
|
|
3829
3901
|
].filter(Boolean).join("\n");
|
|
3830
3902
|
contextBlockCache = contextBlock;
|