pi-orchestrator-config 4.17.0 → 4.18.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 (115) hide show
  1. package/README.md +4 -2
  2. package/extensions/coms/coms-p2p.ts +33 -10
  3. package/extensions/coms/coms-wrapper.ts +4 -1
  4. package/extensions/image-gen/image-gen.ts +10 -1
  5. package/extensions/openai-compatible-discovery/index.ts +8 -8
  6. package/extensions/orchestrator/ask-user.ts +5 -4
  7. package/extensions/orchestrator/async-status-ui.ts +1 -1
  8. package/extensions/orchestrator/cron.ts +1 -1
  9. package/extensions/orchestrator/graft.ts +8 -4
  10. package/extensions/orchestrator/index.ts +5 -3
  11. package/extensions/orchestrator/memory-embeddings.ts +4 -3
  12. package/extensions/orchestrator/memory-scoring.ts +2 -2
  13. package/extensions/orchestrator/memory-tools.ts +26 -22
  14. package/extensions/orchestrator/memory-write.ts +154 -7
  15. package/extensions/orchestrator/project-settings.ts +19 -1
  16. package/extensions/orchestrator/review-learning.ts +1 -1
  17. package/extensions/orchestrator/review-ui.ts +3 -3
  18. package/extensions/orchestrator/rules.ts +2 -2
  19. package/extensions/orchestrator/session-search.ts +2 -0
  20. package/extensions/orchestrator/settings-tui-helpers.ts +1 -1
  21. package/extensions/orchestrator/status.ts +1 -2
  22. package/extensions/orchestrator/subagent-tool.ts +3 -3
  23. package/extensions/pidash/pidash-ui/tsconfig.json +19 -5
  24. package/extensions/pidash/pidash.ts +1 -1
  25. package/extensions/pidiff/pidiff-ui/dist/assets/{angular-html-Bfyy3ooe.js → angular-html-CwyqPeSz.js} +1 -1
  26. package/extensions/pidiff/pidiff-ui/dist/assets/{angular-ts-nVgE7bZq.js → angular-ts-Dcs1FIbz.js} +1 -1
  27. package/extensions/pidiff/pidiff-ui/dist/assets/{apl-tvfwqHpz.js → apl-C7xBLXGx.js} +1 -1
  28. package/extensions/pidiff/pidiff-ui/dist/assets/{astro-l8TnD3AA.js → astro-BFUwqfrd.js} +1 -1
  29. package/extensions/pidiff/pidiff-ui/dist/assets/{blade-BdHBbmKC.js → blade-BjNOHmhK.js} +1 -1
  30. package/extensions/pidiff/pidiff-ui/dist/assets/{c-D1CypT9z.js → c-CZ-CwvED.js} +1 -1
  31. package/extensions/pidiff/pidiff-ui/dist/assets/{chapel-D56VAXz2.js → chapel-CAUGdYR4.js} +1 -1
  32. package/extensions/pidiff/pidiff-ui/dist/assets/{cobol-BwRRZSIY.js → cobol-BXQ0NrmC.js} +1 -1
  33. package/extensions/pidiff/pidiff-ui/dist/assets/{coffee-Cet_1pvq.js → coffee-AFjtB2X3.js} +1 -1
  34. package/extensions/pidiff/pidiff-ui/dist/assets/{cpp-CZ8C9S-h.js → cpp-D1TFCGlp.js} +1 -1
  35. package/extensions/pidiff/pidiff-ui/dist/assets/{crystal-EOxgVG_1.js → crystal-CKcHF4xe.js} +1 -1
  36. package/extensions/pidiff/pidiff-ui/dist/assets/{css-lK_4bfX4.js → css-CLC5Cqcn.js} +1 -1
  37. package/extensions/pidiff/pidiff-ui/dist/assets/{edge-C7pTOYjM.js → edge-BG6TSzTp.js} +1 -1
  38. package/extensions/pidiff/pidiff-ui/dist/assets/{elixir-D1OnpXXN.js → elixir-BZH067kD.js} +1 -1
  39. package/extensions/pidiff/pidiff-ui/dist/assets/{elm-DPOsZNNm.js → elm-lEDyiWLo.js} +1 -1
  40. package/extensions/pidiff/pidiff-ui/dist/assets/{erb-D_E9Tmw3.js → erb-BR8WS6Cx.js} +1 -1
  41. package/extensions/pidiff/pidiff-ui/dist/assets/{git-rebase-FRK9CwsX.js → git-rebase-BdxT0FYt.js} +1 -1
  42. package/extensions/pidiff/pidiff-ui/dist/assets/{glimmer-js-BNQDK9N_.js → glimmer-js-VvMDmlEk.js} +1 -1
  43. package/extensions/pidiff/pidiff-ui/dist/assets/{glimmer-ts-TWnnEP-7.js → glimmer-ts-D03eMuZZ.js} +1 -1
  44. package/extensions/pidiff/pidiff-ui/dist/assets/{glsl-C90MQFKT.js → glsl-BrMuNIO1.js} +1 -1
  45. package/extensions/pidiff/pidiff-ui/dist/assets/{graphql-hToq3IB7.js → graphql-DbYNf72H.js} +1 -1
  46. package/extensions/pidiff/pidiff-ui/dist/assets/{hack-B9KIVyIF.js → hack-BBqyAZi-.js} +1 -1
  47. package/extensions/pidiff/pidiff-ui/dist/assets/{haml-CP3pGCyN.js → haml-DjcXrKGH.js} +1 -1
  48. package/extensions/pidiff/pidiff-ui/dist/assets/{handlebars-D1bq27yj.js → handlebars-CzNOAnV2.js} +1 -1
  49. package/extensions/pidiff/pidiff-ui/dist/assets/{html-DDPkOyoL.js → html-BJVm4DNc.js} +1 -1
  50. package/extensions/pidiff/pidiff-ui/dist/assets/{html-derivative-BR4J_BCH.js → html-derivative-IExyEGQB.js} +1 -1
  51. package/extensions/pidiff/pidiff-ui/dist/assets/{http-BmExgdIq.js → http-DJ3plDxM.js} +1 -1
  52. package/extensions/pidiff/pidiff-ui/dist/assets/{hurl-UvOw6UjF.js → hurl-BrB7dEfw.js} +1 -1
  53. package/extensions/pidiff/pidiff-ui/dist/assets/{index-Bo8LhGN_.js → index-CNGTUxlS.js} +3 -3
  54. package/extensions/pidiff/pidiff-ui/dist/assets/{java-CFjffq9z.js → java-CCGQ3m75.js} +1 -1
  55. package/extensions/pidiff/pidiff-ui/dist/assets/{javascript-D6jF2sRD.js → javascript-Ku50Wq9K.js} +1 -1
  56. package/extensions/pidiff/pidiff-ui/dist/assets/{jinja-_MAtAn3c.js → jinja-CLeKp8_-.js} +1 -1
  57. package/extensions/pidiff/pidiff-ui/dist/assets/{jison-CzjG9woH.js → jison-Cb8rkTso.js} +1 -1
  58. package/extensions/pidiff/pidiff-ui/dist/assets/{json-Cq0awPRn.js → json-CqjpqBY8.js} +1 -1
  59. package/extensions/pidiff/pidiff-ui/dist/assets/{jsx-iffPQUDY.js → jsx-GYEFSJQk.js} +1 -1
  60. package/extensions/pidiff/pidiff-ui/dist/assets/{julia-BJh5g1V6.js → julia-CAuSCy3z.js} +1 -1
  61. package/extensions/pidiff/pidiff-ui/dist/assets/{just-BiplQU-x.js → just-Db5dhjrQ.js} +1 -1
  62. package/extensions/pidiff/pidiff-ui/dist/assets/{latex-C9i8ER6l.js → latex-DF6G6r1P.js} +1 -1
  63. package/extensions/pidiff/pidiff-ui/dist/assets/{liquid-CJFKQVVC.js → liquid-Cq7unoCR.js} +1 -1
  64. package/extensions/pidiff/pidiff-ui/dist/assets/{lua-CQPggDzM.js → lua-JjuxLI5P.js} +1 -1
  65. package/extensions/pidiff/pidiff-ui/dist/assets/{marko-d7_vCT_U.js → marko-Dk-3Aeut.js} +1 -1
  66. package/extensions/pidiff/pidiff-ui/dist/assets/{mdc-5MqUEkF6.js → mdc-CJyvmiwK.js} +1 -1
  67. package/extensions/pidiff/pidiff-ui/dist/assets/{nginx-BGYrJESG.js → nginx-BIsypiXC.js} +1 -1
  68. package/extensions/pidiff/pidiff-ui/dist/assets/{nim-BT5d3bbB.js → nim-D2jvctay.js} +1 -1
  69. package/extensions/pidiff/pidiff-ui/dist/assets/{org-Dcsoek_G.js → org-BRGhyUU7.js} +1 -1
  70. package/extensions/pidiff/pidiff-ui/dist/assets/{perl-Dzum8ON5.js → perl-BmxxOv4e.js} +1 -1
  71. package/extensions/pidiff/pidiff-ui/dist/assets/{php-DMd6jR7H.js → php-D7e1Hsbt.js} +1 -1
  72. package/extensions/pidiff/pidiff-ui/dist/assets/{pug-zSUb8M7G.js → pug-CrUG5Kh6.js} +1 -1
  73. package/extensions/pidiff/pidiff-ui/dist/assets/{qml-B-xKh7qQ.js → qml-PR0-5H8u.js} +1 -1
  74. package/extensions/pidiff/pidiff-ui/dist/assets/{r-Bjwskoo1.js → r-WvcAB6gY.js} +1 -1
  75. package/extensions/pidiff/pidiff-ui/dist/assets/{razor-CXf_WKrJ.js → razor-B1k_Gbl4.js} +1 -1
  76. package/extensions/pidiff/pidiff-ui/dist/assets/{regexp-DT6fzMFy.js → regexp-DuGWerYo.js} +1 -1
  77. package/extensions/pidiff/pidiff-ui/dist/assets/{rst-9nW3vkMx.js → rst-DGuCLeqg.js} +1 -1
  78. package/extensions/pidiff/pidiff-ui/dist/assets/{ruby-qIjs3ahE.js → ruby-BqvCP7Bi.js} +1 -1
  79. package/extensions/pidiff/pidiff-ui/dist/assets/{sas-C5jUuuyr.js → sas-ClnFwsgU.js} +1 -1
  80. package/extensions/pidiff/pidiff-ui/dist/assets/{scss-DDLNCEB-.js → scss-CT4XA26v.js} +1 -1
  81. package/extensions/pidiff/pidiff-ui/dist/assets/{shellscript-BhivxeP8.js → shellscript-DgrnvgHZ.js} +1 -1
  82. package/extensions/pidiff/pidiff-ui/dist/assets/{shellsession-n_duFdfp.js → shellsession-C6PgLTo4.js} +1 -1
  83. package/extensions/pidiff/pidiff-ui/dist/assets/{soy-Dpt5nO7G.js → soy-nIOIsh8p.js} +1 -1
  84. package/extensions/pidiff/pidiff-ui/dist/assets/{sql-BtjLhqvp.js → sql-B2jLPOKf.js} +1 -1
  85. package/extensions/pidiff/pidiff-ui/dist/assets/{stata-D4GbpMWg.js → stata-Cy-cTFpi.js} +1 -1
  86. package/extensions/pidiff/pidiff-ui/dist/assets/{surrealql-Cn3MlekA.js → surrealql-DVUMTW4V.js} +1 -1
  87. package/extensions/pidiff/pidiff-ui/dist/assets/{svelte-DkuizfXa.js → svelte-Cvvw3d91.js} +1 -1
  88. package/extensions/pidiff/pidiff-ui/dist/assets/{templ-Bu3VqwV1.js → templ-BjdAQ08a.js} +1 -1
  89. package/extensions/pidiff/pidiff-ui/dist/assets/{tex-BmuNCLCK.js → tex-DXSGpBNr.js} +1 -1
  90. package/extensions/pidiff/pidiff-ui/dist/assets/{ts-tags-BYWWTBIT.js → ts-tags-DORvofmG.js} +1 -1
  91. package/extensions/pidiff/pidiff-ui/dist/assets/{tsx-DNTVrA8s.js → tsx-RoEQTmWU.js} +1 -1
  92. package/extensions/pidiff/pidiff-ui/dist/assets/{twig-CxSsuKw_.js → twig-Duoy_8TR.js} +1 -1
  93. package/extensions/pidiff/pidiff-ui/dist/assets/{typescript-nBJ-jOMj.js → typescript-CfCnqsgS.js} +1 -1
  94. package/extensions/pidiff/pidiff-ui/dist/assets/{typst-CJ8ooa07.js → typst-D5JfUJK-.js} +1 -1
  95. package/extensions/pidiff/pidiff-ui/dist/assets/{vue-CFh-25xK.js → vue-BYc_7aoa.js} +1 -1
  96. package/extensions/pidiff/pidiff-ui/dist/assets/{vue-html-C0o6Mt8F.js → vue-html-BvkXM02d.js} +1 -1
  97. package/extensions/pidiff/pidiff-ui/dist/assets/{vue-vine-CdvaqX2M.js → vue-vine-DKo-YX4e.js} +1 -1
  98. package/extensions/pidiff/pidiff-ui/dist/assets/{xml-DgTZJwkL.js → xml-CvbayHmE.js} +1 -1
  99. package/extensions/pidiff/pidiff-ui/dist/assets/{xsl-DpEWsE0p.js → xsl-CMBzdYyb.js} +1 -1
  100. package/extensions/pidiff/pidiff-ui/dist/assets/{yaml-DnMT_m8N.js → yaml-B9JM0AY8.js} +1 -1
  101. package/extensions/pidiff/pidiff-ui/dist/index.html +1 -1
  102. package/extensions/pidiff/pidiff-ui/package.json +1 -1
  103. package/extensions/pidiff/pidiff-ui/src/App.tsx +5 -5
  104. package/extensions/pidiff/pidiff-ui/tsconfig.json +15 -4
  105. package/extensions/pidiff/pidiff.ts +4 -1
  106. package/extensions/providers/last-thinking-level.ts +4 -1
  107. package/extensions/shared/build-system-prompt.ts +3 -2
  108. package/extensions/shared/daemon-manager.ts +8 -2
  109. package/extensions/shared/logger-core.d.mts +35 -0
  110. package/extensions/shared/stream-builder.ts +9 -3
  111. package/extensions/shared/ui/tsconfig.json +22 -0
  112. package/package.json +2 -1
  113. package/rules/60-task-tracking.md +1 -1
  114. package/extensions/orchestrator/btw.ts +0 -210
  115. package/extensions/shared/ui/resizable.tsx +0 -48
