pi-orchestrator-config 4.4.0 → 4.5.0

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.
Files changed (133) hide show
  1. package/README.md +44 -2
  2. package/extensions/coms/coms-p2p.ts +51 -39
  3. package/extensions/coms/coms-shared.ts +7 -5
  4. package/extensions/openai-compatible-discovery/index.ts +29 -1
  5. package/extensions/orchestrator/agents.ts +9 -1
  6. package/extensions/orchestrator/async-agents.ts +3 -1
  7. package/extensions/orchestrator/graft.ts +306 -0
  8. package/extensions/orchestrator/index.ts +2 -0
  9. package/extensions/orchestrator/memory-tools.ts +1 -1
  10. package/extensions/orchestrator/project-settings.ts +15 -29
  11. package/extensions/orchestrator/rules.ts +4 -1
  12. package/extensions/orchestrator/session-validation.ts +45 -24
  13. package/extensions/orchestrator/settings-source.ts +139 -0
  14. package/extensions/orchestrator/settings-tui-helpers.ts +2 -3
  15. package/extensions/orchestrator/settings-tui.ts +1 -3
  16. package/extensions/orchestrator/status-bar.ts +1 -0
  17. package/extensions/orchestrator/subagent-tool.ts +3 -2
  18. package/extensions/orchestrator/utils.ts +1 -1
  19. package/extensions/pidash/event-projection.ts +19 -0
  20. package/extensions/pidash/model-info.ts +7 -0
  21. package/extensions/pidash/pidash-ui/dist/assets/{index-Ehfwjr9d.js → index-CI6UL4g4.js} +6 -6
  22. package/extensions/pidash/pidash-ui/dist/assets/index-hM6gC5Yg.css +2 -0
  23. package/extensions/pidash/pidash-ui/dist/index.html +2 -2
  24. package/extensions/pidash/pidash-ui/src/App.tsx +2 -1
  25. package/extensions/pidash/pidash-ui/src/components/InfoBar.tsx +79 -15
  26. package/extensions/pidash/pidash-ui/src/lib/format-total.ts +16 -0
  27. package/extensions/pidash/pidash-ui/src/lib/model-capabilities.ts +11 -0
  28. package/extensions/pidash/pidash.ts +30 -7
  29. package/extensions/pidiff/pidiff-ui/dist/assets/{angular-html-ghA4VuM0.js → angular-html-Bfyy3ooe.js} +1 -1
  30. package/extensions/pidiff/pidiff-ui/dist/assets/{angular-ts-BiEwt-Qs.js → angular-ts-nVgE7bZq.js} +1 -1
  31. package/extensions/pidiff/pidiff-ui/dist/assets/{apl-DmYAGNOL.js → apl-tvfwqHpz.js} +1 -1
  32. package/extensions/pidiff/pidiff-ui/dist/assets/{astro-Bju7L81l.js → astro-l8TnD3AA.js} +1 -1
  33. package/extensions/pidiff/pidiff-ui/dist/assets/{blade-C_YtSRJN.js → blade-BdHBbmKC.js} +1 -1
  34. package/extensions/pidiff/pidiff-ui/dist/assets/{c-DxcP9pmN.js → c-D1CypT9z.js} +1 -1
  35. package/extensions/pidiff/pidiff-ui/dist/assets/{chapel-BjVM8zvB.js → chapel-D56VAXz2.js} +1 -1
  36. package/extensions/pidiff/pidiff-ui/dist/assets/{cobol-CyAifE-8.js → cobol-BwRRZSIY.js} +1 -1
  37. package/extensions/pidiff/pidiff-ui/dist/assets/{coffee-BNW4Zttb.js → coffee-Cet_1pvq.js} +1 -1
  38. package/extensions/pidiff/pidiff-ui/dist/assets/{cpp-De6s9lN-.js → cpp-CZ8C9S-h.js} +1 -1
  39. package/extensions/pidiff/pidiff-ui/dist/assets/{crystal-JzhjW33_.js → crystal-EOxgVG_1.js} +1 -1
  40. package/extensions/pidiff/pidiff-ui/dist/assets/{css-DAnI1EKT.js → css-lK_4bfX4.js} +1 -1
  41. package/extensions/pidiff/pidiff-ui/dist/assets/{edge-DlbkxEj4.js → edge-C7pTOYjM.js} +1 -1
  42. package/extensions/pidiff/pidiff-ui/dist/assets/{elixir-BXz6vpDX.js → elixir-D1OnpXXN.js} +1 -1
  43. package/extensions/pidiff/pidiff-ui/dist/assets/{elm-CJcSKM6_.js → elm-DPOsZNNm.js} +1 -1
  44. package/extensions/pidiff/pidiff-ui/dist/assets/{erb-VHOaTR5g.js → erb-D_E9Tmw3.js} +1 -1
  45. package/extensions/pidiff/pidiff-ui/dist/assets/{git-rebase-PAYwCVk-.js → git-rebase-FRK9CwsX.js} +1 -1
  46. package/extensions/pidiff/pidiff-ui/dist/assets/{glimmer-js-CspJJiU_.js → glimmer-js-BNQDK9N_.js} +1 -1
  47. package/extensions/pidiff/pidiff-ui/dist/assets/{glimmer-ts-CSMIai5o.js → glimmer-ts-TWnnEP-7.js} +1 -1
  48. package/extensions/pidiff/pidiff-ui/dist/assets/{glsl-Cw9yKL2B.js → glsl-C90MQFKT.js} +1 -1
  49. package/extensions/pidiff/pidiff-ui/dist/assets/{graphql-D0OMgGsM.js → graphql-hToq3IB7.js} +1 -1
  50. package/extensions/pidiff/pidiff-ui/dist/assets/{hack-DfiAACVA.js → hack-B9KIVyIF.js} +1 -1
  51. package/extensions/pidiff/pidiff-ui/dist/assets/{haml-DU8pAbRK.js → haml-CP3pGCyN.js} +1 -1
  52. package/extensions/pidiff/pidiff-ui/dist/assets/{handlebars-CoaiK3xT.js → handlebars-D1bq27yj.js} +1 -1
  53. package/extensions/pidiff/pidiff-ui/dist/assets/{html-B4C3aTRB.js → html-DDPkOyoL.js} +1 -1
  54. package/extensions/pidiff/pidiff-ui/dist/assets/{html-derivative-C_NJUsxF.js → html-derivative-BR4J_BCH.js} +1 -1
  55. package/extensions/pidiff/pidiff-ui/dist/assets/{http-Cr9w59E3.js → http-BmExgdIq.js} +1 -1
  56. package/extensions/pidiff/pidiff-ui/dist/assets/{hurl-BEScLvfg.js → hurl-UvOw6UjF.js} +1 -1
  57. package/extensions/pidiff/pidiff-ui/dist/assets/index-Bo8LhGN_.js +2796 -0
  58. package/extensions/pidiff/pidiff-ui/dist/assets/{java-hb-3TTCg.js → java-CFjffq9z.js} +1 -1
  59. package/extensions/pidiff/pidiff-ui/dist/assets/{javascript-Bc0DeQtz.js → javascript-D6jF2sRD.js} +1 -1
  60. package/extensions/pidiff/pidiff-ui/dist/assets/{jinja-DoEIYL2y.js → jinja-_MAtAn3c.js} +1 -1
  61. package/extensions/pidiff/pidiff-ui/dist/assets/{jison-C2OZYR2e.js → jison-CzjG9woH.js} +1 -1
  62. package/extensions/pidiff/pidiff-ui/dist/assets/{json-DKeabECb.js → json-Cq0awPRn.js} +1 -1
  63. package/extensions/pidiff/pidiff-ui/dist/assets/{jsx-Dadu-wAB.js → jsx-iffPQUDY.js} +1 -1
  64. package/extensions/pidiff/pidiff-ui/dist/assets/{julia-CpUr0nh2.js → julia-BJh5g1V6.js} +1 -1
  65. package/extensions/pidiff/pidiff-ui/dist/assets/{just-BysHL69Z.js → just-BiplQU-x.js} +1 -1
  66. package/extensions/pidiff/pidiff-ui/dist/assets/{latex-iNrlK36R.js → latex-C9i8ER6l.js} +1 -1
  67. package/extensions/pidiff/pidiff-ui/dist/assets/{liquid-g8x_A5Ru.js → liquid-CJFKQVVC.js} +1 -1
  68. package/extensions/pidiff/pidiff-ui/dist/assets/{lua-B4BSWsUC.js → lua-CQPggDzM.js} +1 -1
  69. package/extensions/pidiff/pidiff-ui/dist/assets/{marko-CHSFXjEi.js → marko-d7_vCT_U.js} +1 -1
  70. package/extensions/pidiff/pidiff-ui/dist/assets/{mdc-js2C5rVG.js → mdc-5MqUEkF6.js} +1 -1
  71. package/extensions/pidiff/pidiff-ui/dist/assets/{nginx-BblTQvRl.js → nginx-BGYrJESG.js} +1 -1
  72. package/extensions/pidiff/pidiff-ui/dist/assets/{nim-ChxMAPPZ.js → nim-BT5d3bbB.js} +1 -1
  73. package/extensions/pidiff/pidiff-ui/dist/assets/{org-CHoj3SWZ.js → org-Dcsoek_G.js} +1 -1
  74. package/extensions/pidiff/pidiff-ui/dist/assets/{perl-3zusLAIc.js → perl-Dzum8ON5.js} +1 -1
  75. package/extensions/pidiff/pidiff-ui/dist/assets/{php-DVx3dbu3.js → php-DMd6jR7H.js} +1 -1
  76. package/extensions/pidiff/pidiff-ui/dist/assets/{pug-DITNjLsK.js → pug-zSUb8M7G.js} +1 -1
  77. package/extensions/pidiff/pidiff-ui/dist/assets/{qml-BYmD788c.js → qml-B-xKh7qQ.js} +1 -1
  78. package/extensions/pidiff/pidiff-ui/dist/assets/{r-Dp_qVPOX.js → r-Bjwskoo1.js} +1 -1
  79. package/extensions/pidiff/pidiff-ui/dist/assets/{razor-DT4CEDjF.js → razor-CXf_WKrJ.js} +1 -1
  80. package/extensions/pidiff/pidiff-ui/dist/assets/{regexp-DVFGbBOe.js → regexp-DT6fzMFy.js} +1 -1
  81. package/extensions/pidiff/pidiff-ui/dist/assets/{rst-njZM0E3u.js → rst-9nW3vkMx.js} +1 -1
  82. package/extensions/pidiff/pidiff-ui/dist/assets/{ruby-gu2hDDCx.js → ruby-qIjs3ahE.js} +1 -1
  83. package/extensions/pidiff/pidiff-ui/dist/assets/{sas-Bo6NwyH7.js → sas-C5jUuuyr.js} +1 -1
  84. package/extensions/pidiff/pidiff-ui/dist/assets/{scss-COdGMqPA.js → scss-DDLNCEB-.js} +1 -1
  85. package/extensions/pidiff/pidiff-ui/dist/assets/{shellscript-DSg7qNC1.js → shellscript-BhivxeP8.js} +1 -1
  86. package/extensions/pidiff/pidiff-ui/dist/assets/{shellsession-CD1RmUpx.js → shellsession-n_duFdfp.js} +1 -1
  87. package/extensions/pidiff/pidiff-ui/dist/assets/{soy-C0QWCEMl.js → soy-Dpt5nO7G.js} +1 -1
  88. package/extensions/pidiff/pidiff-ui/dist/assets/{sql-VoWQdTUw.js → sql-BtjLhqvp.js} +1 -1
  89. package/extensions/pidiff/pidiff-ui/dist/assets/{stata-BEj75h4q.js → stata-D4GbpMWg.js} +1 -1
  90. package/extensions/pidiff/pidiff-ui/dist/assets/{surrealql-Cj1XbW6j.js → surrealql-Cn3MlekA.js} +1 -1
  91. package/extensions/pidiff/pidiff-ui/dist/assets/{svelte-HcwCBvjC.js → svelte-DkuizfXa.js} +1 -1
  92. package/extensions/pidiff/pidiff-ui/dist/assets/{templ-D775vyzu.js → templ-Bu3VqwV1.js} +1 -1
  93. package/extensions/pidiff/pidiff-ui/dist/assets/{tex-DNmK_71Q.js → tex-BmuNCLCK.js} +1 -1
  94. package/extensions/pidiff/pidiff-ui/dist/assets/{ts-tags-KERjFDJj.js → ts-tags-BYWWTBIT.js} +1 -1
  95. package/extensions/pidiff/pidiff-ui/dist/assets/{tsx-CMT8AiHA.js → tsx-DNTVrA8s.js} +1 -1
  96. package/extensions/pidiff/pidiff-ui/dist/assets/{twig-B5uxCA6R.js → twig-CxSsuKw_.js} +1 -1
  97. package/extensions/pidiff/pidiff-ui/dist/assets/{typescript-5zwf3bJw.js → typescript-nBJ-jOMj.js} +1 -1
  98. package/extensions/pidiff/pidiff-ui/dist/assets/{typst-xx02l63O.js → typst-CJ8ooa07.js} +1 -1
  99. package/extensions/pidiff/pidiff-ui/dist/assets/{vue-BvvQRkFj.js → vue-CFh-25xK.js} +1 -1
  100. package/extensions/pidiff/pidiff-ui/dist/assets/{vue-html-BrBDvgBy.js → vue-html-C0o6Mt8F.js} +1 -1
  101. package/extensions/pidiff/pidiff-ui/dist/assets/{vue-vine-1CllPWUz.js → vue-vine-CdvaqX2M.js} +1 -1
  102. package/extensions/pidiff/pidiff-ui/dist/assets/worker-1fjNO6T8.js +164 -0
  103. package/extensions/pidiff/pidiff-ui/dist/assets/{xml-C3nsfg4x.js → xml-DgTZJwkL.js} +1 -1
  104. package/extensions/pidiff/pidiff-ui/dist/assets/{xsl-DDOShcMh.js → xsl-DpEWsE0p.js} +1 -1
  105. package/extensions/pidiff/pidiff-ui/dist/assets/{yaml-BGNXenqm.js → yaml-DnMT_m8N.js} +1 -1
  106. package/extensions/pidiff/pidiff-ui/dist/index.html +1 -1
  107. package/extensions/pidiff/pidiff-ui/package.json +1 -1
  108. package/extensions/providers/acpx-driver.ts +136 -69
  109. package/extensions/providers/claude-driver.ts +20 -18
  110. package/extensions/providers/cursor-acpx-driver.ts +136 -70
  111. package/extensions/providers/cursor-cli-driver.ts +22 -17
  112. package/extensions/providers/gemini-driver.ts +20 -17
  113. package/extensions/providers/index.ts +22 -7
  114. package/extensions/providers/last-thinking-level.ts +174 -0
  115. package/extensions/shared/build-system-prompt.ts +12 -5
  116. package/extensions/shared/create-runtime-provider.ts +1 -1
  117. package/extensions/shared/file-logger.ts +22 -18
  118. package/extensions/shared/format-total.ts +11 -0
  119. package/extensions/shared/install-logger.mjs +18 -0
  120. package/extensions/shared/logger-core.mjs +35 -0
  121. package/extensions/shared/logger.ts +10 -27
  122. package/extensions/shared/openai-compatible-discovery.ts +28 -0
  123. package/extensions/shared/types.ts +4 -0
  124. package/package.json +11 -2
  125. package/scripts/daemon-shared.ts +11 -0
  126. package/scripts/graft-allow-scripts.mjs +47 -0
  127. package/scripts/pidash-server.ts +9 -5
  128. package/scripts/pidash-session-state.ts +10 -0
  129. package/scripts/pidiff-server.ts +2 -2
  130. package/settings-keys.json +21 -0
  131. package/extensions/pidash/pidash-ui/dist/assets/index-j_XBAdtq.css +0 -2
  132. package/extensions/pidiff/pidiff-ui/dist/assets/index-CisAus9r.js +0 -2801
  133. package/extensions/pidiff/pidiff-ui/dist/assets/worker-BX5VNpEv.js +0 -161
