@agent-plan/pi-adapter 0.2.7 → 0.2.9

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;AA00BtF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAqxF9D"}
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;AA4yBtF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAmrF9D"}
package/dist/index.js CHANGED
@@ -111,8 +111,8 @@ const PLANNER_COMMAND_COMPLETIONS = [
111
111
  { value: "export-full", label: "export-full", description: "Export full detailed plan as Markdown" },
112
112
  { value: "bypass", label: "bypass", description: "Authorize edit/write without a task in-progress (15 min)" },
113
113
  { value: "clear-bypass", label: "clear-bypass", description: "Revoke the guard bypass" },
114
- { value: "load", label: "load", description: "Re-enable planner and start web UI" },
115
- { value: "disable", label: "disable", description: "Reset planner preferences and disable for this session" },
114
+ { value: "load", label: "load", description: "Enable planner + start web UI (LAN) and show resume summary" },
115
+ { value: "stop", label: "stop", description: "Disable planner and shut down the web UI" },
116
116
  ];
117
117
  // ─── Helpers ────────────────────────────────────────────────────────────
118
118
  function nowISO() {
@@ -165,24 +165,6 @@ 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
- }
186
168
  async function ensureProjectLanguagePreferences(st) {
187
169
  const project = await st.loadProject();
188
170
  const contentLanguage = normalizeLanguagePref(project.contentLanguage);
@@ -661,16 +643,14 @@ async function maybeStartWeb(ctx) {
661
643
  plannerSessionEnabled = true;
662
644
  return;
663
645
  }
664
- const ans = await ctx.ui.input("Planner active for this session. Start the web UI now? (y/N)");
665
- if (ans && /^(y|yes|s|si|sì)$/i.test(ans.trim())) {
666
- ctx.ui.notify("Starting web server…", "info");
667
- try {
668
- await startServer(ctx);
669
- ctx.ui.notify(`Web UI ready. Open: ${server?.url ?? "?"}`, "info");
670
- }
671
- catch (err) {
672
- ctx.ui.notify(`Failed to start web server: ${String(err)}`, "error");
673
- }
646
+ ctx.ui.notify("Starting web server (LAN)…", "info");
647
+ try {
648
+ await startServer(ctx, undefined, "lan");
649
+ const srv = server;
650
+ ctx.ui.notify(srv?.lanUrl ? `Web UI ready. Local: ${srv.localUrl} — LAN: ${srv.lanUrl}` : `Web UI ready. Open: ${srv?.url ?? "?"}`, "info");
651
+ }
652
+ catch (err) {
653
+ ctx.ui.notify(`Failed to start web server: ${String(err)}`, "error");
674
654
  }
675
655
  }
676
656
  function normalizeVisibility(input) {
@@ -683,17 +663,7 @@ function normalizeVisibility(input) {
683
663
  return "local";
684
664
  return undefined;
685
665
  }
686
- async function promptWebVisibility(ctx) {
687
- try {
688
- const ans = await ctx.ui.input("Web UI visibility: (local) only this machine / (lan) visible on the local network? [local/lan]");
689
- const v = normalizeVisibility(ans);
690
- if (v)
691
- return v;
692
- }
693
- catch { }
694
- return "local";
695
- }
696
- async function startServer(ctx, requestedPort, visibility = "local") {
666
+ async function startServer(ctx, requestedPort, visibility = "lan") {
697
667
  plannerSessionEnabled = true;
698
668
  if (server)
699
669
  return;
@@ -819,61 +789,14 @@ export default function planPiExtension(pi) {
819
789
  const exists = await st.exists().catch(() => false);
820
790
  let project = exists ? await st.loadProject().catch(() => null) : null;
821
791
  // ── Step 1: Enable gating ───────────────────────────────────────
792
+ // No blocking prompt in session_start: ctx.ui.input()/select() do not
793
+ // render for NPM-loaded ESM extensions during session_start (they hang
794
+ // until the safety-net timeout). The planner is always DISABLED at
795
+ // startup; the user enables it with '/planner load' (which also starts
796
+ // the web UI on LAN and triggers the resume summary showing the URL).
822
797
  let enablePlanner = false;
823
798
  if (exists) {
824
- if (project?.plannerAutoEnable) {
825
- enablePlanner = true; // 'always' persisted — no prompt
826
- }
827
- else if (project?.plannerNeverAsk) {
828
- enablePlanner = false; // 'never' persisted — no prompt, no activation
829
- }
830
- else {
831
- try {
832
- const ans = await safeInput(ctx, "Planner detected in this project. Enable the planner extension? (y)es / (n)o / (a)lways / n(e)ver)");
833
- const normalized = ans?.trim().toLowerCase() ?? "";
834
- if (["a", "always", "sempre"].includes(normalized)) {
835
- enablePlanner = true;
836
- if (project) {
837
- project.plannerAutoEnable = true;
838
- project.plannerNeverAsk = false;
839
- await st.saveProject(project);
840
- }
841
- ctx.ui.notify("Saved: planner will auto-enable for this project.", "info");
842
- }
843
- else if (["e", "never", "mai"].includes(normalized)) {
844
- enablePlanner = false;
845
- if (project) {
846
- project.plannerNeverAsk = true;
847
- project.plannerAutoEnable = false;
848
- await st.saveProject(project);
849
- }
850
- try {
851
- capturedPi?.appendEntry("plan-web-state", { running: false });
852
- }
853
- catch { }
854
- ctx.ui.notify("Saved: planner will not ask again for this project. Use '/planner load' to re-enable manually.", "info");
855
- }
856
- else if (/^(y|yes|s|si|sì)$/i.test(normalized)) {
857
- enablePlanner = true; // this session only
858
- }
859
- else {
860
- // 'n' / 'no' / empty — this session only, NOT persisted
861
- enablePlanner = false;
862
- try {
863
- capturedPi?.appendEntry("plan-web-state", { running: false });
864
- }
865
- catch { }
866
- ctx.ui.notify("Planner disabled for this session. Use '/planner load' to enable manually.", "info");
867
- }
868
- }
869
- catch {
870
- enablePlanner = false;
871
- try {
872
- capturedPi?.appendEntry("plan-web-state", { running: false });
873
- }
874
- catch { }
875
- }
876
- }
799
+ ctx.ui.notify("Planner detected in this project. Run '/planner load' to enable the planner and start the web UI (LAN).", "info");
877
800
  }
878
801
  plannerSessionEnabled = enablePlanner;
879
802
  // Proactive check for leftover handoff files.
@@ -889,95 +812,6 @@ export default function planPiExtension(pi) {
889
812
  startupResumeSummaryPending = false;
890
813
  return;
891
814
  }
892
- // ── Step 2: Web UI gating (only if planner enabled) ─────────────
893
- startupResumePromptPending = exists && plannerSessionEnabled;
894
- startupResumeSummaryPending = exists && plannerSessionEnabled;
895
- if (exists && plannerSessionEnabled) {
896
- // Migration first.
897
- try {
898
- await migrateToUuids(st);
899
- }
900
- catch (e) {
901
- ctx.ui.notify(`Migration failed: ${e instanceof Error ? e.message : String(e)}`, "error");
902
- }
903
- await st.ensureStructureOrdering().catch(() => { });
904
- // Reconcile derived statuses once on startup: backfills any drift from
905
- // manual edits, pre-fix data, or tools that bypassed the rollup.
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(() => { });
909
- // Decide whether to start the web UI based on persisted prefs or prompt.
910
- let startWeb = false;
911
- if (project?.plannerAutoStartWeb) {
912
- startWeb = true; // 'always' persisted
913
- }
914
- else if (project?.plannerNeverStartWeb) {
915
- startWeb = false; // 'never' persisted
916
- }
917
- else if (server === null) {
918
- try {
919
- const ans = await safeInput(ctx, "Start the planner web UI? (y)es / (n)o / (a)lways / n(e)ver)");
920
- const normalized = ans?.trim().toLowerCase() ?? "";
921
- if (["a", "always", "sempre"].includes(normalized)) {
922
- startWeb = true;
923
- if (project) {
924
- project.plannerAutoStartWeb = true;
925
- project.plannerNeverStartWeb = false;
926
- await st.saveProject(project);
927
- }
928
- }
929
- else if (["e", "never", "mai"].includes(normalized)) {
930
- startWeb = false;
931
- if (project) {
932
- project.plannerNeverStartWeb = true;
933
- project.plannerAutoStartWeb = false;
934
- await st.saveProject(project);
935
- }
936
- }
937
- else if (/^(y|yes|s|si|sì)$/i.test(normalized)) {
938
- startWeb = true;
939
- }
940
- else {
941
- startWeb = false; // 'n' / 'no' / empty — this session only
942
- }
943
- }
944
- catch {
945
- startWeb = false;
946
- }
947
- }
948
- if (startWeb && server === null) {
949
- const visibility = await promptWebVisibility(ctx);
950
- await startServer(ctx, preferredPort, visibility).catch(() => { });
951
- const srv = server;
952
- if (srv) {
953
- const urls = srv.lanUrl ? `${srv.localUrl} (LAN: ${srv.lanUrl})` : srv.url;
954
- ctx.ui.notify(`Web UI started at ${urls}`, "info");
955
- }
956
- }
957
- const url = server?.url ?? "Starting server...";
958
- ctx.ui.notify(`Planner enabled. Dashboard: ${url}\nAnalyzing project state and preparing resume summary...`, "info");
959
- await ensureProjectLanguagePreferences(st).catch(() => null);
960
- // (maybeHealStatuses already ran above — skipped here thanks to the guard.)
961
- // Background cleanup of orphan .bak/.tmp.* files (non-blocking)
962
- st.cleanupOrphanBackups().catch(() => { });
963
- const handoff = await st.loadHandoff().catch(() => null);
964
- if (handoff?.content) {
965
- ctx.ui.notify(`Handoff detected at .planner/HANDOFF.md (updated ${handoff.updatedAt}). It will be loaded automatically when the agent starts.`, "info");
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;
973
- pi.sendMessage({
974
- customType: "planner-resume-trigger",
975
- content: "Emit the mandatory planner startup resume summary now.",
976
- display: false,
977
- }, {
978
- triggerTurn: true,
979
- });
980
- }
981
815
  }
982
816
  catch (e) {
983
817
  // Defensive: never let session_start throw crash Pi silently.
@@ -1158,9 +992,9 @@ export default function planPiExtension(pi) {
1158
992
  "bypass",
1159
993
  "clear-bypass",
1160
994
  "load",
1161
- "disable",
995
+ "stop",
1162
996
  ];
1163
- const SUB_HELP = "Available: init, show, repair, project, feature, phase, task, discuss, handoff, web, export, export-full, bypass, clear-bypass, load, disable\n" +
997
+ const SUB_HELP = "Available: init, show, repair, project, feature, phase, task, discuss, handoff, web, export, export-full, bypass, clear-bypass, load, stop\n" +
1164
998
  "Try: /planner <TAB> | /planner feature list | /planner task start | /planner export-full\n" +
1165
999
  "Handoff actions: /planner handoff prepare | show | write | clear";
1166
1000
  if (!a) {
@@ -2345,7 +2179,7 @@ export default function planPiExtension(pi) {
2345
2179
  }
2346
2180
  const visibilityArg = normalizeVisibility(parts[2]);
2347
2181
  const portArg = parts[2]?.trim() && Number.isFinite(parseInt(parts[2], 10)) ? parseInt(parts[2], 10) : undefined;
2348
- const visibility = visibilityArg ?? await promptWebVisibility(ctx);
2182
+ const visibility = visibilityArg ?? "lan";
2349
2183
  ctx.ui.notify(`Starting web server (${visibility})${portArg ? ` on port ${portArg}` : ""} …`, "info");
2350
2184
  await startServer(ctx, portArg, visibility);
2351
2185
  const srv = server;
@@ -2418,46 +2252,34 @@ export default function planPiExtension(pi) {
2418
2252
  return;
2419
2253
  }
2420
2254
  if (a === "load") {
2421
- const project = await st.loadProject().catch(() => null);
2422
- if (project) {
2423
- let changed = false;
2424
- if (project.plannerNeverAsk) {
2425
- project.plannerNeverAsk = false;
2426
- changed = true;
2427
- }
2428
- if (project.plannerNeverStartWeb) {
2429
- project.plannerNeverStartWeb = false;
2430
- changed = true;
2431
- }
2432
- if (changed) {
2433
- await st.saveProject(project);
2434
- ctx.ui.notify("Cleared 'never' flags. Planner enabled for this session.", "info");
2435
- }
2436
- }
2437
2255
  plannerSessionEnabled = true;
2438
2256
  if (!server) {
2439
- ctx.ui.notify("Starting web server …", "info");
2440
- await startServer(ctx);
2441
- }
2442
- ctx.ui.notify(`Planner loaded. Web UI: ${server?.url ?? "(not started)"}`, "info");
2257
+ ctx.ui.notify("Starting web server (LAN) …", "info");
2258
+ await startServer(ctx, undefined, "lan").catch(() => { });
2259
+ }
2260
+ const srv = server;
2261
+ ctx.ui.notify(srv?.lanUrl ? `Planner loaded. Web UI: ${srv.localUrl} (LAN: ${srv.lanUrl})` : `Planner loaded. Web UI: ${srv?.url ?? "(not started)"}`, "info");
2262
+ // Trigger the mandatory startup resume summary so the agent surfaces the
2263
+ // web UI address (and project state) immediately after loading.
2264
+ startupResumePromptPending = true;
2265
+ startupResumeSummaryPending = true;
2266
+ pi.sendMessage({
2267
+ customType: "planner-resume-trigger",
2268
+ content: "Emit the mandatory planner startup resume summary now (include the web UI address and port).",
2269
+ display: false,
2270
+ }, {
2271
+ triggerTurn: true,
2272
+ });
2443
2273
  return;
2444
2274
  }
2445
- if (a === "disable") {
2446
- const project = await st.loadProject().catch(() => null);
2447
- if (project) {
2448
- project.plannerAutoEnable = false;
2449
- project.plannerNeverAsk = false;
2450
- project.plannerAutoStartWeb = false;
2451
- project.plannerNeverStartWeb = false;
2452
- await st.saveProject(project);
2453
- }
2275
+ if (a === "stop" || a === "disable") {
2454
2276
  plannerSessionEnabled = false;
2455
2277
  await stopServer().catch(() => { });
2456
2278
  try {
2457
2279
  capturedPi?.appendEntry("plan-web-state", { running: false });
2458
2280
  }
2459
2281
  catch { }
2460
- ctx.ui.notify("Planner preferences reset to 'ask'. Disabled for this session. Next restart will prompt again.", "info");
2282
+ ctx.ui.notify("Planner stopped. Web UI shut down. Run '/planner load' to re-enable.", "info");
2461
2283
  return;
2462
2284
  }
2463
2285
  ctx.ui.notify(`Unknown "${a}". ${SUB_HELP}`, "warning");
@@ -3649,7 +3471,18 @@ export default function planPiExtension(pi) {
3649
3471
  : (handoff?.updatedAt
3650
3472
  ? "Read the handoff, then validate current plan ordering and dependencies before picking the next task."
3651
3473
  : "Review the current plan and choose the next task.");
3652
- const webUrl = server?.url ?? "";
3474
+ const webServer = server;
3475
+ const webLocalUrl = webServer?.localUrl ?? webServer?.url ?? "";
3476
+ const webLanUrl = webServer?.lanUrl ?? "";
3477
+ let webPort = "";
3478
+ try {
3479
+ const p = new URL(webLocalUrl).port;
3480
+ if (p)
3481
+ webPort = p;
3482
+ }
3483
+ catch { }
3484
+ const webUrl = webLocalUrl;
3485
+ const webUrlFull = webLanUrl ? `${webLocalUrl} (LAN: ${webLanUrl})` : webLocalUrl;
3653
3486
  const openQuestions = plan.phases
3654
3487
  .flatMap((p) => p.openQuestions.map((q) => `[${p.id}] ${q}`))
3655
3488
  .slice(0, 12);
@@ -3679,7 +3512,7 @@ export default function planPiExtension(pi) {
3679
3512
  currentPhase ? `- Mention current phase ONLY because it is actually active: ${currentPhase.id} — ${phaseLabel(currentPhase)} (${currentPhase.status}).` : "- Mention that no current phase is active.",
3680
3513
  currentTask ? `- Mention current task ONLY because it is actually active: ${currentTask.id} — ${taskLabel(currentTask)} (${currentTask.status}).` : "- Mention that no current task is active.",
3681
3514
  !hasActiveWork ? "- If a handoff exists, describe it only as a previous-session hint to validate against current planner state, ordering, and dependencies. Do NOT present it as the current focus." : "",
3682
- webUrl ? `- Mention the active web dashboard URL: ${webUrl}` : "- Explicitly say whether the web dashboard is active in this session.",
3515
+ webUrl ? `- At the END of your summary, you MUST print a dedicated line with the web dashboard address and port, exactly: '🌐 Web UI: ${webLocalUrl}${webPort ? ` (port ${webPort})` : ""}${webLanUrl ? ` — LAN: ${webLanUrl}` : ""}'. This line is mandatory; do not omit it.` : "- Explicitly tell the user whether the web dashboard is active in this session (and how to start it with '/planner web start' if not).",
3683
3516
  `- Mention the next suggested activity: ${nextActivity}`,
3684
3517
  "- THEN ask the user explicitly whether they want to resume that activity now.",
3685
3518
  "- Do NOT assume yes. Wait for the user's answer before continuing implementation work.",
@@ -3688,6 +3521,9 @@ export default function planPiExtension(pi) {
3688
3521
  // Build/refresh the context block (slow path: cache miss).
3689
3522
  const contextBlock = [
3690
3523
  `[Plan Context — ${project.name}]`,
3524
+ // ── Web UI status (prominent — agent must surface URL in its summary) ──
3525
+ webUrl ? `🌐 WEB UI RUNNING: ${webUrlFull}${webPort ? ` (port ${webPort})` : ""}. Include this address+port at the end of your resume summary.` : `Web UI: not running. Start it with '/planner web start'.`,
3526
+ "",
3691
3527
  // ── MANDATORY OPERATIONAL PROTOCOL (top priority, non-negotiable) ──
3692
3528
  "",
3693
3529
  "═══════════════════════════════════════════════════════════════",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/pi-adapter",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "private": false,
5
5
  "description": "Pi agent extension for Agent Plan — structured project planning in .planner/",
6
6
  "keywords": [