package/README.md CHANGED
@@ -16,7 +16,6 @@ Single extension that provides:
16
16
  | **`list_models`** | List available models and providers for subagent model override |
17
17
  | **Async background agents** | Spawn agents in background with `async: true` — results surface automatically when complete. Fullscreen overlay dashboard with live output, keyboard nav, and kill support (`/async-status`: `x` kills, `Space` multi-selects, `a` kills all). On **acpx** parents, optional async is coerced to sync; dream/cron need `internal_operations_provider` + `internal_operations_model`. `cli-*` providers support async natively (see `docs/cli-provider.md`) |
18
18
  | **CLI / ACPX providers** | Optional `cli_agents` / `acpx_agents` register `cli-*` / `acpx-*` via native `createProvider` (pi ≥ 0.84): `/login cli-<agent>` or `/login acpx-<agent>`, model refresh, filter when unavailable. Missing context window / maxTokens / cost filled from models.dev (`~/.pi/pi-config/models.dev.json`, 1-day refresh). Cold-start default restore (#753): gates `startup`\|`resume` (`new` only when no model is selected), empty `enabledModels`, trusted project merge — see `docs/cli-provider.md`. Headless Cursor `--approve-mcps` when `CLI_APPROVE_MCPS` is set (`false` opts out) or the process is a sidecar (`SIDECAR_PORT`; `startSidecar()` stamps it while running and restores it on `close()`). Executable sidecar consumers should call `bindSidecarListenExit()` so bind/`stopped` fatals exit 1. Headless Gemini defaults `GEMINI_CLI_TRUST_WORKSPACE=true` and preserves an explicit parent value (including `false`). |
19
- | **`/btw` command** | Quick side questions without polluting conversation history — ephemeral overlay |
20
19
  | **`/async-status` command** | Show status of background agents — select one for live output streaming, multi-select and kill (`x`), or kill everything (`a`) |
21
20
  | **`ask_user` tool** | Structured user input with options and free-text — used by workflows |
22
21
  | **Python/pip enforcement** | Auto-fixes `python`/`python3` → `uv run python`; blocks `pip`/`pip3` — requires `uv` |
@@ -36,7 +35,7 @@ Single extension that provides:
36
35
  | **Task tracking** | Structured task lists for multi-step workflows — live widget, progress tracking, reminder nudges (owned, based on [@tintinweb/pi-tasks](https://github.com/tintinweb/pi-tasks) MIT) |
37
36
  | **Neovim integration** | Send changed files and review findings to nvim's quickfix list — only active when running inside nvim |
38
37
  | **Inter-agent communication** | P2P (`/coms`) agent communication — on-demand activation via slash command |
39
- | **Slash commands** | `/pr-review`, `/issue-review`, `/release`, `/review-local`, `/review-status`, `/query-db`, `/btw`, `/async-status`, `/status`, `/dream`, `/remember`, `/coms`, `/pi-config-settings` — with autocomplete argument hints |
38
+ | **Slash commands** | `/pr-review`, `/issue-review`, `/release`, `/review-local`, `/review-status`, `/query-db`, `/async-status`, `/status`, `/dream`, `/remember`, `/coms`, `/pi-config-settings` — with autocomplete argument hints |
40
39
  | **GitHub autocomplete** | Type `#` in the editor to get issue/PR suggestions from the current repo — lazy-loaded, 5min cache |
41
40
  | **Command arg completions** | Tab-complete arguments for slash commands — providers and models for `/external-ai`, branches for `/review-local`, PR numbers for `/pr-review`, and more |
42
41
  | **Discord bot** | Control pi sessions from your phone via Discord DMs — send prompts, answer ask_user dialogs, switch sessions |
@@ -808,6 +807,7 @@ PI_PIDIFF_ENABLE=false pi
808
807
  | `myk-pi-tools release create` | Create a GitHub release with changelog |
809
808
  | `myk-pi-tools db` | Review database query commands |
810
809
  | `myk-pi-tools reviews` | Review handling commands (fetch, poll, address, post, store, store-session) |
810
+ | `myk-pi-tools reviews poll` | Poll a PR for review comments until approved or actionable — `--source qodo` reads findings through the `qodo` CLI (structured, with spans and authoritative open/closed status) when the binary is installed and authenticated, and falls back to the GitHub sticky-comment scrape otherwise. Read-only: replies are always posted as GitHub comments via `reviews address` |
811
811
  | `myk-pi-tools reviews address` | Apply replies/statuses to a fetched reviews JSON, then post and store it (see below) |
812
812
  | `myk-pi-tools reviews store-session` | Store Qodo pr-review-session findings JSON into the reviews DB (idempotent; run at loop convergence) |
813
813
  | `myk-pi-tools memory` | Project memory commands — persistent per-repo learning |
@@ -961,6 +961,8 @@ The bot starts automatically with pidash when `DISCORD_BOT_TOKEN` is configured.
961
961
  ## Development
962
962
 
963
963
  See [DEVELOPMENT.md](DEVELOPMENT.md) for tips on testing extensions locally, running tests, and managing Python dependencies.
964
+ The repository-wide TypeScript gate is `npm run typecheck` (strict `tsc --noEmit` across all six projects);
965
+ it also runs as a pre-commit hook when TypeScript files change.
964
966
 
965
967
  ## Prerequisites
966
968
 
@@ -15,7 +15,7 @@
15
15
  * Usage: loaded via extensions/coms/index.ts
16
16
  */
17
17
 
18
- import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
18
+ import type { AgentToolResult, ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
19
19
  import { Text, truncateToWidth } from "@earendil-works/pi-tui";
20
20
  import { Type } from "typebox";
21
21
  import { applyExtensionDefaults } from "./themeMap.js";
@@ -187,6 +187,22 @@ interface PendingReply {
187
187
  progress?: { note: string; at: string };
188
188
  }
189
189
 
190
+ /** #951: coms_send tool result details — blocked send or delivered message. */
191
+ type ComsSendDetails =
192
+ | { blocked: boolean; reason: string; target: string }
193
+ | { msg_id: string; target: string; target_session: string; hops: number; peer_queue_depth: number; peer_status: string };
194
+
195
+ /** #951: coms_get tool result details — error, completed reply, or pending progress. */
196
+ type ComsGetDetails =
197
+ | { status: "error"; error: string }
198
+ | { status: "complete"; response: unknown; error: string | null; queued_msg_ids: string[] }
199
+ | { status: "pending"; progress: { note: string; at: string } | null };
200
+
201
+ /** #951: coms_task_delete tool result details — blocked or deleted. */
202
+ type ComsTaskDeleteDetails =
203
+ | { blocked: boolean; reason: string }
204
+ | { target: string; task_id: string };
205
+
190
206
  interface InboundContext {
191
207
  msg_id: string;
192
208
  hops: number;
@@ -2235,7 +2251,7 @@ Do not respond to this message.`;
2235
2251
  entry.timer = setTimeout(() => {
2236
2252
  if (entry.result) return;
2237
2253
  entry.result = { error: "timeout" };
2238
- try { entry.resolve(entry.result); } catch { /* ignore */ }
2254
+ try { entry.resolve?.(entry.result); } catch { /* ignore */ }
2239
2255
  // Clean up timed-out entry from both maps
2240
2256
  const pSet = pendingOutbound.get(target.name);
2241
2257
  if (pSet) { pSet.delete(msg_id); if (pSet.size === 0) pendingOutbound.delete(target.name); }
@@ -2389,7 +2405,9 @@ Do not respond to this message.`;
2389
2405
  priority: Type.Optional(Type.Boolean({ description: "Jump the peer's queued message backlog (urgent messages). Default false." })),
2390
2406
  clearPrevious: Type.Optional(Type.Boolean({ description: "Deprecated and rejected: inspect with coms_queue_inspect, then explicitly clear using preview_id." })),
2391
2407
  }),
2392
- async execute(_callId, params) {
2408
+ // #951: explicit result type so the blocked and delivered details shapes form
2409
+ // one union instead of racing TDetails inference.
2410
+ async execute(_callId, params): Promise<AgentToolResult<ComsSendDetails>> {
2393
2411
  if (!identity) {
2394
2412
  throw new Error("coms not initialised");
2395
2413
  }
@@ -2618,7 +2636,7 @@ Do not respond to this message.`;
2618
2636
  parameters: Type.Object({
2619
2637
  msg_id: Type.String({ description: "msg_id returned by coms_send." }),
2620
2638
  }),
2621
- async execute(_callId, params) {
2639
+ async execute(_callId, params): Promise<AgentToolResult<ComsGetDetails>> {
2622
2640
  const entry = pendingReplies.get(params.msg_id);
2623
2641
  if (!entry) {
2624
2642
  return {
@@ -2731,7 +2749,7 @@ Do not respond to this message.`;
2731
2749
  if (!target) throw new Error(`coms: no live agent matching "${params.target}"`);
2732
2750
  const result = await sendQueueManage(target, "delete", params.msg_id, undefined, params.preview_id);
2733
2751
  if (result.error) throw new Error(`coms_queue_delete failed: ${result.error}`);
2734
- return { content: [{ type: "text" as const, text: `coms_queue_delete → ${target.name}\nmsg_id: ${params.msg_id}` }] };
2752
+ return { content: [{ type: "text" as const, text: `coms_queue_delete → ${target.name}\nmsg_id: ${params.msg_id}` }], details: undefined };
2735
2753
  },
2736
2754
  });
2737
2755
 
@@ -2749,7 +2767,7 @@ Do not respond to this message.`;
2749
2767
  if (!target) throw new Error(`coms: no live agent matching "${params.target}"`);
2750
2768
  const result = await sendQueueManage(target, "edit", params.msg_id, params.new_content);
2751
2769
  if (result.error) throw new Error(`coms_queue_edit failed: ${result.error}`);
2752
- return { content: [{ type: "text" as const, text: `coms_queue_edit → ${target.name}\nmsg_id: ${params.msg_id}` }] };
2770
+ return { content: [{ type: "text" as const, text: `coms_queue_edit → ${target.name}\nmsg_id: ${params.msg_id}` }], details: undefined };
2753
2771
  },
2754
2772
  });
2755
2773
 
@@ -2784,7 +2802,7 @@ Do not respond to this message.`;
2784
2802
  if (!target) throw new Error(`coms: no live agent matching "${params.target}"`);
2785
2803
  const result = await sendQueueManage(target, "prioritize", params.msg_id);
2786
2804
  if (result.error) throw new Error(`coms_queue_prioritize failed: ${result.error}`);
2787
- return { content: [{ type: "text" as const, text: `coms_queue_prioritize → ${target.name}\nmsg_id: ${params.msg_id}` }] };
2805
+ return { content: [{ type: "text" as const, text: `coms_queue_prioritize → ${target.name}\nmsg_id: ${params.msg_id}` }], details: undefined };
2788
2806
  },
2789
2807
  });