package/README.md CHANGED
@@ -28,6 +28,7 @@ Single extension that provides:
28
28
  | **Git status** | Live git status in status line with colored icons — updates after every tool call. Shows clickable `#N` when the branch has an open PR. Last-activity clock `⏱ HH:MM (Xm/Xh ago)` shows time since last response |
29
29
  | **Desktop notifications** | Notifies via `notify-send` on task completion, waiting for input, and action required |
30
30
  | **File preview** | Serves generated HTML/frontend files via HTTP for browser preview from container |
31
+ | **Graft integration** | When `graft_enable=true`, main agents and subagents query the local Graft graph before raw project navigation on substantive prompts. Children only consume graphs. Stale graphs remain usable, retrieval failures fall back to raw tools, and main processes coordinate rebuilds with a cross-process lock. |
31
32
  | **Pidash dashboard** | Live web dashboard — multi-session monitoring, browser messaging, model switching, live session name updates, reasoning token display |
32
33
  | **Pidiff viewer** | Per-project diff viewer with review comments — branch diffs, file tree, inline comments, git-based ignore rules |
33
34
  | **Dreaming** | Background memory consolidation — extracts memories from sessions, deduplicates, maintains topic-based memory |
@@ -157,7 +158,7 @@ The installer covers:
157
158
 
