@agent-plan/pi-adapter 0.2.19-next.1 → 0.2.19-next.10

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.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
- "- When work is fully resumed and this handoff is no longer needed, clear the phase handoff with handoff clear <ref> (or /planner handoff clear).",
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 = (await st.loadFeatures()).features.length + 1;
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.id} — ${feature.name}`, "info");
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.id} — ${feature.name} (${feature.status})`, "info");
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 = featurePhases.reduce((max, p) => Math.max(max, p.number), 0) + 1;
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.id} — ${title} (feature ${feature.name})`, "info");
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.id}…`, "info");
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.id} is now **planned**.`, "info");
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.id}`, "info");
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.id} — ${phase.title} (${phase.status})`, "info");
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 = phase.tasks.length + 1;
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.id} discussed and updated.`, "info");
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.id}`, "info");
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.id} — ${task.title} (${task.status})`, "info");
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
- // Hygiene notice (non-blocking): surface an existing handoff but never
1981
- // block task_start — the handoff is a captured-context artifact, not a
1982
- // lock (a small modification after writing a handoff should start without
1983
- // forcing deletion, which would lose context).
1984
- if ((await st.listHandoffs()).length > 0) {
1985
- ctx.ui.notify("ℹ️ One or more phases have a pending handoff — if relevant to the task you're starting, read it with /planner handoff show <ref>, then clear with /planner handoff clear. Proceeding with task start.", "info");
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.id} — ${task.title} (in-progress)`, "info");
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.id} — ${task.title} (done)`, "info");
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 = await st.authorizeGuardBypass(Number.isFinite(mins) && mins > 0 ? mins : 15);
2225
- ctx.ui.notify(`Guard bypass authorized until ${until}. edit/write will work without a task in-progress for that window.`, "info");
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
- await st.clearGuardBypass();
2229
- ctx.ui.notify("Guard bypass revoked. edit/write now requires a task in-progress again.", "info");
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}. (handoffUpdatedAt preserved as audit)` }], details: { phaseRef: r.compositeRef, phaseId: r.phase.id } };
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 tools can proceed even when no task is in-progress. Use this ONLY after the user explicitly authorizes proceeding without a task. Harness-agnostic: stored in resume.json so all adapters respect it.",
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 st = await requirePlan(ctx);
2780
- if (!st)
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 = await st.authorizeGuardBypass(mins);
2784
- return { content: [{ type: "text", text: `Guard bypass authorized until ${until}. edit/write is allowed without a task in-progress for ${mins} minutes.` }], details: { until } };
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 any active guard bypass so edit/write again requires a task in-progress.",
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 st = await requirePlan(ctx);
2794
- if (!st)
2820
+ const ok = await requirePlan(ctx);
2821
+ if (!ok)
2795
2822
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
2796
- await st.clearGuardBypass();
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 all features with their ids, statuses, and phase counts.",
2805
- parameters: Type.Object({}),
2806
- async execute(_id, _params, _signal, _onUpdate, ctx) {
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 features = (await st.loadFeatures()).features;
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 `- ${statusIcon(f.status)} ${f.id} — ${f.name} (${fp.length} phases, ${fp.reduce((t, p) => t + p.tasks.length, 0)} tasks)`;
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: { features } };
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: "Read full details of a feature, including its phases and their tasks.",
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 ID, e.g. feature-001-dynamic-header" }),
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 feature = features.find((f) => f.id === params.featureId);
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: ${params.featureId}` }], details: {} };
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
- return { content: [{ type: "text", text: `${statusIcon(feature.status)} ${feature.name} (${feature.id}) — ${phases.length} phases` }], details: { feature, phases } };
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: currentFeatures.length + 1,
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: `Feature created: ${feature.id}` }], details: feature };
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 ID" }),
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: `Feature updated: ${feature.id} (${feature.status})` }], details: feature };
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 ID to delete" }),
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 all phases (optionally filtered by featureId) with id, status, and task counts.",
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
- featureId: Type.Optional(Type.String({ description: "If provided, only list phases of this feature" })),
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
- if (params.featureId)
3011
- phases = phases.filter((p) => p.featureId === params.featureId);
3012
- const summary = phases.map((p) => `- ${statusIcon(p.status)} ${p.id} — ${p.title} (${p.tasks.length} tasks, feature ${p.featureId ?? "none"})`).join("\n");
3013
- return { content: [{ type: "text", text: summary || "No phases" }], details: { phases } };
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: "Read full details of a phase, including its tasks.",
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 ID, e.g. phase-01-setup" }),
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
- let phase;
3028
- try {
3029
- phase = await st.loadPhase(params.phaseId);
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: `${statusIcon(phase.status)} ${phase.title} (${phase.id}) — ${phase.tasks.length} tasks` }], details: phase };
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 = featurePhases.reduce((max, p) => Math.max(max, p.number), 0) + 1;
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
- return { content: [{ type: "text", text: `Phase created: ${phase.id}` }], details: phase };
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 ID" }),
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
- return { content: [{ type: "text", text: `Phase updated: ${phase.id} (${phase.status})` }], details: phase };
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 ID to delete" }),
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 of a phase with id, status, and title.",
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
- phaseId: Type.String({ description: "Phase ID" }),
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
- let phase;
3228
- try {
3229
- phase = await st.loadPhase(params.phaseId);
3230
- }
3231
- catch {
3232
- return { content: [{ type: "text", text: `Phase not found: ${params.phaseId}` }], details: {} };
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
- const summary = phase.tasks.map((t) => `- ${statusIcon(t.status)} ${t.id} — ${t.title}`).join("\n");
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: "Read full details of a single task.",
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 ID" }),
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 phases = await st.loadAllPhases();
3250
- const phase = phases.find((p) => p.tasks.some((t) => t.id === params.taskId));
3251
- const task = phase?.tasks.find((t) => t.id === params.taskId);
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
- return { content: [{ type: "text", text: `${statusIcon(task.status)} ${task.title} (${task.id}) — phase ${phase.id}` }], details: { task, phaseId: phase.id } };
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: existingPhase.tasks.length + 1, shortId, priority, shortName,
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 ID" }),
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 ID to delete" }),
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 ID to start" }),
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 ID to complete" }),
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 = hasActiveWork
3692
- ? (resume.nextSteps[0]
3693
- ?? (currentTask ? `Resume task ${currentTask.id} — ${taskLabel(currentTask)}` : currentPhase ? `Resume phase ${currentPhase.id} — ${phaseLabel(currentPhase)}` : "Resume the active work stream"))
3694
- : (handoffs.length > 0
3695
- ? "Read the most recent phase handoff (handoff show <ref>), validate it against current plan ordering and dependencies, then pick the next task."
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>, then CLEAR it with handoff clear <ref> once consumed. Handoffs are non-blocking context artifacts, not locks — they never block task_start.",
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: When you mention a feature/phase/task in chat, use its composite ID (F00x / P00x / T00x, e.g. T003(P002/F001)) or its 5-char shortId (e.g. UUXD1) — NEVER a raw UUID like bd6ed366. Tool responses already show these refs (compositeRef, shortId); reuse them.",
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 ? "Once a handoff is fully consumed and work is safely resumed, clear it with handoff clear <ref> (or /planner handoff clear)." : "",
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;