2790
2808
 
@@ -2896,7 +2914,7 @@ Do not respond to this message.`;
2896
2914
  target: Type.String({ description: "Peer name or session_id." }),
2897
2915
  task_id: Type.String({ description: "Task ID to delete." }),
2898
2916
  }),
2899
- async execute(_callId, params) {
2917
+ async execute(_callId, params): Promise<AgentToolResult<ComsTaskDeleteDetails>> {
2900
2918
  log.debug("coms_task_delete_start", { target: params.target, taskId: params.task_id });
2901
2919
  if (!identity) throw new Error("coms not initialised");
2902
2920
  const target = await resolveTarget(params.target);
@@ -3721,7 +3739,10 @@ Do not respond to this message.`;
3721
3739
  // Expose command handlers to pidash for browser command dispatch
3722
3740
  try {
3723
3741
  if ((globalThis as any).__coms_p2p_pidash_listener) {
3724
- try { pi.events.removeListener("pidash:request-commands", (globalThis as any).__coms_p2p_pidash_listener); } catch {}
3742
+ // #951: pi's EventBus exposes only emit/on — no removeListener. This cleanup has
3743
+ // always thrown (swallowed by the catch), so the stale listener is never removed;
3744
+ // latent bug left as-is, typed to match reality.
3745
+ try { (pi.events as { removeListener?: (channel: string, handler: (data: unknown) => void) => void }).removeListener?.("pidash:request-commands", (globalThis as any).__coms_p2p_pidash_listener); } catch {}
3725
3746
  }
3726
3747
  const registerWithPidash = () => {
3727
3748
  pi.events.emit("pidash:register-command", { name: "coms-queue", handler: comsQueueCmdHandler });
@@ -3790,7 +3811,9 @@ Do not respond to this message.`;
3790
3811
  try {
3791
3812
  for (const connection of connections) connection.destroy();
3792
3813
  connections.clear();
3793
- activeServer.closeAllConnections?.(); activeServer.close(); activeServer.unref();
3814
+ // #951: net.Server has no closeAllConnections at runtime or in @types/node —
3815
+ // the optional call is a graceful no-op; typed accordingly.
3816
+ (activeServer as net.Server & { closeAllConnections?: () => void }).closeAllConnections?.(); activeServer.close(); activeServer.unref();
3794
3817
  } catch { /* ignore */ }
3795
3818
  }
3796
3819
  if (pingWorker) {
@@ -166,7 +166,10 @@ export function registerComs(pi: ExtensionAPI) {
166
166
  // Expose handler to pidash for browser command dispatch
167
167
  try {
168
168
  if ((globalThis as any).__coms_pidash_listener) {
169
- try { pi.events.removeListener("pidash:request-commands", (globalThis as any).__coms_pidash_listener); } catch {}
169
+ // #951: pi's EventBus exposes only emit/on — no removeListener. This cleanup has
170
+ // always thrown (swallowed by the catch), so the stale listener is never removed;
171
+ // latent bug left as-is, typed to match reality.
172
+ try { (pi.events as { removeListener?: (channel: string, handler: (data: unknown) => void) => void }).removeListener?.("pidash:request-commands", (globalThis as any).__coms_pidash_listener); } catch {}
170
173
  }
171
174
  const registerWithPidash = () => pi.events.emit("pidash:register-command", { name: "coms", handler: comsCmdHandler });
172
175
  (globalThis as any).__coms_pidash_listener = registerWithPidash;
@@ -152,7 +152,7 @@ function serveImage(imagePath: string): { url: string; port: string; dir: string
152
152
  }
153
153
  }
154
154
 
155
- export function createImageGenTool(): ToolDefinition {
155
+ export function createImageGenTool(): ToolDefinition<typeof imageGenSchema> {
156
156
  return {
157
157
  name: "generate_image",
158
158
  label: "GenerateImage",
@@ -173,6 +173,7 @@ export function createImageGenTool(): ToolDefinition {
173
173
  type: "text" as const,
174
174
  text: "Error: image model is not set. Add image_model to pi-config-settings.json (or PI_IMAGE_MODEL env) and reload pi.",
175
175
  }],
176
+ details: undefined,
176
177
  };
177
178
  }
178
179
  if (!isImageGenModelId(model)) {
@@ -182,6 +183,7 @@ export function createImageGenTool(): ToolDefinition {
182
183
  type: "text" as const,
183
184
  text: `Error: image_model '${model}' is not an image-generation model. Choose a Gemini image model (id contains image or imagen).`,
184
185
  }],
186
+ details: undefined,
185
187
  };
186
188
  }
187
189
 
@@ -192,6 +194,7 @@ export function createImageGenTool(): ToolDefinition {
192
194
  type: "text" as const,
193
195
  text: "Error: No API key found. Set GEMINI_API_KEY or GOOGLE_API_KEY environment variable and restart pi.",
194
196
  }],
197
+ details: undefined,
195
198
  };
196
199
  }
197
200
 
@@ -231,6 +234,7 @@ export function createImageGenTool(): ToolDefinition {
231
234
  type: "text" as const,
232
235
  text: `Error: Gemini API request failed: ${err?.message ?? String(err)}`,
233
236
  }],
237
+ details: undefined,
234
238
  };
235
239
  }
236
240
 
@@ -248,6 +252,7 @@ export function createImageGenTool(): ToolDefinition {
248
252
  type: "text" as const,
249
253
  text: `Error: Gemini image request failed (${response.status}): ${message}`,
250
254
  }],
255
+ details: undefined,
251
256
  };
252
257
  }
253
258
 
@@ -260,6 +265,7 @@ export function createImageGenTool(): ToolDefinition {
260
265
  type: "text" as const,
261
266
  text: `Error: Failed to parse Gemini response: ${rawText.slice(0, 200)}`,
262
267
  }],
268
+ details: undefined,
263
269
  };
264
270
  }
265
271
 
@@ -269,6 +275,7 @@ export function createImageGenTool(): ToolDefinition {
269
275
  type: "text" as const,
270
276
  text: `Error: Image generation blocked by safety filter: ${data.promptFeedback.blockReason}`,
271
277
  }],
278
+ details: undefined,
272
279
  };
273
280
  }
274
281
 
@@ -296,6 +303,7 @@ export function createImageGenTool(): ToolDefinition {
296
303
  type: "text" as const,
297
304
  text: `No image data returned from Gemini.${extra}`,
298
305
  }],
306
+ details: undefined,
299
307
  };
300
308
  }
301
309
 
@@ -344,6 +352,7 @@ export function createImageGenTool(): ToolDefinition {
344
352
  type: "text" as const,
345
353
  text: lines.join("\n"),
346
354
  }],
355
+ details: undefined,
347
356
  };
348
357
  },
349
358
  };
@@ -1,4 +1,4 @@
1
- import type { AuthResult, Model, Provider, RefreshModelsContext } from "@earendil-works/pi-ai";
1
+ import type { Api, AuthResult, Model, Provider, RefreshModelsContext } from "@earendil-works/pi-ai";
2
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
  import { createLogger } from "../shared/logger.js";
4
4
  import {
@@ -47,7 +47,7 @@ function resolvedConnection(source: Provider, auth?: AuthResult, staticHeaders?:
47
47
  };
48
48
  }
49
49
 
50
- function combineModels(sourceProviderId: string, staticModels: readonly Model[], records: readonly OpenAiCompatibleModelRecord[], baseUrl: string): Model[] {
50
+ function combineModels(sourceProviderId: string, staticModels: readonly Model<Api>[], records: readonly OpenAiCompatibleModelRecord[], baseUrl: string): Model<Api>[] {
51
51
  const existingIds = new Set(staticModels.map((model) => model.id));
52
52
  const discovered = materializeOpenAiCompatibleModels(records, baseUrl, sourceProviderId)
53
53
  .filter((model) => !existingIds.has(model.id));
@@ -59,14 +59,14 @@ async function refreshProviderModels(
59
59
  ctx: any,
60
60
  sourceProviderId: string,
61
61
  source: Provider,
62
- staticModels: readonly Model[],
62
+ staticModels: readonly Model<Api>[],
63
63
  refresh: RefreshModelsContext,
64
64
  appendSummary: () => boolean,
65
65
  discoverModelCapabilities: boolean,
66
- rawStaticHeaders?: Record<string, string>,
67
- snapshot: { discovered: Model[]; fingerprint?: string; generation: number },
66
+ rawStaticHeaders: Record<string, string> | undefined,
67
+ snapshot: { discovered: Model<Api>[]; fingerprint?: string; generation: number },
68
68
  lifecycle: { signal: AbortSignal; isCurrent: () => boolean },
69
- ): Promise<Model[]> {
69
+ ): Promise<Model<Api>[]> {
70
70
  const generation = ++snapshot.generation;
71
71
  const active = () => {
72
72
  const current = lifecycle.isCurrent();
@@ -206,9 +206,9 @@ export default function (pi: ExtensionAPI) {
206
206
  log.warn(`${id}: configured provider was not resolved`);
207
207
  continue;
208
208
  }
209
- const staticModels = (ctx.modelRegistry.getAll() as Model[])
209
+ const staticModels = (ctx.modelRegistry.getAll() as Model<Api>[])
210
210
  .filter((model) => model.provider === id);
211
- const snapshot = { discovered: [] as Model[], fingerprint: undefined as string | undefined, generation: 0 };
211
+ const snapshot = { discovered: [] as Model<Api>[], fingerprint: undefined as string | undefined, generation: 0 };
212
212
  // Registration starts an unawaited offline refresh. Its first callback signals
213
213
  // that it has acquired its generation, so the network pass can safely supersede it.
214
214
  let registrationStarted!: () => void;
@@ -48,6 +48,7 @@ export function registerAskUser(
48
48
  content: [
49
49
  { type: "text", text: "No UI available for user interaction" },
50
50
  ],
51
+ details: undefined,
51
52
  isError: true,
52
53
  };
53
54
  }
@@ -265,17 +266,17 @@ export function registerAskUser(
265
266
  unsubscribe();
266
267
 
267
268
  if (result === null) {
268
- return { content: [{ type: "text", text: "User cancelled" }], terminate: true };
269
+ return { content: [{ type: "text", text: "User cancelled" }], details: undefined, terminate: true };
269
270
  }
270
- return { content: [{ type: "text", text: result }] };
271
+ return { content: [{ type: "text", text: result }], details: undefined };
271
272
  },
272
273
 
273
274
  renderCall(args, theme) {
274
275
  let t =
275
276
  theme.fg("toolTitle", theme.bold("ask_user ")) +
276
277
  theme.fg("accent", args.question || "...");
277
- if (args.options?.length > 0) {
278
- t += `\n ${theme.fg("dim", args.options.join(" • "))}`;
278
+ if ((args.options?.length ?? 0) > 0) {
279
+ t += `\n ${theme.fg("dim", args.options!.join(" • "))}`;
279
280
  }
280
281
  return new Text(t, 0, 0);
281
282
  },
@@ -57,7 +57,7 @@ function jobTitle(job: AsyncStatusJobView): string {
57
57
  */
58
58
  export function formatKillOriginSuffix(
59
59
  killOrigin: string | undefined,
60
- theme: { fg: (color: string, value: string) => string },
60
+ theme: Pick<Theme, "fg">,
61
61
  ): string {
62
62
  if (!killOrigin) {
63
63
  log.debug("kill_origin_header: no origin recorded");
@@ -193,7 +193,7 @@ export function registerCron(pi: ExtensionAPI, spawnAsyncAgent: any): { getCronT
193
193
  const task = tasks.get(id); if (scope === "session") { if (!task) return false; stop(id); tasks.delete(id); if (sessionStore) writeSessionTasks(sessionStore, [...tasks.values()].filter(t => t.scope === "session")); } else { const file = projectStore(ctx.cwd); let found = false; mutateDurableCronStore(file, old => { found = old.some(t => t.id === id); return old.filter(t => t.id !== id); }); if (!found) return false; if (task) { stop(id); tasks.delete(id); } }
194
194
  updateStatus(); return true;
195
195
  }
196
- pi.registerTool({ name: "cron_manage", description: "Manage scheduled tasks. Set persist=true to keep a task across Pi sessions in this project. Set model to \"provider/model-id\" (or a bare \"model-id\") to pin that cron's model.", parameters: Type.Object({ action: Type.Union([Type.Literal("add"), Type.Literal("list"), Type.Literal("remove")]), persist: Type.Optional(Type.Boolean()), description: Type.Optional(Type.String()), task: Type.Optional(Type.String()), model: Type.Optional(Type.String()), interval_seconds: Type.Optional(Type.Number()), at_hour: Type.Optional(Type.Number()), at_minute: Type.Optional(Type.Number()), id: Type.Optional(Type.String()) }), async execute(_id, params: any) {
196
+ pi.registerTool({ name: "cron_manage", description: "Manage scheduled tasks. Set persist=true to keep a task across Pi sessions in this project. Set model to \"provider/model-id\" (or a bare \"model-id\") to pin that cron's model.", parameters: Type.Object({ action: Type.Union([Type.Literal("add"), Type.Literal("list"), Type.Literal("remove")]), persist: Type.Optional(Type.Boolean()), description: Type.Optional(Type.String()), task: Type.Optional(Type.String()), model: Type.Optional(Type.String()), interval_seconds: Type.Optional(Type.Number()), at_hour: Type.Optional(Type.Number()), at_minute: Type.Optional(Type.Number()), id: Type.Optional(Type.String()) }), async execute(_id: string, params: any) {
197
197
  const scope: CronScope = params.persist ? "project" : "session";
198
198
  try { if (params.action === "add" && (ctx?.mode === "print" || ctx?.mode === "json")) return { content: [{ type: "text", text: "Error: Cron scheduling is unavailable in one-shot mode." }] }; if (params.action === "add") { const task = add(scope, params); return { content: [{ type: "text", text: `Cron ${qualifyCronId(task)} created: ${formatCronSchedule(task)} → ${task.description}${task.model ? ` [${task.model}]` : ""}` }] }; } if (params.action === "remove") return { content: [{ type: "text", text: remove(params.id || "") ? "Cron removed." : "Task not found." }] }; const selected = [...tasks.values()].filter(t => !params.persist || t.scope === "project"); return { content: [{ type: "text", text: selected.length ? selected.map(t => `${qualifyCronId(t)} | ${formatCronSchedule(t)} | ${t.description} | ${t.model || "inherited model"} | ${t.leader === false ? "waiting for leader" : "active"}`).join("\n") : "No scheduled tasks." }] }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.message}` }] }; }