158
159
  - **Pi Packages** — pi-config, pi-vertex-claude, pi-web-access, myk-pi-tools, bun
159
160
  - **Python Tools** — prek
160
- - **npm Packages** — mcpc, acpx, agent-browser
161
+ - **npm Packages** — mcpc, acpx, agent-browser, graft
161
162
  - **Browser Automation** — playwright + chromium
162
163
  - **Environment Setup** — gitignore configuration
163
164
 
@@ -404,6 +405,29 @@ Set `image_model` in `pi-config-settings.json` or use `PI_IMAGE_MODEL` env var.
404
405
 
405
406
  In containers, images are auto-served via HTTP for browser preview.
406
407
 
408
+ ### Graft repository graph (optional)
409
+
410
+ Set `graft_enable` to `true` in `.pi/pi-config-settings.json` (or set
411
+ `PI_GRAFT_ENABLE=true`) to enable local Graft repository graph context:
412
+
413
+ ```json
414
+ {
415
+ "graft_enable": true
416
+ }
417
+ ```
418
+
419
+ Graft requires Node.js 22+, npm 12+, node-gyp prerequisites (Python 3, `make`, and
420
+ G++), and the `graft` CLI. The container includes them; native installs can use
421
+ `uv run scripts/install.py`. The installer downloads `@nanonets/graft@latest` with
422
+ scripts disabled into a dedicated npm prefix, audits that isolated dependency tree
423
+ for lifecycle scripts, and strictly rebuilds it with only those packages approved.
424
+ The completed prefix is then published under the global npm prefix and its `graft`
425
+ binary linked globally, so unrelated global packages are never evaluated. The
426
+ installer verifies `graft --version`; `DO_NOT_TRACK=1` disables Graft telemetry.
427
+ When enabled, Graft reads the trusted project and writes a regenerable `./graft/`
428
+ cache. Delete that directory or set `graft_enable` to `false` to disable it. If the
429
+ native CLI is unavailable or its installation fails, pi continues without Graft.
430
+
407
431
  ### Cache Miss Notices
