@agent-plan/pi-adapter 0.2.5 → 0.2.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;AA+xBtF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA8vF9D"}
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;AA00BtF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAqxF9D"}
package/dist/index.js CHANGED
@@ -165,6 +165,24 @@ async function maybeHealStatuses(st) {
165
165
  function normalizeLanguagePref(value) {
166
166
  return value?.trim() ?? "";
167
167
  }
168
+ // Safety net: ctx.ui.input() can hang forever when the extension is loaded from
169
+ // an NPM-installed package (the prompt never renders, the await never resolves,
170
+ // and Pi freezes). Race the prompt against a timeout; on timeout, fall back to the
171
+ // empty answer (treated as "no / this session only" by the gating logic) and notify
172
+ // the user how to enable manually. This preserves the interactive prompt for the
173
+ // working case while guaranteeing session_start can never block Pi permanently.
174
+ const PROMPT_TIMEOUT_MS = 20000;
175
+ async function safeInput(ctx, prompt) {
176
+ try {
177
+ return await Promise.race([
178
+ ctx.ui.input(prompt),
179
+ new Promise((resolve) => setTimeout(() => resolve("__PLANNER_PROMPT_TIMEOUT__"), PROMPT_TIMEOUT_MS)),
180
+ ]);
181
+ }
182
+ catch {
183
+ return "";
184
+ }
185
+ }
168
186
  async function ensureProjectLanguagePreferences(st) {
169
187
  const project = await st.loadProject();
170
188
  const contentLanguage = normalizeLanguagePref(project.contentLanguage);
@@ -429,6 +447,24 @@ async function buildHandoffMarkdown(st, reason, overrides) {
429
447
  : inferredRecentChanges.length > 0
430
448
  ? inferredRecentChanges.map((entry) => `- ${entry}`)
431
449
  : ["- No recent activity recorded"];
450
+ // Auto-derived: all task statuses for the current phase, so a resuming agent
451
+ // sees the full picture (not just the single in-progress task).
452
+ const currentPhaseTaskLines = currentPhase && currentPhase.tasks.length > 0
453
+ ? [...currentPhase.tasks]
454
+ .sort((a, b) => a.number - b.number || a.createdAt.localeCompare(b.createdAt))
455
+ .map((task) => `- ${statusIcon(task.status)} \`${task.id}\` — ${task.title} (${task.status})`)
456
+ : [];
457
+ // Caller-supplied rich context sections (locked design decisions,
458
+ // architecture, mode/state flows, plugin/API contracts, data mappings,
459
+ // known gaps, ...) injected between "What was being done" and "How to resume".
460
+ const extraSectionLines = (overrides?.extraSections ?? []).flatMap((section) => [
461
+ "",
462
+ `## ${section.heading}`,
463
+ section.body,
464
+ ]);
465
+ const autoCurrentTaskSection = currentPhaseTaskLines.length > 0
466
+ ? ["", `## Current Task Statuses (phase ${currentPhase.id})`, ...currentPhaseTaskLines]
467
+ : [];
432
468
  return [
433
469
  "# Handoff",
434
470
  "",
@@ -448,6 +484,8 @@ async function buildHandoffMarkdown(st, reason, overrides) {
448
484
  "",
449
485
  "## What was being done",
450
486
  whatWasBeingDone || "No additional execution notes were captured.",
487
+ ...extraSectionLines,
488
+ ...autoCurrentTaskSection,
451
489
  "",
452
490
  "## How to resume",
453
491
  howToResume,
@@ -791,7 +829,7 @@ export default function planPiExtension(pi) {
791
829
  }
792
830
  else {
793
831
  try {
794
- const ans = await ctx.ui.input("Planner detected in this project. Enable the planner extension? (y)es / (n)o / (a)lways / n(e)ver)");
832
+ const ans = await safeInput(ctx, "Planner detected in this project. Enable the planner extension? (y)es / (n)o / (a)lways / n(e)ver)");
795
833
  const normalized = ans?.trim().toLowerCase() ?? "";
796
834
  if (["a", "always", "sempre"].includes(normalized)) {
797
835
  enablePlanner = true;
@@ -865,7 +903,9 @@ export default function planPiExtension(pi) {
865
903
  await st.ensureStructureOrdering().catch(() => { });
866
904
  // Reconcile derived statuses once on startup: backfills any drift from
867
905
  // manual edits, pre-fix data, or tools that bypassed the rollup.
868
- await st.syncStatuses().catch(() => { });
906
+ // Uses maybeHealStatuses (idempotent via healedStatusRoots guard) so the
907
+ // later call below and before_agent_start don't re-run it (startup dedup).
908
+ await maybeHealStatuses(st).catch(() => { });
869
909
  // Decide whether to start the web UI based on persisted prefs or prompt.
870
910
  let startWeb = false;
871
911
  if (project?.plannerAutoStartWeb) {
@@ -876,7 +916,7 @@ export default function planPiExtension(pi) {
876
916
  }
877
917
  else if (server === null) {
878
918
  try {
879
- const ans = await ctx.ui.input("Start the planner web UI? (y)es / (n)o / (a)lways / n(e)ver)");
919
+ const ans = await safeInput(ctx, "Start the planner web UI? (y)es / (n)o / (a)lways / n(e)ver)");
880
920
  const normalized = ans?.trim().toLowerCase() ?? "";
881
921
  if (["a", "always", "sempre"].includes(normalized)) {
882
922
  startWeb = true;
@@ -917,13 +957,19 @@ export default function planPiExtension(pi) {
917
957
  const url = server?.url ?? "Starting server...";
918
958
  ctx.ui.notify(`Planner enabled. Dashboard: ${url}\nAnalyzing project state and preparing resume summary...`, "info");
919
959
  await ensureProjectLanguagePreferences(st).catch(() => null);
920
- await maybeHealStatuses(st).catch(() => { });
960
+ // (maybeHealStatuses already ran above — skipped here thanks to the guard.)
921
961
  // Background cleanup of orphan .bak/.tmp.* files (non-blocking)
922
962
  st.cleanupOrphanBackups().catch(() => { });
923
963
  const handoff = await st.loadHandoff().catch(() => null);
924
964
  if (handoff?.content) {
925
965
  ctx.ui.notify(`Handoff detected at .planner/HANDOFF.md (updated ${handoff.updatedAt}). It will be loaded automatically when the agent starts.`, "info");
926
966
  }
967
+ // Mark heavy init done so before_agent_start doesn't re-run migration,
968
+ // status healing, and language prefs on the first turn (startup dedup:
969
+ // all of these already ran in this session_start block). refreshResume is
970
+ // safe to skip here because the per-turn context builder falls back to
971
+ // loadResume() ?? refreshResume() when building the system prompt.
972
+ plannerHeavyInitDone = true;
927
973
  pi.sendMessage({
928
974
  customType: "planner-resume-trigger",
929
975
  content: "Emit the mandatory planner startup resume summary now.",
@@ -2748,11 +2794,15 @@ export default function planPiExtension(pi) {
2748
2794
  pi.registerTool({
2749
2795
  name: "plan_write_handoff",
2750
2796
  label: "Plan Write Handoff",
2751
- description: "Write or refresh the canonical .planner/HANDOFF.md document.",
2797
+ description: "Write or refresh the canonical .planner/HANDOFF.md document. Capture full design context, not just operational resume steps: pass extraSections with locked decisions, architecture (with file:line refs), mode/state flows, plugin/API contracts, data mappings, files touched, and known gaps so a resuming agent doesn't re-discover decisions already made.",
2752
2798
  parameters: Type.Object({
2753
2799
  reason: Type.Optional(Type.String({ description: "Why the handoff is being written" })),
2754
2800
  whatWasBeingDone: Type.Optional(Type.String({ description: "Optional override for the current work summary" })),
2755
2801
  howToResume: Type.Optional(Type.String({ description: "Optional override for resume instructions" })),
2802
+ extraSections: Type.Optional(Type.Array(Type.Object({
2803
+ heading: Type.String({ description: "Section heading (without the leading ## markers)" }),
2804
+ body: Type.String({ description: "Markdown body of the section. Use file:line refs, IDs, and concrete mappings." }),
2805
+ }), { description: "Rich context sections injected between 'What was being done' and 'How to resume'. Use for: Locked Design Decisions ({id,title,decision,rationale}), Architecture (new modules with file:line), Mode/State Flow (textual diagrams), Plugin/API Contracts, Data Mappings, Files Touched (new/modified/planner), Known Gaps. Maximize the design context a resuming agent needs." })),
2756
2806
  }),
2757
2807
  async execute(_id, params, _signal, _onUpdate, ctx) {
2758
2808
  const st = await requirePlan(ctx);
@@ -2761,6 +2811,13 @@ export default function planPiExtension(pi) {
2761
2811
  await writeProjectHandoff(st, params.reason?.trim() || "manual tool handoff", {
2762
2812
  ...(params.whatWasBeingDone?.trim() ? { whatWasBeingDone: params.whatWasBeingDone.trim() } : {}),
2763
2813
  ...(params.howToResume?.trim() ? { howToResume: params.howToResume.trim() } : {}),
2814
+ ...(params.extraSections && params.extraSections.length > 0
2815
+ ? {
2816
+ extraSections: params.extraSections
2817
+ .map((section) => ({ heading: section.heading?.trim() ?? "", body: section.body ?? "" }))
2818
+ .filter((section) => section.heading),
2819
+ }
2820
+ : {}),
2764
2821
  });
2765
2822
  const handoff = await st.loadHandoff();
2766
2823
  return { content: [{ type: "text", text: "Wrote .planner/HANDOFF.md" }], details: handoff ?? {} };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/pi-adapter",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "private": false,
5
5
  "description": "Pi agent extension for Agent Plan — structured project planning in .planner/",
6
6
  "keywords": [
@@ -25,11 +25,6 @@
25
25
  "."
26
26
  ]
27
27
  },
28
- "scripts": {
29
- "build": "tsc -p tsconfig.json && rm -f dist/.tsbuildinfo",
30
- "build:full": "pnpm build && ../../scripts/copy-web-ui.sh",
31
- "check": "tsc -p tsconfig.json --noEmit"
32
- },
33
28
  "dependencies": {
34
29
  "@agent-plan/core": "^0.2.0",
35
30
  "@agent-plan/server": "^0.2.0",
@@ -39,5 +34,10 @@
39
34
  "devDependencies": {
40
35
  "@earendil-works/pi-coding-agent": "^0.74.0",
41
36
  "@earendil-works/pi-ai": "^0.74.0"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc -p tsconfig.json && rm -f dist/.tsbuildinfo",
40
+ "build:full": "pnpm build && ../../scripts/copy-web-ui.sh",
41
+ "check": "tsc -p tsconfig.json --noEmit"
42
42
  }
43
- }
43
+ }