199
199
  }, } as any);
@@ -147,11 +147,15 @@ export function acquireBuildLock(root: string, afterClaim?: () => void): (() =>
147
147
  function trusted(ctx: any): boolean { return typeof ctx?.isProjectTrusted !== "function" || ctx.isProjectTrusted() === true; }
148
148
  function relevant(ask: Ask): Hit[] { const hits = ask.hits?.filter(hit => text(hit.pointer) && text(hit.title)) ?? []; if (!hits.length) return []; if ((typeof ask.coverage === "number" || typeof ask.coverageStrong === "number") && (ask.coverageStrong ?? 0) < .1 && (ask.coverage ?? 0) < .5) return []; return hits.slice(0, 3); }
149
149
  function pointerText(hits: Hit[]): string { return hits.map((hit, i) => { const snippet = text(hit.snippet, 140); return ` ${i + 1}. ${text(hit.title)!}: ${text(hit.pointer)!}${snippet ? `\n ${snippet.replace(/\s+/g, " ")}` : ""}`; }).join("\n"); }
150
+ type GraftNode = { id: string; name: string; path: string };
151
+ type GraftEdge = { source: string; target: string; relation: string };
152
+ type GraftWiring = { nodes?: GraftNode[]; edges?: GraftEdge[] };
150
153
  function blastHint(root: string, path: unknown): string | null {
151
- const file = typeof path === "string" ? path : null; const graph = file && wiring(root); if (!file || !graph) return null;
152
- const names = new Map((graph.nodes ?? []).map((node: any) => [node.id, node]));
153
- const ids = new Set((graph.nodes ?? []).filter((node: any) => node.path === file || file.endsWith(`/${node.path}`)).map((node: any) => node.id));
154
- const dependents = (graph.edges ?? []).filter((edge: any) => ids.has(edge.target) && !ids.has(edge.source)).slice(0, 8).map((edge: any) => { const node = names.get(edge.source); return ` • ${edge.relation} ← ${node ? `${node.name} (${basename(node.path)})` : edge.source}`; });
154
+ const file = typeof path === "string" ? path : null; const graph = file && (wiring(root) as GraftWiring | null); if (!file || !graph) return null;
155
+ const graphNodes = graph.nodes ?? [];
156
+ const names = new Map<string, GraftNode>(graphNodes.map((node) => [node.id, node] as [string, GraftNode]));
157
+ const ids = new Set(graphNodes.filter((node) => node.path === file || file.endsWith(`/${node.path}`)).map((node) => node.id));
158
+ const dependents = (graph.edges ?? []).filter((edge) => ids.has(edge.target) && !ids.has(edge.source)).slice(0, 8).map((edge) => { const node = names.get(edge.source); return ` • ${edge.relation} ← ${node ? `${node.name} (${basename(node.path)})` : edge.source}`; });
155
159
  return dependents.length ? `[graft] blast radius for ${basename(file)}, who depends on it:\n${dependents.join("\n")}` : null;
156
160
  }
157
161
  export function isEligibleGraftStartup(event: any, ctx: any): boolean { return event?.reason === "startup" && ctx?.mode !== "print" && ctx?.mode !== "json" && process.env.PI_SUBAGENT_CHILD !== "1" && !isPiOneshotInvocation(); }
@@ -5,7 +5,7 @@
5
5
  * - Subagent tool (based on pi's subagent example, with package agent discovery)
6
6
  * - Enforcement handlers (python/pip, git protection, dangerous commands)
7
7
  * - Rule injection & memory loading (before_agent_start)
8
- * - Slash commands (/btw, /async-status, /dream-auto)
8
+ * - Slash commands (/async-status, /dream-auto)
9
9
  * - Notifications and status line
10
10
  * - Session validation (required tools check)
11
11
  * - Async agent infrastructure
@@ -17,7 +17,6 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
17
  import { Type } from "typebox";
18
18
  import { registerAskUser } from "./ask-user.js";
19
19
  import { registerAsyncAgents } from "./async-agents.js";
20
- import { registerBtw } from "./btw.js";
21
20
  import { registerDreaming } from "./dreaming.js";
22
21
  import { registerEnforcement } from "./enforcement.js";
23
22
  import { registerProjectSettings } from "./project-settings.js";
@@ -104,7 +103,6 @@ export default function (pi: ExtensionAPI) {
104
103
  registerRules(pi, getAsyncJobs);
105
104
 
106
105
  registerStatusLine(pi, IN_CONTAINER, terminalNotify);
107
- registerBtw(pi);
108
106
  registerDreaming(pi, spawnAsyncAgent);
109
107
  const { getCronTasks } = registerCron(pi, spawnAsyncAgent);
110
108
  registerSessionValidation(pi);
@@ -137,12 +135,14 @@ export default function (pi: ExtensionAPI) {
137
135
  if (!registry) {
138
136
  return {
139
137
  content: [{ type: "text" as const, text: "Model registry not available." }],
138
+ details: undefined,
140
139
  };
141
140
  }
142
141
  models = registry.getAvailable?.() || registry.getAll?.() || [];
143
142
  } catch (e: any) {
144
143
  return {
145
144
  content: [{ type: "text" as const, text: `Failed to list models: ${e?.message || e}` }],
145
+ details: undefined,
146
146
  };
147
147
  }
148
148
  // Filter out acpx-* providers — they cannot work in subagent children
@@ -154,6 +154,7 @@ export default function (pi: ExtensionAPI) {
154
154
  if (filtered.length === 0) {
155
155
  return {
156
156
  content: [{ type: "text" as const, text: params.provider ? `No models found for provider "${params.provider}".` : "No models available." }],
157
+ details: undefined,
157
158
  };
158
159
  }
159
160
  // Group by provider for readability
@@ -172,6 +173,7 @@ export default function (pi: ExtensionAPI) {
172
173
  }
173
174
  return {
174
175
  content: [{ type: "text" as const, text: lines.join("\n") }],
176
+ details: undefined,
175
177
  };
176
178
  },
177
179
  });
@@ -94,9 +94,10 @@ async function getPipeline(): Promise<any> {
94
94
  pipelineLoading = (async () => {
95
95
  try {
96
96
  const { pipeline } = await import("@huggingface/transformers");
97
- const extractor = await pipeline("feature-extraction", "Xenova/bge-small-en-v1.5", {
98
- quantized: true,
99
- });
97
+ // q8 quantized weights: ~4x smaller and faster than fp32, negligible
98
+ // accuracy loss for embedding similarity (dtype replaced `quantized` in
99
+ // @huggingface/transformers v4 — issue #951 follow-up).
100
+ const extractor = await pipeline("feature-extraction", "Xenova/bge-small-en-v1.5", { dtype: "q8" });
100
101
  pipelineInstance = extractor;
101
102
  return extractor;
102
103
  } catch {
@@ -302,7 +302,7 @@ export function loadScores(cwd: string): ScoresFile {
302
302
  // the cross-process lock — migrating the snapshot loaded above could
303
303
  // append a stale state and erase a concurrent writer's update.
304
304
  if (data.hashAlgo !== HASH_ALGO) {
305
- getScoresStore(cwd).mutate<ScoresFile>(
305
+ getScoresStore(cwd).mutate(
306
306
  (latest) => {
307
307
  if (latest.hashAlgo === HASH_ALGO) return; // a concurrent process already migrated
308
308
  migrateScoreKeysInPlace(cwd, latest);
@@ -408,7 +408,7 @@ export function mutateScores(
408
408
  ): ScoresFile | null {
409
409
  // Ensure key migration has run before mutating (idempotent, cached read)
410
410
  loadScores(cwd);
411
- return getScoresStore(cwd).mutate<ScoresFile>(fn, {
411
+ return getScoresStore(cwd).mutate(fn, {
412
412
  entries: {},
413
413
  lastRebuild: new Date().toISOString(),
414
414
  hashAlgo: HASH_ALGO,