408
432
 
409
433
  Enable `showCacheMissNotices` in pi settings to see transcript notices on significant prompt-cache misses — useful for investigating unexpected token costs:
@@ -425,6 +449,23 @@ Run pi inside a disposable container for **filesystem isolation** — the agent
425
449
  - **Consistent tooling** — All required tools pre-installed in a single image
426
450
  - **Disposable** — Container is destroyed after each session (`--rm`)
427
451
 
452
+ **Graft (optional `graft_enable`):** The image installs the latest
453
+ `@nanonets/graft` with `DO_NOT_TRACK=1`. It downloads with scripts disabled, derives
454
+ an exact script allowlist by auditing the installed package's reachable dependency
455
+ metadata in a dedicated npm prefix, then strictly rebuilds that isolated tree with
456
+ those approvals. Only after the rebuild succeeds is the completed prefix moved under
457
+ the global npm prefix and its `graft` binary linked globally. This prevents unrelated
458
+ global packages from entering npm's strict preflight. The allowlist is command-scoped
459
+ and changes with `@latest`; other npm commands keep npm 12's install-script
460
+ protections. The image includes Python 3,
461
+ `make`, and the GCC/G++ toolchain required by node-gyp for Graft and other native
462
+ npm packages; these remain available for reliable package rebuilds. The build then
463
+ verifies `graft --version`. Enable Graft in trusted project or global pi-config
464
+ settings; it reads the project locally and stores its regenerable graph in `./graft/`. Set
465
+ `graft_enable` to `false` and delete that directory to disable and remove the cache.
466
+ Native installations require Node.js 22+, npm 12+, Python 3, `make`, and G++; the
467
+ installer uses the same audited strict installation and failure remains non-fatal.
468
+
428
469
  **CLI provider binaries (optional `cli_agents`):** The image installs the CLIs used by
429
470
  `cli-*` providers — `claude` (Claude Code), `gemini` (`@google/gemini-cli`), and
