@agent-plan/pi-adapter 0.2.19-next.5 → 0.2.19-next.7

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,CAk7F9D"}
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,CAw9F9D"}
package/dist/index.js CHANGED
@@ -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) {
@@ -1379,7 +1382,7 @@ export default function planPiExtension(pi) {
1379
1382
  return doc;
1380
1383
  });
1381
1384
  await st.writeGenerated();
1382
- 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");
1383
1386
  return;
1384
1387
  }
1385
1388
  if (b === "show") {
@@ -1460,7 +1463,7 @@ export default function planPiExtension(pi) {
1460
1463
  });
1461
1464
  feature = updatedDoc.features.find((entry) => entry.id === featureId) ?? feature;
1462
1465
  await st.writeGenerated();
1463
- 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");
1464
1467
  return;
1465
1468
  }
1466
1469
  if (b === "delete") {
@@ -2605,7 +2608,7 @@ export default function planPiExtension(pi) {
2605
2608
  pi.registerTool({
2606
2609
  name: "plan_repair",
2607
2610
  label: "Plan Repair",
2608
- 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/.",
2609
2612
  parameters: Type.Object({}),
2610
2613
  async execute(_id, _params, _signal, _onUpdate, ctx) {
2611
2614
  const st = await requirePlan(ctx);
@@ -2613,9 +2616,11 @@ export default function planPiExtension(pi) {
2613
2616
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
2614
2617
  const report = await st.repair();
2615
2618
  const m = report.migrated;
2619
+ const c = report.containment;
2616
2620
  const lines = [
2617
2621
  `Repair complete.`,
2618
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.`,
2619
2624
  `Integrity: duplicatePhaseIds=${report.integrity.duplicatePhaseIds.length}, danglingPhaseIds=${report.integrity.danglingPhaseIds.length}.`,
2620
2625
  ];
2621
2626
  if (report.integrity.danglingPhaseIds.length)
@@ -2823,38 +2828,44 @@ export default function planPiExtension(pi) {
2823
2828
  pi.registerTool({
2824
2829
  name: "feature_list",
2825
2830
  label: "Feature List",
2826
- description: "List all features with their ids, statuses, and phase counts.",
2827
- parameters: Type.Object({}),
2828
- 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) {
2829
2834
  const st = await requirePlan(ctx);
2830
2835
  if (!st)
2831
2836
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
2832
- 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);
2833
2841
  const phases = await st.loadAllPhases();
2834
2842
  const summary = features.map((f) => {
2835
2843
  const fp = phases.filter((p) => p.featureId === f.id);
2836
- 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)`;
2837
2845
  }).join("\n");
2838
- return { content: [{ type: "text", text: summary || "No features" }], details: { features } };
2846
+ return { content: [{ type: "text", text: summary || "No features" }], details: {} };
2839
2847
  },
2840
2848
  });
2841
2849
  pi.registerTool({
2842
2850
  name: "feature_get",
2843
2851
  label: "Feature Get",
2844
- 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.",
2845
2853
  parameters: Type.Object({
2846
- 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." })),
2847
2856
  }),
2848
2857
  async execute(_id, params, _signal, _onUpdate, ctx) {
2849
2858
  const st = await requirePlan(ctx);
2850
2859
  if (!st)
2851
2860
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
2852
2861
  const features = (await st.loadFeatures()).features;
2853
- 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());
2854
2864
  if (!feature)
2855
- return { content: [{ type: "text", text: `Feature not found: ${params.featureId}` }], details: {} };
2865
+ return { content: [{ type: "text", text: `Feature not found: ${ref}` }], details: {} };
2856
2866
  const phases = (await st.loadAllPhases()).filter((p) => p.featureId === feature.id);
2857
- 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: {} };
2858
2869
  },
2859
2870
  });
