@agent-plan/pi-adapter 0.2.19-next.4 → 0.2.19-next.6

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,iCAAiC,CAAC;AA6uBtF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA67F9D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,iCAAiC,CAAC;AAgvBtF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAs9F9D"}
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";
@@ -512,6 +512,9 @@ function statusIcon(status) {
512
512
  };
513
513
  return icons[status] ?? "❓";
514
514
  }
515
+ function pad(n) {
516
+ return String(n).padStart(3, "0");
517
+ }
515
518
  function applyTaskLifecycleDates(task, nextStatus, now) {
516
519
  const previousStatus = task.status;
517
520
  if (nextStatus === "in-progress" && !task.startedAt) {
@@ -1146,19 +1149,6 @@ export default function planPiExtension(pi) {
1146
1149
  ?? phases.find((phase) => phase.title.toLowerCase().includes(normalized))
1147
1150
  ?? null;
1148
1151
  }
1149
- function findTaskByRef(phases, ref) {
1150
- const normalized = ref.trim().toLowerCase();
1151
- if (!normalized)
1152
- return null;
1153
- for (const phase of phases) {
1154
- const task = phase.tasks.find((entryTask) => entryTask.id.toLowerCase() === normalized
1155
- || entryTask.title.toLowerCase() === normalized
1156
- || entryTask.title.toLowerCase().includes(normalized));
1157
- if (task)
1158
- return { phase, task };
1159
- }
1160
- return null;
1161
- }
1162
1152
  function profile_packageManager(pkg, lockfile) {
1163
1153
  if (pkg?.packageManager)
1164
1154
  return pkg.packageManager;
@@ -1363,7 +1353,7 @@ export default function planPiExtension(pi) {
1363
1353
  return;
1364
1354
  }
1365
1355
  const now = nowISO();
1366
- const featureNumber = (await st.loadFeatures()).features.length + 1;
1356
+ const featureNumber = await st.allocFeatureNumber();
1367
1357
  const shortId = createShortId(await st.assignedShortIds());
1368
1358
  const priority = await st.nextPriority("feature");
1369
1359
  const feature = {
@@ -1392,7 +1382,7 @@ export default function planPiExtension(pi) {
1392
1382
  return doc;
1393
1383
  });
1394
1384
  await st.writeGenerated();
1395
- 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");
1396
1386
  return;
1397
1387
  }
1398
1388
  if (b === "show") {
@@ -1473,7 +1463,7 @@ export default function planPiExtension(pi) {
1473
1463
  });
1474
1464
  feature = updatedDoc.features.find((entry) => entry.id === featureId) ?? feature;
1475
1465
  await st.writeGenerated();
1476
- 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");
1477
1467
  return;
1478
1468
  }
1479
1469
  if (b === "delete") {
@@ -1550,7 +1540,7 @@ export default function planPiExtension(pi) {
1550
1540
  await withFeatureLock(feature.id, async () => {
1551
1541
  const phases = await st.loadAllPhases();
1552
1542
  const featurePhases = phases.filter((p) => p.featureId === feature.id);
1553
- const number = featurePhases.reduce((max, p) => Math.max(max, p.number), 0) + 1;
1543
+ const number = await st.allocPhaseNumber();
1554
1544
  const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1555
1545
  const id = createPhaseId();
1556
1546
  const shortId = createShortId(await st.assignedShortIds());
@@ -1577,7 +1567,7 @@ export default function planPiExtension(pi) {
1577
1567
  });
1578
1568
  if (!phase)
1579
1569
  return;
1580
- 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");
1581
1571
  const startDiscuss = await ctx.ui.input("Start phase discuss now? Type yes to continue");
1582
1572
  if (isYes(startDiscuss)) {
1583
1573
  await handlePlanner(`phase discuss ${phase.id}`, ctx);
@@ -1602,7 +1592,7 @@ export default function planPiExtension(pi) {
1602
1592
  phase.status = "discovery";
1603
1593
  phase.updatedAt = nowISO();
1604
1594
  await st.savePhase(phase);
1605
- ctx.ui.notify(`Discovery for ${phase.id}…`, "info");
1595
+ ctx.ui.notify(`Discovery for ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, (await st.loadFeatures()).features))}…`, "info");
1606
1596
  // ── Recap: give full context before planning tasks ──
1607
1597
  const recapPlan = await st.loadAll();
1608
1598
  const recapProject = recapPlan.project;
@@ -1667,7 +1657,7 @@ export default function planPiExtension(pi) {
1667
1657
  phase.updatedAt = nowISO();
1668
1658
  await st.savePhase(phase);
1669
1659
  await st.writeGenerated();
1670
- 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");
1671
1661
  return;
1672
1662
  }
1673
1663
  if (b === "show") {
@@ -1715,7 +1705,7 @@ export default function planPiExtension(pi) {
1715
1705
  }
1716
1706
  await st.deletePhase(phase.id);
1717
1707
  await st.writeGenerated();
1718
- 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");
1719
1709
  return;
1720
1710
  }
1721
1711
  if (b === "update") {
@@ -1767,7 +1757,7 @@ export default function planPiExtension(pi) {
1767
1757
  phase.updatedAt = nowISO();
1768
1758
  await st.savePhase(phase);
1769
1759
  await st.writeGenerated();
1770
- 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");
1771
1761
  return;
1772
1762
  }
1773
1763
  ctx.ui.notify(`Unknown phase action "${b}". Try: add, show, discuss, delete, update`, "warning");
@@ -1793,7 +1783,7 @@ export default function planPiExtension(pi) {
1793
1783
  return;
1794
1784
  }
1795
1785
  const shortName = clampSlug(title, 30, `task-${Date.now().toString(36)}`);
1796
- const taskNum = phase.tasks.length + 1;
1786
+ const taskNum = await st.allocTaskNumber();
1797
1787
  const taskId = createTaskId();
1798
1788
  const shortId = createShortId(await st.assignedShortIds());
1799
1789
  const priority = await st.nextPriority("task", phase.id);
@@ -1825,7 +1815,7 @@ export default function planPiExtension(pi) {
1825
1815
  }
1826
1816
  if (b === "discuss") {
1827
1817
  const phases = await st.loadAllPhases();
1828
- let resolved = subArgs.trim() ? findTaskByRef(phases, subArgs.trim()) : null;
1818
+ let resolved = subArgs.trim() ? findTaskByRef(phases, (await st.loadFeatures()).features, subArgs.trim()) : null;
1829
1819
  if (!resolved) {
1830
1820
  const phase = await pickPhase();
1831
1821
  if (!phase) {
@@ -1859,14 +1849,14 @@ export default function planPiExtension(pi) {
1859
1849
  phase.updatedAt = nowISO();
1860
1850
  await st.savePhase(phase);
1861
1851
  await st.writeGenerated();
1862
- 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");
1863
1853
  return;
1864
1854
  }
1865
1855
  if (b === "show") {
1866
1856
  let phase = null;
1867
1857
  let task = null;
1868
1858
  if (subArgs.trim()) {
1869
- const resolved = findTaskByRef(await st.loadAllPhases(), subArgs.trim());
1859
+ const resolved = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, subArgs.trim());
1870
1860
  phase = resolved?.phase ?? null;
1871
1861
  task = resolved?.task ?? null;
1872
1862
  }
@@ -1928,7 +1918,7 @@ export default function planPiExtension(pi) {
1928
1918
  phase.updatedAt = nowISO();
1929
1919
  await st.savePhase(phase);
1930
1920
  await st.writeGenerated();
1931
- 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");
1932
1922
  return;
1933
1923
  }
1934
1924
  if (b === "update") {
@@ -1977,14 +1967,14 @@ export default function planPiExtension(pi) {
1977
1967
  phase.updatedAt = now;
1978
1968
  await st.savePhase(phase);
1979
1969
  await st.writeGenerated();
1980
- 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");
1981
1971
  return;
1982
1972
  }
1983
1973
  if (b === "start") {
1984
1974
  let phase = null;
1985
1975
  let task = null;
1986
1976
  if (subArgs.trim()) {
1987
- const resolved = findTaskByRef(await st.loadAllPhases(), subArgs.trim());
1977
+ const resolved = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, subArgs.trim());
1988
1978
  phase = resolved?.phase ?? null;
1989
1979
  task = resolved?.task ?? null;
1990
1980
  }
@@ -2036,14 +2026,14 @@ export default function planPiExtension(pi) {
2036
2026
  phase.updatedAt = now;
2037
2027
  await st.savePhase(phase);
2038
2028
  await st.writeGenerated();
2039
- 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");
2040
2030
  return;
2041
2031
  }
2042
2032
  if (b === "complete") {
2043
2033
  let phase = null;
2044
2034
  let task = null;
2045
2035
  if (subArgs.trim()) {
2046
- const resolved = findTaskByRef(await st.loadAllPhases(), subArgs.trim());
2036
+ const resolved = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, subArgs.trim());
2047
2037
  phase = resolved?.phase ?? null;
2048
2038
  task = resolved?.task ?? null;
2049
2039
  }
@@ -2091,7 +2081,7 @@ export default function planPiExtension(pi) {
2091
2081
  phase.updatedAt = now;
2092
2082
  await st.savePhase(phase);
2093
2083
  await st.writeGenerated();
2094
- 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");
2095
2085
  return;
2096
2086
  }
2097
2087
  ctx.ui.notify(`Unknown task action "${b}". Try: add, show, discuss, delete, update, start, complete`, "warning");
@@ -2836,38 +2826,44 @@ export default function planPiExtension(pi) {
2836
2826
  pi.registerTool({
2837
2827
  name: "feature_list",
2838
2828
  label: "Feature List",
2839
- description: "List all features with their ids, statuses, and phase counts.",
2840
- parameters: Type.Object({}),
2841
- async execute(_id, _params, _signal, _onUpdate, ctx) {
2829
+ 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.",
2830
+ parameters: Type.Object({ featureRef: Type.Optional(Type.String({ description: "Optional: filter to one feature (F00x/shortId/UUID/name)" })) }),
2831
+ async execute(_id, params, _signal, _onUpdate, ctx) {
2842
2832
  const st = await requirePlan(ctx);
2843
2833
  if (!st)
2844
2834
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
2845
- const features = (await st.loadFeatures()).features;
2835
+ const all = (await st.loadFeatures()).features;
2836
+ const ref = params.featureRef?.trim();
2837
+ const matchF = (f) => !ref || formatFeatureRef(f.number) === ref || f.shortId === ref || f.id === ref || f.name.toLowerCase() === ref.toLowerCase();
2838
+ const features = all.filter(matchF);
2846
2839
  const phases = await st.loadAllPhases();
2847
2840
  const summary = features.map((f) => {
2848
2841
  const fp = phases.filter((p) => p.featureId === f.id);
2849
- return `- ${statusIcon(f.status)} ${f.id} — ${f.name} (${fp.length} phases, ${fp.reduce((t, p) => t + p.tasks.length, 0)} tasks)`;
2842
+ 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)`;
2850
2843
  }).join("\n");
2851
- return { content: [{ type: "text", text: summary || "No features" }], details: { features } };
2844
+ return { content: [{ type: "text", text: summary || "No features" }], details: {} };
2852
2845
  },
2853
2846
  });
2854
2847
  pi.registerTool({
2855
2848
  name: "feature_get",
2856
2849
  label: "Feature Get",
2857
- description: "Read full details of a feature, including its phases and their tasks.",
2850
+ description: "Show a feature. Compact identity by default (saves tokens); pass full=true to include the description.",
2858
2851
  parameters: Type.Object({
2859
- featureId: Type.String({ description: "Feature ID, e.g. feature-001-dynamic-header" }),
2852
+ featureId: Type.String({ description: "Feature ref: F00x, shortId, UUID, or name" }),
2853
+ full: Type.Optional(Type.Boolean({ description: "If true, include the feature description. Default: compact identity only." })),
2860
2854
  }),
2861
2855
  async execute(_id, params, _signal, _onUpdate, ctx) {
2862
2856
  const st = await requirePlan(ctx);
2863
2857
  if (!st)
2864
2858
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
2865
2859
  const features = (await st.loadFeatures()).features;
2866
- const feature = features.find((f) => f.id === params.featureId);
2860
+ const ref = params.featureId.trim();
2861
+ const feature = features.find((f) => formatFeatureRef(f.number) === ref || f.shortId === ref || f.id === ref || f.name.toLowerCase() === ref.toLowerCase());
2867
2862
  if (!feature)
2868
- return { content: [{ type: "text", text: `Feature not found: ${params.featureId}` }], details: {} };
2863
+ return { content: [{ type: "text", text: `Feature not found: ${ref}` }], details: {} };
2869
2864
  const phases = (await st.loadAllPhases()).filter((p) => p.featureId === feature.id);
2870
- return { content: [{ type: "text", text: `${statusIcon(feature.status)} ${feature.name} (${feature.id}) — ${phases.length} phases` }], details: { feature, phases } };
2865
+ const summary = `${feature.name} — ${formatFeatureRef(feature.number)}${feature.shortId ? ` · ${feature.shortId}` : ""} (${feature.status}; ${phases.length} phases)`;
2866
+ return { content: [{ type: "text", text: params.full ? `${summary}\n\n${feature.description || ""}` : summary }], details: {} };
2871
2867
  },
2872
2868
  });
2873
2869
  pi.registerTool({
@@ -2890,7 +2886,7 @@ export default function planPiExtension(pi) {
2890
2886
  const priority = await st.nextPriority("feature");
2891
2887
  const feature = {
2892
2888
  id: createFeatureId(),
2893
- number: currentFeatures.length + 1,
2889
+ number: await st.allocFeatureNumber(),
2894
2890
  shortId,
2895
2891
  priority,
2896
2892
  name: params.name,
@@ -2924,7 +2920,7 @@ export default function planPiExtension(pi) {
2924
2920
  label: "Feature Update",
2925
2921
  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.",
2926
2922
  parameters: Type.Object({
2927
- featureId: Type.String({ description: "Feature ID" }),
2923
+ featureId: Type.String({ description: "Feature ref: F00x, shortId, UUID, or name" }),
2928
2924
  name: Type.Optional(Type.String({ description: "New name" })),
2929
2925
  description: Type.Optional(Type.String({ description: "New description" })),
2930
2926
  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." })),
@@ -2999,7 +2995,7 @@ export default function planPiExtension(pi) {
2999
2995
  label: "Feature Delete",
3000
2996
  description: "Delete a feature. Its phases are unlinked (featureId cleared) but NOT deleted unless cascade=true.",
3001
2997
  parameters: Type.Object({
3002
- featureId: Type.String({ description: "Feature ID to delete" }),
2998
+ featureId: Type.String({ description: "Feature ref: F00x, shortId, UUID, or name to delete" }),
3003
2999
  cascade: Type.Optional(Type.Boolean({ description: "If true, also delete all phases belonging to this feature. Default: false" })),
3004
3000
  }),
3005
3001
  async execute(_id, params, _signal, _onUpdate, ctx) {
@@ -3033,40 +3029,52 @@ export default function planPiExtension(pi) {
3033
3029
  pi.registerTool({
3034
3030
  name: "phase_list",
3035
3031
  label: "Phase List",
3036
- description: "List all phases (optionally filtered by featureId) with id, status, and task counts.",
3032
+ 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.",
3037
3033
  parameters: Type.Object({
3038
- featureId: Type.Optional(Type.String({ description: "If provided, only list phases of this feature" })),
3034
+ featureRef: Type.Optional(Type.String({ description: "Optional: filter to one feature (F00x/shortId/UUID/name)" })),
3035
+ status: Type.Optional(Type.String({ description: "Optional: filter by status name" })),
3039
3036
  }),
3040
3037
  async execute(_id, params, _signal, _onUpdate, ctx) {
3041
3038
  const st = await requirePlan(ctx);
3042
3039
  if (!st)
3043
3040
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
3041
+ const features = (await st.loadFeatures()).features;
3044
3042
  let phases = await st.loadAllPhases();
3045
- if (params.featureId)
3046
- phases = phases.filter((p) => p.featureId === params.featureId);
3047
- const summary = phases.map((p) => `- ${statusIcon(p.status)} ${p.id} — ${p.title} (${p.tasks.length} tasks, feature ${p.featureId ?? "none"})`).join("\n");
3048
- return { content: [{ type: "text", text: summary || "No phases" }], details: { phases } };
3043
+ const fref = params.featureRef?.trim();
3044
+ if (fref) {
3045
+ const f = features.find((x) => formatFeatureRef(x.number) === fref || x.shortId === fref || x.id === fref || x.name.toLowerCase() === fref.toLowerCase());
3046
+ if (!f)
3047
+ return { content: [{ type: "text", text: `Feature not found: ${fref}` }], details: {} };
3048
+ phases = phases.filter((p) => p.featureId === f.id);
3049
+ }
3050
+ if (params.status)
3051
+ phases = phases.filter((p) => p.status === params.status);
3052
+ const summary = phases.map((p) => {
3053
+ const fNum = featureNumberOfPhase(p, features);
3054
+ const fTag = fNum !== undefined ? ` [F${pad(fNum)}]` : "";
3055
+ return `- ${formatPhaseRef(p.number, fNum)}${p.shortId ? ` · ${p.shortId}` : ""} — ${p.title} (${p.status}; ${p.tasks.length} tasks)${fTag}`;
3056
+ }).join("\n");
3057
+ return { content: [{ type: "text", text: summary || "No phases" }], details: {} };
3049
3058
  },
3050
3059
  });
3051
3060
  pi.registerTool({
3052
3061
  name: "phase_get",
3053
3062
  label: "Phase Get",
3054
- description: "Read full details of a phase, including its tasks.",
3063
+ description: "Show a phase. Compact identity by default (saves tokens); pass full=true to include the description.",
3055
3064
  parameters: Type.Object({
3056
- phaseId: Type.String({ description: "Phase ID, e.g. phase-01-setup" }),
3065
+ phaseId: Type.String({ description: "Phase ref: F00x/P00x, bare P00x (global), shortId, UUID, or title" }),
3066
+ full: Type.Optional(Type.Boolean({ description: "If true, include the phase description. Default: compact identity only." })),
3057
3067
  }),
3058
3068
  async execute(_id, params, _signal, _onUpdate, ctx) {
3059
3069
  const st = await requirePlan(ctx);
3060
3070
  if (!st)
3061
3071
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
3062
- let phase;
3063
- try {
3064
- phase = await st.loadPhase(params.phaseId);
3065
- }
3066
- catch {
3072
+ const features = (await st.loadFeatures()).features;
3073
+ const phase = findPhaseByRef(await st.loadAllPhases(), features, params.phaseId.trim());
3074
+ if (!phase)
3067
3075
  return { content: [{ type: "text", text: `Phase not found: ${params.phaseId}` }], details: {} };
3068
- }
3069
- return { content: [{ type: "text", text: `${statusIcon(phase.status)} ${phase.title} (${phase.id}) — ${phase.tasks.length} tasks` }], details: phase };
3076
+ const summary = `${phase.title} — ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, features))}${phase.shortId ? ` · ${phase.shortId}` : ""} (${phase.status}; ${phase.tasks.length} tasks)`;
3077
+ return { content: [{ type: "text", text: params.full ? `${summary}\n\n${phase.description || ""}` : summary }], details: {} };
3070
3078
  },
3071
3079
  });
3072
3080
  pi.registerTool({
@@ -3090,7 +3098,7 @@ export default function planPiExtension(pi) {
3090
3098
  await withFeatureLock(params.featureId, async () => {
3091
3099
  const phases = await st.loadAllPhases();
3092
3100
  const featurePhases = phases.filter((p) => p.featureId === params.featureId);
3093
- const number = featurePhases.reduce((max, p) => Math.max(max, p.number), 0) + 1;
3101
+ const number = await st.allocPhaseNumber();
3094
3102
  const id = createPhaseId();
3095
3103
  const shortId = createShortId(await st.assignedShortIds());
3096
3104
  const priority = await st.nextPriority("phase", params.featureId);
@@ -3133,7 +3141,7 @@ export default function planPiExtension(pi) {
3133
3141
  label: "Phase Update",
3134
3142
  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.",
3135
3143
  parameters: Type.Object({
3136
- phaseId: Type.String({ description: "Phase ID" }),
3144
+ phaseId: Type.String({ description: "Phase ref: F00x/P00x, bare P00x (global), shortId, UUID, or title" }),
3137
3145
  title: Type.Optional(Type.String({ description: "New title" })),
3138
3146
  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." })),
3139
3147
  summary: Type.Optional(Type.String({ description: "New summary" })),
@@ -3223,7 +3231,7 @@ export default function planPiExtension(pi) {
3223
3231
  label: "Phase Delete",
3224
3232
  description: "Delete a phase. Its tasks are deleted with it (cascade). Unlinks the phase from its feature.",
3225
3233
  parameters: Type.Object({
3226
- phaseId: Type.String({ description: "Phase ID to delete" }),
3234
+ phaseId: Type.String({ description: "Phase ref: F00x/P00x, bare P00x (global), shortId, UUID, or title to delete" }),
3227
3235
  }),
3228
3236
  async execute(_id, params, _signal, _onUpdate, ctx) {
3229
3237
  const st = await requirePlan(ctx);
@@ -3255,42 +3263,64 @@ export default function planPiExtension(pi) {
3255
3263
  pi.registerTool({
3256
3264
  name: "task_list",
3257
3265
  label: "Task List",
3258
- description: "List tasks of a phase with id, status, and title.",
3266
+ 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.",
3259
3267
  parameters: Type.Object({
3260
- phaseId: Type.String({ description: "Phase ID" }),
3268
+ featureRef: Type.Optional(Type.String({})),
3269
+ phaseRef: Type.Optional(Type.String({})),
3270
+ status: Type.Optional(Type.String({ description: "Optional: filter by status name" })),
3261
3271
  }),
3262
3272
  async execute(_id, params, _signal, _onUpdate, ctx) {
3263
3273
  const st = await requirePlan(ctx);
3264
3274
  if (!st)
3265
3275
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
3266
- let phase;
3267
- try {
3268
- phase = await st.loadPhase(params.phaseId);
3269
- }
3270
- catch {
3271
- return { content: [{ type: "text", text: `Phase not found: ${params.phaseId}` }], details: {} };
3276
+ const features = (await st.loadFeatures()).features;
3277
+ let phases = await st.loadAllPhases();
3278
+ const fref = params.featureRef?.trim();
3279
+ if (fref) {
3280
+ const f = features.find((x) => formatFeatureRef(x.number) === fref || x.shortId === fref || x.id === fref || x.name.toLowerCase() === fref.toLowerCase());
3281
+ if (!f)
3282
+ return { content: [{ type: "text", text: `Feature not found: ${fref}` }], details: {} };
3283
+ phases = phases.filter((p) => p.featureId === f.id);
3284
+ }
3285
+ const pref = params.phaseRef?.trim();
3286
+ if (pref) {
3287
+ const p = findPhaseByRef(phases, features, pref);
3288
+ if (!p)
3289
+ return { content: [{ type: "text", text: `Phase not found: ${pref}` }], details: {} };
3290
+ phases = [p];
3291
+ }
3292
+ const out = [];
3293
+ for (const phase of phases) {
3294
+ for (const task of phase.tasks) {
3295
+ if (params.status && task.status !== params.status)
3296
+ continue;
3297
+ out.push(`- ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, features))}/T${pad(task.number)}${task.shortId ? ` · ${task.shortId}` : ""} — ${task.title} (${task.status})`);
3298
+ }
3272
3299
  }
3273
- const summary = phase.tasks.map((t) => `- ${statusIcon(t.status)} ${t.id} — ${t.title}`).join("\n");
3274
- return { content: [{ type: "text", text: summary || "No tasks" }], details: { tasks: phase.tasks } };
3300
+ return { content: [{ type: "text", text: out.join("\n") || "No tasks" }], details: {} };
3275
3301
  },
3276
3302
  });
3277
3303
  pi.registerTool({
3278
3304
  name: "task_get",
3279
3305
  label: "Task Get",
3280
- description: "Read full details of a single task.",
3306
+ description: "Show a task. Compact identity by default (saves tokens); pass full=true to include the description + statusLog.",
3281
3307
  parameters: Type.Object({
3282
- taskId: Type.String({ description: "Task ID" }),
3308
+ taskId: Type.String({ description: "Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title" }),
3309
+ full: Type.Optional(Type.Boolean({ description: "If true, include the task description + statusLog. Default: compact identity only." })),
3283
3310
  }),
3284
3311
  async execute(_id, params, _signal, _onUpdate, ctx) {
3285
3312
  const st = await requirePlan(ctx);
3286
3313
  if (!st)
3287
3314
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
3288
- const phases = await st.loadAllPhases();
3289
- const phase = phases.find((p) => p.tasks.some((t) => t.id === params.taskId));
3290
- const task = phase?.tasks.find((t) => t.id === params.taskId);
3291
- if (!task || !phase)
3315
+ const features = (await st.loadFeatures()).features;
3316
+ const found = findTaskByRef(await st.loadAllPhases(), features, params.taskId.trim());
3317
+ if (!found)
3292
3318
  return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], details: {} };
3293
- return { content: [{ type: "text", text: `${statusIcon(task.status)} ${task.title} (${task.id}) — phase ${phase.id}` }], details: { task, phaseId: phase.id } };
3319
+ 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})`;
3320
+ if (!params.full)
3321
+ return { content: [{ type: "text", text: summary }], details: {} };
3322
+ const log = (found.task.statusLog ?? []).map((e) => ` - ${e.date.slice(0, 10)} ${e.title}`).join("\n");
3323
+ return { content: [{ type: "text", text: `${summary}\n\n${found.task.description || ""}${log ? `\n\nStatus log:\n${log}` : ""}` }], details: {} };
3294
3324
  },
3295
3325
  });
3296
3326
  pi.registerTool({
@@ -3323,7 +3353,7 @@ export default function planPiExtension(pi) {
3323
3353
  const initialStatus = params.status ?? "planned";
3324
3354
  const existingPhase = await st.loadPhase(params.phaseId);
3325
3355
  const task = {
3326
- id: taskId, phaseId: params.phaseId, number: existingPhase.tasks.length + 1, shortId, priority, shortName,
3356
+ id: taskId, phaseId: params.phaseId, number: await st.allocTaskNumber(), shortId, priority, shortName,
3327
3357
  title: params.title,
3328
3358
  status: initialStatus,
3329
3359
  description: params.description ?? "",
@@ -3354,7 +3384,7 @@ export default function planPiExtension(pi) {
3354
3384
  label: "Task Update",
3355
3385
  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.",
3356
3386
  parameters: Type.Object({
3357
- taskId: Type.String({ description: "Task ID" }),
3387
+ taskId: Type.String({ description: "Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title" }),
3358
3388
  title: Type.Optional(Type.String({ description: "New title" })),
3359
3389
  status: Type.Optional(Type.String({ description: "New status: planned|in-progress|done|blocked|canceled|rejected|deferred|waiting" })),
3360
3390
  description: Type.Optional(Type.String({ description: "New description" })),
@@ -3442,7 +3472,7 @@ export default function planPiExtension(pi) {
3442
3472
  label: "Task Delete",
3443
3473
  description: "Delete a task from its phase.",
3444
3474
  parameters: Type.Object({
3445
- taskId: Type.String({ description: "Task ID to delete" }),
3475
+ taskId: Type.String({ description: "Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title to delete" }),
3446
3476
  }),
3447
3477
  async execute(_id, params, _signal, _onUpdate, ctx) {
3448
3478
  const st = await requirePlan(ctx);
@@ -3468,7 +3498,7 @@ export default function planPiExtension(pi) {
3468
3498
  label: "Task Start",
3469
3499
  description: "Set a task to in-progress. Use this BEFORE starting implementation work on any task. Sets startedAt timestamp automatically.",
3470
3500
  parameters: Type.Object({
3471
- taskId: Type.String({ description: "Task ID to start" }),
3501
+ taskId: Type.String({ description: "Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title to start" }),
3472
3502
  }),
3473
3503
  async execute(_id, params, _signal, _onUpdate, ctx) {
3474
3504
  const st = await requirePlan(ctx);
@@ -3506,7 +3536,7 @@ export default function planPiExtension(pi) {
3506
3536
  label: "Task Complete",
3507
3537
  description: "Mark a task as done. Sets completedAt and startedAt (if missing) automatically. Checks for unchecked checklist items and warns unless force=true.",
3508
3538
  parameters: Type.Object({
3509
- taskId: Type.String({ description: "Task ID to complete" }),
3539
+ taskId: Type.String({ description: "Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title to complete" }),
3510
3540
  force: Type.Optional(Type.Boolean({ description: "Skip checklist completion check. Default: false" })),
3511
3541
  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." })),
3512
3542
  }),
@@ -3646,6 +3676,7 @@ export default function planPiExtension(pi) {
3646
3676
  // add seconds of latency on every turn if run unconditionally.
3647
3677
  if (!plannerHeavyInitDone) {
3648
3678
  await migrateToUuids(st);
3679
+ await migrateToGlobalSequence(st).catch(() => null);
3649
3680
  await st.ensureShortIdsAndPriority().catch(() => null);
3650
3681
  await ensureProjectLanguagePreferences(st).catch(() => null);
3651
3682
  await maybeHealStatuses(st, ctx);
@@ -3788,7 +3819,7 @@ export default function planPiExtension(pi) {
3788
3819
  "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.",
3789
3820
  "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.",
3790
3821
  "5. NO SHORTCUTS: If a tool blocks you, follow the protocol. Bypasses are for emergencies only, not for convenience.",
3791
- "6. ENTITY REFERENCES: When you mention a feature/phase/task in chat, PREFER its 5-char shortId (globally unique, e.g. UUXD1). The composite T00x is per-phase and AMBIGUOUS unless fully qualified as T00x(P00x/F00x) — do NOT use bare 'T00x'. NEVER a raw UUID like bd6ed366. Tool responses already show shortId + compositeRef; reuse them.",
3822
+ "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.",
3792
3823
  "═══════════════════════════════════════════════════════════════",
3793
3824
  "",
3794
3825
  // ── Project details ──
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/pi-adapter",
3
- "version": "0.2.19-next.4",
3
+ "version": "0.2.19-next.6",
4
4
  "private": false,
5
5
  "description": "Pi agent extension for Agent Plan — structured project planning in .planner/",
6
6
  "keywords": [
@@ -28,8 +28,8 @@
28
28
  "dependencies": {
29
29
  "typebox": "^1.1.24",
30
30
  "zod": "^3.24.0",
31
- "@agent-plan/core": "0.2.19-next.4",
32
- "@agent-plan/server": "0.2.19-next.4"
31
+ "@agent-plan/core": "0.2.19-next.6",
32
+ "@agent-plan/server": "0.2.19-next.6"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@earendil-works/pi-coding-agent": "^0.74.0",