430
471
  `agent` (Cursor Agent CLI). Enable with `cli_agents` in settings (e.g.
@@ -691,6 +732,7 @@ PI_PIDIFF_ENABLE=false pi
691
732
  | `acpx` | Agent proxy for remote models |
692
733
  | `kubectl` / `oc` | Kubernetes and OpenShift CLI |
693
734
  | `agent-browser` | Browser automation CLI (navigate, click, screenshot, forms) |
735
+ | `graft` | Local repository code graph for opt-in `graft_enable`; graph cache is stored in the mounted project’s `./graft/` directory |
694
736
  | `procps` | Process utilities (ps, top, pgrep, pkill) |
695
737
  | `passwd` | `usermod`/`groupmod` — init remaps user `node` to `PI_HOST_UID`/`PI_HOST_GID` |
696
738
  | `docker` / `podman` | Container CLIs (used via `docker-safe` read-only wrapper) |
@@ -844,7 +886,7 @@ See [DEVELOPMENT.md](DEVELOPMENT.md) for tips on testing extensions locally, run
844
886
 
845
887
  ## Prerequisites
846
888
 
847
- - [pi](https://github.com/badlogic/pi-mono) (minimum version: **0.84.4**). Pidash uses Pi's `ui_prompt_start` and `ui_prompt_end` events to show prompt wait separately from agent work.
889
+ - [pi](https://github.com/badlogic/pi-mono) (minimum version: **0.87.0**). Pidash uses Pi's `ui_prompt_start` and `ui_prompt_end` events to show prompt wait separately from agent work.
848
890
  - `gh` CLI (for GitHub operations)
849
891
  - `uv` (for Python execution)
850
892
  - `myk-pi-tools` (optional, for `/pr-review` and `/release`)
@@ -848,10 +848,11 @@ export default function (pi: ExtensionAPI) {
848
848
  ackOk(socket, env.msg_id);
849
849
  }
850
850
 
851
- function buildAgentCard(): AgentCard {
851
+ async function buildAgentCard(): Promise<AgentCard> {
852
852
  const ctx = currentCtx;
853
853
  const ident = identity;
854
854
  const pct = ctx ? Math.round(ctx.getContextUsage()?.percent ?? 0) : 0;
855
+ log.debug("build_agent_card", { hasIdentity: !!ident });
855
856
  return {
856
857
  name: ident?.name ?? "unknown",
857
858
  purpose: ident?.purpose ?? "",
@@ -859,22 +860,24 @@ export default function (pi: ExtensionAPI) {
859
860
  color: ident?.color ?? "#36F9F6",
860
861
  context_used_pct: pct,
861
862
  queue_depth: getPendingInboundCount(),
862
- tasks_summary: readTaskSummary(currentCtx?.cwd ?? process.cwd(), currentCtx?.sessionManager?.getSessionId?.()),
863
+ tasks_summary: await readTaskSummary(currentCtx?.cwd ?? process.cwd(), currentCtx?.sessionManager?.getSessionId?.()),
863
864
  };
864
865
  }
865
866
 
866
867
  /** Push current agent card to ping worker so it can respond independently */
867
- function updatePingWorkerCard(): void {
868
+ async function updatePingWorkerCard(): Promise<void> {
869
+ log.debug("update_ping_worker_card", { ready: pingWorkerReady });
868
870
  if (!pingWorker || !pingWorkerReady) return;
869
871
  try {
870
- const card = buildAgentCard();
872
+ const card = await buildAgentCard();
871
873
  const pong: Pong = { type: "pong", msg_id: "", agent_card: card };
872
874
  pingWorker.postMessage({ type: "update_card", pong });
873
875
  } catch { /* ignore */ }
874
876
  }
875
877
 
876
- function handlePing(socket: net.Socket, env: PingEnvelope): void {
877
- const card = buildAgentCard();
878
+ async function handlePing(socket: net.Socket, env: PingEnvelope): Promise<void> {
879
+ log.debug("handle_ping", { msgId: env.msg_id });
880
+ const card = await buildAgentCard();
878
881
  const pong: Pong = { type: "pong", msg_id: env.msg_id, agent_card: card };
879
882
  try {
880
883
  socket.write(JSON.stringify(pong) + "\n");
@@ -883,7 +886,7 @@ export default function (pi: ExtensionAPI) {
883
886
  }
884
887
  try { socket.end(); } catch { /* ignore */ }
885
888
  // Also update worker with latest card
886
- updatePingWorkerCard();
889
+ await updatePingWorkerCard();
887
890
  }
888
891
 
889
892
  function isValidEnvelope(obj: any): obj is Envelope {
@@ -935,7 +938,7 @@ export default function (pi: ExtensionAPI) {
935
938
  } else if (parsed.type === "response") {
936
939
  handleResponse(socket, parsed as ResponseEnvelope);
937
940
  } else if (parsed.type === "ping") {
938
- handlePing(socket, parsed as PingEnvelope);
941
+ void handlePing(socket, parsed as PingEnvelope).catch(() => nack(socket, parsed.msg_id, "internal error"));
939
942
  } else if (parsed.type === "task_update") {
940
943
  handleTaskUpdate(socket, parsed as TaskUpdateEnvelope);
941
944
  } else if (parsed.type === "queue_manage") {
@@ -1994,7 +1997,7 @@ Do not respond to this message.`;
1994
1997
  // Append sender's active tasks for this peer to the prompt
1995
1998
  let promptText = params.prompt;
1996
1999
  try {
1997
- const { listTasksForSession } = require("../pitasks/index.js");
2000
+ const { listTasksForSession } = await import("../pitasks/index.js");
1998
2001
  const targetPiSessionId = target.pi_session_id || target.coms_session_id;
1999
2002
  const allTasks = listTasksForSession(targetPiSessionId, target.cwd) || [];
2000
2003
  const myTasks = allTasks.filter((t: any) =>
@@ -2021,27 +2024,8 @@ Do not respond to this message.`;
2021
2024
  tasks: params.tasks ?? null,
2022
2025
  };
2023
2026
 
2024
- // Send the envelope synchronously and wait for the receiver's ack.
2025
- await sendEnvelope(target.endpoint, env);
2026
- comsSendCalledThisTurn.add(target.coms_session_id);
2027
-
2028
- // Auto-create tasks on the target's task store (sender-side, instant)
2029
- if (params.tasks && params.tasks.length > 0 && target.coms_session_id) {
2030
- try {
2031
- const count = createComsInboundTasks(
2032
- params.tasks,
2033
- { sender_session: identity.coms_session_id, sender_name: identity.name, sender_endpoint: identity.endpoint },
2034
- target.pi_session_id || target.coms_session_id,
2035
- target.cwd,
2036
- );
2037
- if (count > 0) {
2038
- log.info("tasks_auto_created", msg_id, "to", target.name, "count", count);
2039
- }
2040
- } catch { /* best-effort */ }
2041
- }
2042
-
2043
- // Register a pending entry whose promise the receiver-side handleResponse
2044
- // (or the timeout below) will settle.
2027
+ // Register before sending. A peer may acknowledge and respond before
2028
+ // task creation's dynamic import completes.
2045
2029
  let resolveFn!: (v: { response?: any; error?: string | null }) => void;
2046
2030
  let rejectFn!: (e: Error) => void;
2047
2031
  const promise = new Promise<{ response?: any; error?: string | null }>((res, rej) => {
@@ -2069,13 +2053,37 @@ Do not respond to this message.`;
2069
2053
  try { (entry.timer as any).unref?.(); } catch { /* ignore */ }
2070
2054
  pendingReplies.set(msg_id, entry);
2071
2055
 
2072
- log.debug("outbound_prompt", msg_id, "to", target.name, "hops", hops);
2073
-
2074
- // Track pending outbound
2075
2056
  const targetKey = target.name;
2076
2057
  if (!pendingOutbound.has(targetKey)) pendingOutbound.set(targetKey, new Set());
2077
2058
  pendingOutbound.get(targetKey)!.add(msg_id);
2078
2059
 
2060
+ try {
2061
+ await sendEnvelope(target.endpoint, env);
2062
+ } catch (error) {
2063
+ pendingReplies.delete(msg_id);
2064
+ pendingOutbound.get(targetKey)?.delete(msg_id);
2065
+ if (pendingOutbound.get(targetKey)?.size === 0) pendingOutbound.delete(targetKey);
2066
+ if (entry.timer) clearTimeout(entry.timer);
2067
+ throw error;
2068
+ }
2069
+ comsSendCalledThisTurn.add(target.coms_session_id);
2070
+
2071
+ if (params.tasks && params.tasks.length > 0 && target.coms_session_id) {
2072
+ try {
2073
+ const count = await createComsInboundTasks(
2074
+ params.tasks,
2075
+ { sender_session: identity.coms_session_id, sender_name: identity.name, sender_endpoint: identity.endpoint },
2076
+ target.pi_session_id || target.coms_session_id,
2077
+ target.cwd,
2078
+ );
2079
+ if (count > 0) log.info("tasks_auto_created", msg_id, "to", target.name, "count", count);
2080
+ } catch (error) {
2081
+ log.warn("tasks_auto_create_failed", { msg_id, target: target.name, error: error instanceof Error ? error.message : String(error) });
2082
+ }
2083
+ }
2084
+
2085
+ log.debug("outbound_prompt", msg_id, "to", target.name, "hops", hops);
2086
+
2079
2087
  // Peer status from peerCards
2080
2088
  const peerCard = peerCards.get(target.coms_session_id);
2081
2089
  const peer_queue_depth = peerCard?.queue_depth ?? 0;
@@ -2305,6 +2313,7 @@ Do not respond to this message.`;
2305
2313
  }), { description: "Tasks to create on the peer" }),
2306
2314
  }),
2307
2315
  async execute(_callId, params) {
2316
+ log.debug("coms_tasks_create_start", { target: params.target, count: params.tasks?.length ?? 0 });
2308
2317
  if (!identity) throw new Error("coms not initialised");
2309
2318
  if (!params.tasks || params.tasks.length === 0) throw new Error("coms_tasks_create requires at least one task");
2310
2319
  const target = await resolveTarget(params.target);
@@ -2320,7 +2329,7 @@ Do not respond to this message.`;
2320
2329
  if (identity.endpoint) meta.sender_endpoint = identity.endpoint;
2321
2330
 
2322
2331
  try {
2323
- const { createTasksForSession, listTasksForSession, updateTaskForSession } = require("../pitasks/index.js");
2332
+ const { createTasksForSession, listTasksForSession, updateTaskForSession } = await import("../pitasks/index.js");
2324
2333
  const targetPiSessionId = target.pi_session_id || target.coms_session_id;
2325
2334
 
2326
2335
  // Create all tasks in one batch
@@ -2401,12 +2410,13 @@ Do not respond to this message.`;
2401
2410
  task_id: Type.String({ description: "Task ID to delete." }),
2402
2411
  }),
2403
2412
  async execute(_callId, params) {
2413
+ log.debug("coms_task_delete_start", { target: params.target, taskId: params.task_id });
2404
2414
  if (!identity) throw new Error("coms not initialised");
2405
2415
  const target = await resolveTarget(params.target);
2406
2416
  if (!target) throw new Error(`coms: no live agent matching "${params.target}"`);
2407
2417
 
2408
2418
  try {
2409
- const { getTaskForSession, deleteTaskForSession } = require("../pitasks/index.js");
2419
+ const { getTaskForSession, deleteTaskForSession } = await import("../pitasks/index.js");
2410
2420
  const targetPiSessionId = target.pi_session_id || target.coms_session_id;
2411
2421
  const task = getTaskForSession(targetPiSessionId, params.task_id, target.cwd);
2412
2422
  if (!task) throw new Error(`Task #${params.task_id} not found`);
@@ -2440,12 +2450,13 @@ Do not respond to this message.`;
2440
2450
  target: Type.String({ description: "Peer name or session_id." }),
2441
2451
  }),
2442
2452
  async execute(_callId, params) {
2453
+ log.debug("coms_task_list_start", { target: params.target });
2443
2454
  if (!identity) throw new Error("coms not initialised");
2444
2455
  const target = await resolveTarget(params.target);
2445
2456
  if (!target) throw new Error(`coms: no live agent matching "${params.target}"`);
2446
2457
 
2447
2458
  try {
2448
- const { listTasksForSession } = require("../pitasks/index.js");
2459
+ const { listTasksForSession } = await import("../pitasks/index.js");
2449
2460
  const targetPiSessionId = target.pi_session_id || target.coms_session_id;
2450
2461
  const tasks = listTasksForSession(targetPiSessionId, target.cwd);
2451
2462
  log.debug("coms_task_list", target.name, "tasks", tasks?.length ?? 0);
@@ -2494,12 +2505,13 @@ Do not respond to this message.`;
2494
2505
  task_id: Type.String({ description: "Task ID to retrieve." }),
2495
2506
  }),
2496
2507
  async execute(_callId, params) {
2508
+ log.debug("coms_task_get_start", { target: params.target, taskId: params.task_id });
2497
2509
  if (!identity) throw new Error("coms not initialised");
2498
2510
  const target = await resolveTarget(params.target);
2499
2511
  if (!target) throw new Error(`coms: no live agent matching "${params.target}"`);
2500
2512
 
2501
2513
  try {
2502
- const { getTaskForSession } = require("../pitasks/index.js");
2514
+ const { getTaskForSession, listTasksForSession } = await import("../pitasks/index.js");
2503
2515
  const targetPiSessionId = target.pi_session_id || target.coms_session_id;
2504
2516
  const task = getTaskForSession(targetPiSessionId, params.task_id, target.cwd);
2505
2517
  if (!task) throw new Error(`Task #${params.task_id} not found on ${target.name}`);
@@ -2512,7 +2524,6 @@ Do not respond to this message.`;
2512
2524
  if (task.owner) lines.push(`Owner: ${task.owner}`);
2513
2525
  lines.push(`Description: ${desc}`);
2514
2526
  if (task.blockedBy?.length > 0) {
2515
- const { listTasksForSession } = require("../pitasks/index.js");
2516
2527
  const allTasks = listTasksForSession(targetPiSessionId, target.cwd);
2517
2528
  const openBlockers = task.blockedBy.filter((bid: string) => {
2518
2529
  const blocker = allTasks.find((bt: any) => bt.id === bid);
@@ -2549,12 +2560,13 @@ Do not respond to this message.`;
2549
2560
  addBlockedBy: Type.Optional(Type.Array(Type.String(), { description: "Task IDs that block this task" })),
2550
2561
  }),
2551
2562
  async execute(_callId, params) {
2563
+ log.debug("coms_task_update_start", { target: params.target, taskId: params.task_id });
2552
2564
  if (!identity) throw new Error("coms not initialised");
2553
2565
  const target = await resolveTarget(params.target);
2554
2566
  if (!target) throw new Error(`coms: no live agent matching "${params.target}"`);
2555
2567
 
2556
2568
  try {
2557
- const { getTaskForSession, updateTaskForSession } = require("../pitasks/index.js");
2569
+ const { getTaskForSession, updateTaskForSession } = await import("../pitasks/index.js");
2558
2570
  const targetPiSessionId = target.pi_session_id || target.coms_session_id;
2559
2571
  const task = getTaskForSession(targetPiSessionId, params.task_id, target.cwd);
2560
2572
  if (!task) throw new Error(`Task #${params.task_id} not found on ${target.name}`);
@@ -461,9 +461,10 @@ export function readFrontmatterFromArgv(argv: string[]): { name?: string; descri
461
461
  }
462
462
  }
463
463
 
464
- export function readTaskSummary(_cwd: string, _sessionId?: string): { total: number; completed: number; in_progress: number } | null {
464
+ export async function readTaskSummary(_cwd: string, _sessionId?: string): Promise<{ total: number; completed: number; in_progress: number } | null> {
465
+ log.debug("read_task_summary", { hasSessionId: !!_sessionId });
465
466
  try {
466
- const { listTasks } = require("../pitasks/index.js");
467
+ const { listTasks } = await import("../pitasks/index.js");
467
468
  const tasks = listTasks();
468
469
  if (!tasks || tasks.length === 0) return null;
469
470
  let total = 0, completed = 0, in_progress = 0;
@@ -650,17 +651,18 @@ const MAX_TASK_FIELD_LEN = 2000;
650
651
  * Runs on the SENDER side — uses createTaskForSession with the target's session ID.
651
652
  * TaskStore handles locking, file format, everything.
652
653
  */
653
- export function createComsInboundTasks(
654
+ export async function createComsInboundTasks(
654
655
  tasks: Array<{ subject: string; description: string }>,
655
656
  origin: ComsOrigin,
656
657
  targetSessionId: string,
657
658
  targetCwd?: string,
658
- ): number {
659
+ ): Promise<number> {
660
+ log.debug("create_coms_inbound_tasks", { targetSessionId, count: tasks?.length ?? 0 });
659
661
  if (!tasks || tasks.length === 0 || !targetSessionId) return 0;
660
662
 
661
663
  let createTaskForSession: any;
662
664
  try {
663
- createTaskForSession = require("../pitasks/index.js").createTaskForSession;
665
+ createTaskForSession = (await import("../pitasks/index.js")).createTaskForSession;
664
666
  } catch { return 0; }
665
667
  if (!createTaskForSession) return 0;
666
668
 
@@ -4,13 +4,16 @@ import { createRuntimeProvider } from "../shared/create-runtime-provider.js";
4
4
  import { createLogger } from "../shared/logger.js";
5
5
  import {
6
6
  OpenAiCompatibleDiscoveryCache,
7
+ buildLiteLlmCapabilitiesUrl,
7
8
  buildOpenAiCompatibleModelsRequest,
9
+ enrichLiteLlmReasoning,
8
10
  findEligibleOpenAiCompatibleProviderConfigsResult,
9
11
  formatOpenAiCompatibleDiscoverySummary,
10
12
  materializeOpenAiCompatibleModels,
11
13
  openAiCompatibleConnectionFingerprint,
12
14
  redactOpenAiCompatibleDiagnostic,
13
15
  resolveStaticOpenAiCompatibleHeaders,
16
+ type LiteLlmCapabilityRecord,
14
17
  type OpenAiCompatibleModelRecord,
15
18
  type ResolvedOpenAiCompatibleConnection,
16
19
  } from "../shared/openai-compatible-discovery.js";
@@ -28,6 +31,18 @@ function responseRecords(payload: unknown): OpenAiCompatibleModelRecord[] {
28
31
  });
29
32
  }
30
33
 
34
+ function capabilityRecords(payload: unknown): LiteLlmCapabilityRecord[] {
35
+ if (!payload || typeof payload !== "object" || !Array.isArray((payload as { data?: unknown }).data))
36
+ throw new Error("LiteLLM capability response must contain a data array");
37
+ return (payload as { data: unknown[] }).data.filter(
38
+ (record): record is LiteLlmCapabilityRecord => Boolean(record) && typeof record === "object" && !Array.isArray(record),
39
+ );
40
+ }
41
+
42
+ function isLiteLlmProvider(sourceProviderId: string, source: Provider): boolean {
43
+ return `${sourceProviderId} ${source.name ?? ""}`.toLowerCase().includes("litellm");
44
+ }
45
+
31
46
  function resolvedConnection(source: Provider, auth?: AuthResult, staticHeaders?: Record<string, string>): ResolvedOpenAiCompatibleConnection {
32
47
  return {
33
48
  baseUrl: auth?.auth.baseUrl ?? source.baseUrl,
@@ -84,7 +99,20 @@ async function discoverProvider(
84
99
  signal: AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]),
85
100
  });
86
101
  if (!response.ok) throw new Error(`OpenAI-compatible /v1/models returned HTTP ${response.status}`);
87
- return responseRecords(await response.json());
102
+ const records = responseRecords(await response.json());
103
+ if (!isLiteLlmProvider(sourceProviderId, source)) return records;
104
+ try {
105
+ const capabilityResponse = await fetch(buildLiteLlmCapabilitiesUrl(request.url), {
106
+ headers: request.headers,
107
+ redirect: "error",
108
+ signal: AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]),
109
+ });
110
+ if (!capabilityResponse.ok) throw new Error(`LiteLLM capability endpoint returned HTTP ${capabilityResponse.status}`);
111
+ return enrichLiteLlmReasoning(records, capabilityRecords(await capabilityResponse.json()));
112
+ } catch (error) {
113
+ log.warn(`${sourceProviderId}: reasoning capability enrichment unavailable`, redactOpenAiCompatibleDiagnostic(error instanceof Error ? error.message : String(error), connection));
114
+ return records;
115
+ }
88
116
  } catch (error) {
89
117
  log.warn(`${sourceProviderId}: discovery refresh failed`, redactOpenAiCompatibleDiagnostic(error instanceof Error ? error.message : String(error), connection));
90
118
  throw error;
@@ -9,7 +9,13 @@
9
9
 
10
10
  import * as fs from "node:fs";
11
11
  import * as path from "node:path";
12
+ import { fileURLToPath } from "node:url";
12
13
  import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
14
+ import { createLogger } from "../shared/logger.js";
15
+
16
+ const log = createLogger("agents");
17
+
18
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
13
19
 
14
20
  export type AgentScope = "user" | "project" | "both";
15
21
 
@@ -103,7 +109,9 @@ function findNearestProjectAgentsDir(cwd: string): string | null {
103
109
  * and the agents are in ../../agents/ relative to this file.
104
110
  */
105
111
  function getPackageAgentsDir(): string {
106
- return path.resolve(__dirname, "..", "..", "agents");
112
+ const resolvedPath = path.resolve(MODULE_DIR, "..", "..", "agents");
113
+ log.debug("resolved package agents directory", { path: resolvedPath });
114
+ return resolvedPath;
107
115
  }
108
116
 
109
117
  export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
@@ -15,6 +15,7 @@ const require = createRequire(import.meta.url);
15
15
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
16
  import type { AgentConfig, AgentDiscoveryResult } from "./agents.js";
17
17
  import { resolveAgentModelProvider } from "./resolve-agent-model.js";
18
+ import { withGraftTools } from "./graft.js";
18
19
  import { getPiInvocation, getProjectTmpDir, parseProcStartTime, djb2Hash } from "./utils.js";
19
20
  import { addReviewerPending, recordReviewerResult, countFindings, readReviewState } from "./pi-config-review-state.js";
20
21
  import {
@@ -924,7 +925,8 @@ export function registerAsyncAgents(
924
925
  piArgs.push("--session-id", deterministicSessionId);
925
926
  if (effectiveModel) piArgs.push("--model", effectiveModel);
926
927
  if (effectiveProvider) piArgs.push("--provider", effectiveProvider);
927
- if (agent.tools?.length) piArgs.push("--tools", agent.tools.join(","));
928
+ const tools = withGraftTools(agent.tools, getSetting(cwd, "graft_enable"));
929
+ if (tools?.length) piArgs.push("--tools", tools.join(","));
928
930
 
929
931
  if (agent.systemPrompt?.trim()) {
930
932
  const promptPath = path.join(workerDir, "system-prompt.md");