2860
2871
  pi.registerTool({
@@ -3020,40 +3031,52 @@ export default function planPiExtension(pi) {
3020
3031
  pi.registerTool({
3021
3032
  name: "phase_list",
3022
3033
  label: "Phase List",
3023
- 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.",
3024
3035
  parameters: Type.Object({
3025
- 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" })),
3026
3038
  }),
3027
3039
  async execute(_id, params, _signal, _onUpdate, ctx) {
3028
3040
  const st = await requirePlan(ctx);
3029
3041
  if (!st)
3030
3042
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
3043
+ const features = (await st.loadFeatures()).features;
3031
3044
  let phases = await st.loadAllPhases();
3032
- if (params.featureId)
3033
- phases = phases.filter((p) => p.featureId === params.featureId);
3034
- const summary = phases.map((p) => `- ${statusIcon(p.status)} ${p.id} — ${p.title} (${p.tasks.length} tasks, feature ${p.featureId ?? "none"})`).join("\n");
3035
- 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: {} };
3036
3060
  },
3037
3061
  });
3038
3062
  pi.registerTool({
3039
3063
  name: "phase_get",
3040
3064
  label: "Phase Get",
3041
- 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.",
3042
3066
  parameters: Type.Object({
3043
- 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." })),
3044
3069
  }),
3045
3070
  async execute(_id, params, _signal, _onUpdate, ctx) {
3046
3071
  const st = await requirePlan(ctx);
3047
3072
  if (!st)
3048
3073
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
3049
- let phase;
3050
- try {
3051
- phase = await st.loadPhase(params.phaseId);
3052
- }
3053
- catch {
3074
+ const features = (await st.loadFeatures()).features;
3075
+ const phase = findPhaseByRef(await st.loadAllPhases(), features, params.phaseId.trim());
3076
+ if (!phase)
3054
3077
  return { content: [{ type: "text", text: `Phase not found: ${params.phaseId}` }], details: {} };
3055
- }
3056
- 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: {} };
3057
3080
  },
3058
3081
  });
3059
3082
  pi.registerTool({
@@ -3242,42 +3265,64 @@ export default function planPiExtension(pi) {
3242
3265
  pi.registerTool({
3243
3266
  name: "task_list",
3244
3267
  label: "Task List",
3245
- 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.",
3246
3269
  parameters: Type.Object({
3247
- phaseId: Type.String({ description: "Phase ref: F00x/P00x, bare P00x (global), shortId, UUID, or title" }),
3270
+ featureRef: Type.Optional(Type.String({})),
3271
+ phaseRef: Type.Optional(Type.String({})),
3272
+ status: Type.Optional(Type.String({ description: "Optional: filter by status name" })),
3248
3273
  }),
3249
3274
  async execute(_id, params, _signal, _onUpdate, ctx) {
3250
3275
  const st = await requirePlan(ctx);
3251
3276
  if (!st)
3252
3277
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
3253
- let phase;
3254
- try {
3255
- phase = await st.loadPhase(params.phaseId);
3256
- }
3257
- catch {
3258
- return { content: [{ type: "text", text: `Phase not found: ${params.phaseId}` }], details: {} };
3259
- }
3260
- const summary = phase.tasks.map((t) => `- ${statusIcon(t.status)} ${t.id} — ${t.title}`).join("\n");
3261
- return { content: [{ type: "text", text: summary || "No tasks" }], details: { tasks: phase.tasks } };
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
+ }
3301
+ }
3302
+ return { content: [{ type: "text", text: out.join("\n") || "No tasks" }], details: {} };
3262
3303
  },
3263
3304
  });
3264
3305
  pi.registerTool({
3265
3306
  name: "task_get",
3266
3307
  label: "Task Get",
3267
- 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.",
3268
3309
  parameters: Type.Object({
3269
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." })),
3270
3312
  }),
3271
3313
  async execute(_id, params, _signal, _onUpdate, ctx) {
3272
3314
  const st = await requirePlan(ctx);
3273
3315
  if (!st)
3274
3316
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
3275
- const phases = await st.loadAllPhases();
3276
- const phase = phases.find((p) => p.tasks.some((t) => t.id === params.taskId));
3277
- const task = phase?.tasks.find((t) => t.id === params.taskId);
3278
- if (!task || !phase)
3317
+ const features = (await st.loadFeatures()).features;
3318
+ const found = findTaskByRef(await st.loadAllPhases(), features, params.taskId.trim());
3319
+ if (!found)
3279
3320
  return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], details: {} };
3280
- 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: {} };
3281
3326
  },
3282
3327
  });
3283
3328
  pi.registerTool({
@@ -3776,7 +3821,7 @@ export default function planPiExtension(pi) {
3776
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.",
3777
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.",
3778
3823
  "5. NO SHORTCUTS: If a tool blocks you, follow the protocol. Bypasses are for emergencies only, not for convenience.",
3779
- "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.",
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.",
3780
3825
  "═══════════════════════════════════════════════════════════════",
3781
3826
  "",
3782
3827
  // ── 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.5",
3
+ "version": "0.2.19-next.7",
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.5",
32
- "@agent-plan/server": "0.2.19-next.5"
31
+ "@agent-plan/core": "0.2.19-next.7",
32
+ "@agent-plan/server": "0.2.19-next.7"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@earendil-works/pi-coding-agent": "^0.74.0",