@oh-my-pi/pi-coding-agent 17.2.8 → 17.2.10

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 (246) hide show
  1. package/CHANGELOG.md +94 -1
  2. package/dist/{CHANGELOG-0b0w17se.md → CHANGELOG-rhwzpexa.md} +94 -1
  3. package/dist/cli.js +13216 -15614
  4. package/dist/types/async/job-manager.d.ts +12 -0
  5. package/dist/types/cli/args.d.ts +1 -0
  6. package/dist/types/config/model-resolver.d.ts +13 -0
  7. package/dist/types/discovery/agents.d.ts +11 -0
  8. package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
  9. package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
  10. package/dist/types/extensibility/extensions/loader.d.ts +2 -2
  11. package/dist/types/extensibility/extensions/types.d.ts +34 -8
  12. package/dist/types/extensibility/hooks/types.d.ts +5 -5
  13. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +10 -0
  14. package/dist/types/hindsight/state.d.ts +0 -1
  15. package/dist/types/index.d.ts +2 -2
  16. package/dist/types/lsp/utils.d.ts +8 -0
  17. package/dist/types/main.d.ts +1 -1
  18. package/dist/types/modes/acp/acp-agent.d.ts +1 -1
  19. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  20. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
  21. package/dist/types/modes/acp/acp-mode.d.ts +1 -1
  22. package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
  23. package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
  24. package/dist/types/modes/components/agent-hub.d.ts +12 -5
  25. package/dist/types/modes/components/model-browser.d.ts +9 -1
  26. package/dist/types/modes/interactive-mode.d.ts +8 -1
  27. package/dist/types/modes/session-observer-registry.d.ts +2 -0
  28. package/dist/types/registry/agent-lifecycle.d.ts +1 -1
  29. package/dist/types/registry/agent-registry.d.ts +47 -0
  30. package/dist/types/registry/persisted-agents.d.ts +3 -1
  31. package/dist/types/session/agent-session-types.d.ts +7 -0
  32. package/dist/types/session/agent-session.d.ts +8 -4
  33. package/dist/types/session/session-entries.d.ts +10 -0
  34. package/dist/types/session/session-loader.d.ts +11 -1
  35. package/dist/types/session/session-manager.d.ts +9 -1
  36. package/dist/types/session/session-tools.d.ts +10 -1
  37. package/dist/types/session/turn-recovery.d.ts +6 -2
  38. package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
  39. package/dist/types/slash-commands/available-commands.d.ts +1 -1
  40. package/dist/types/task/executor.d.ts +10 -0
  41. package/dist/types/task/index.d.ts +2 -3
  42. package/dist/types/task/read-only-policy.d.ts +3 -0
  43. package/dist/types/task/structured-subagent.d.ts +2 -0
  44. package/dist/types/task/types.d.ts +4 -0
  45. package/dist/types/tools/bash.d.ts +3 -3
  46. package/dist/types/tools/browser/launch.d.ts +3 -0
  47. package/dist/types/tools/browser/tab-worker.d.ts +2 -2
  48. package/dist/types/tools/path-utils.d.ts +8 -0
  49. package/dist/types/tools/read.d.ts +3 -5
  50. package/dist/types/tools/run-scope.d.ts +27 -1
  51. package/dist/types/tools/shell-tokenize.d.ts +21 -3
  52. package/dist/types/tools/terminal-output.d.ts +1 -1
  53. package/dist/types/utils/changelog.d.ts +3 -2
  54. package/dist/types/utils/late-cleanup.d.ts +2 -0
  55. package/dist/types/utils/turndown.d.ts +1 -1
  56. package/dist/types/vibe/runtime.d.ts +4 -3
  57. package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
  58. package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
  59. package/examples/custom-tools/README.md +1 -1
  60. package/examples/extensions/README.md +1 -1
  61. package/examples/extensions/api-demo.ts +5 -6
  62. package/examples/extensions/chalk-logger.ts +1 -1
  63. package/examples/extensions/hello.ts +2 -2
  64. package/examples/extensions/reload-runtime.ts +1 -1
  65. package/examples/extensions/tools.ts +2 -2
  66. package/examples/extensions/with-deps/index.ts +1 -1
  67. package/examples/sdk/06-extensions.ts +1 -1
  68. package/package.json +15 -31
  69. package/scripts/bundle-dist.ts +1 -11
  70. package/scripts/omp +11 -2
  71. package/src/advisor/runtime.ts +54 -0
  72. package/src/async/job-manager.ts +26 -0
  73. package/src/cli/agents-cli.ts +1 -1
  74. package/src/cli/args.ts +24 -2
  75. package/src/cli/auth-broker-cli.ts +1 -1
  76. package/src/cli/auth-gateway-cli.ts +1 -1
  77. package/src/cli/bench-cli.ts +1 -1
  78. package/src/cli/claude-trace-cli.ts +1 -1
  79. package/src/cli/config-cli.ts +1 -1
  80. package/src/cli/dry-balance-cli.ts +1 -1
  81. package/src/cli/file-processor.ts +1 -1
  82. package/src/cli/flag-tables.ts +4 -0
  83. package/src/cli/grep-cli.ts +1 -1
  84. package/src/cli/grievances-cli.ts +1 -1
  85. package/src/cli/help-extra.ts +1 -1
  86. package/src/cli/models-cli.ts +1 -1
  87. package/src/cli/plugin-cli.ts +1 -1
  88. package/src/cli/read-cli.ts +1 -1
  89. package/src/cli/setup-cli.ts +1 -1
  90. package/src/cli/shell-cli.ts +1 -1
  91. package/src/cli/ssh-cli.ts +1 -1
  92. package/src/cli/stats-cli.ts +4 -4
  93. package/src/cli/tiny-models-cli.ts +1 -1
  94. package/src/cli/ttsr-cli.ts +1 -1
  95. package/src/cli/update-cli.ts +3 -57
  96. package/src/cli/usage-cli.ts +1 -1
  97. package/src/cli/web-search-cli.ts +1 -1
  98. package/src/cli/worktree-cli.ts +1 -1
  99. package/src/commands/say.ts +1 -1
  100. package/src/commands/token.ts +1 -1
  101. package/src/commit/agentic/agent.ts +1 -1
  102. package/src/commit/cli.ts +1 -1
  103. package/src/config/config-file.ts +2 -2
  104. package/src/config/keybindings.ts +3 -3
  105. package/src/config/model-discovery.ts +36 -2
  106. package/src/config/model-registry.ts +4 -1
  107. package/src/config/model-resolver.ts +54 -7
  108. package/src/discovery/agents.ts +73 -3
  109. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
  110. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
  111. package/src/discovery/claude.ts +8 -5
  112. package/src/discovery/cursor.ts +8 -5
  113. package/src/discovery/gemini.ts +11 -8
  114. package/src/discovery/vscode.ts +1 -0
  115. package/src/discovery/windsurf.ts +4 -2
  116. package/src/edit/hashline/filesystem.ts +7 -7
  117. package/src/eval/agent-bridge.ts +1 -3
  118. package/src/eval/jl/prelude.jl +4 -4
  119. package/src/eval/js/shared/prelude.txt +2 -2
  120. package/src/eval/py/prelude.py +0 -3
  121. package/src/eval/py/runner.py +38 -1
  122. package/src/eval/rb/prelude.rb +1 -2
  123. package/src/extensibility/custom-commands/loader.ts +4 -4
  124. package/src/extensibility/custom-commands/types.ts +5 -5
  125. package/src/extensibility/custom-tools/loader.ts +4 -4
  126. package/src/extensibility/custom-tools/types.ts +1 -1
  127. package/src/extensibility/extensions/loader.ts +7 -6
  128. package/src/extensibility/extensions/types.ts +39 -8
  129. package/src/extensibility/hooks/loader.ts +4 -5
  130. package/src/extensibility/hooks/types.ts +5 -5
  131. package/src/extensibility/legacy-pi-coding-agent-shim.ts +23 -0
  132. package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
  133. package/src/hindsight/state.ts +2 -22
  134. package/src/index.ts +2 -2
  135. package/src/internal-urls/memory-protocol.ts +33 -0
  136. package/src/launch/terminal-output.ts +1 -1
  137. package/src/lsp/client.ts +2 -2
  138. package/src/lsp/utils.ts +29 -0
  139. package/src/main.ts +85 -21
  140. package/src/markit/converters/docx.ts +1 -1
  141. package/src/markit/converters/epub.ts +1 -1
  142. package/src/markit/converters/pptx.ts +1 -1
  143. package/src/markit/converters/xlsx.ts +1 -1
  144. package/src/modes/acp/acp-agent.ts +7 -6
  145. package/src/modes/acp/acp-client-bridge.ts +1 -1
  146. package/src/modes/acp/acp-event-mapper.ts +1 -1
  147. package/src/modes/acp/acp-mode.ts +1 -1
  148. package/src/modes/components/agent-hub-projection.ts +248 -0
  149. package/src/modes/components/agent-hub-renderer.ts +194 -0
  150. package/src/modes/components/agent-hub.ts +670 -192
  151. package/src/modes/components/agent-transcript-viewer.ts +1 -3
  152. package/src/modes/components/assistant-message.ts +1 -1
  153. package/src/modes/components/bordered-loader.ts +1 -1
  154. package/src/modes/components/custom-editor.ts +2 -2
  155. package/src/modes/components/extensions/inspector-panel.ts +11 -6
  156. package/src/modes/components/footer.ts +1 -3
  157. package/src/modes/components/hook-input.ts +1 -1
  158. package/src/modes/components/hook-selector.ts +2 -2
  159. package/src/modes/components/model-browser.ts +11 -3
  160. package/src/modes/components/model-hub.ts +4 -1
  161. package/src/modes/components/session-selector.ts +2 -2
  162. package/src/modes/components/status-line/segments.ts +4 -3
  163. package/src/modes/controllers/event-controller.ts +188 -7
  164. package/src/modes/controllers/extension-ui-controller.ts +4 -3
  165. package/src/modes/controllers/mcp-command-controller.ts +81 -20
  166. package/src/modes/controllers/selector-controller.ts +40 -49
  167. package/src/modes/controllers/streaming-reveal.ts +1 -1
  168. package/src/modes/interactive-mode.ts +13 -2
  169. package/src/modes/print-mode.ts +29 -12
  170. package/src/modes/rpc/rpc-mode.ts +2 -2
  171. package/src/modes/runtime-init.ts +1 -1
  172. package/src/modes/session-observer-registry.ts +5 -0
  173. package/src/modes/theme/theme.ts +2 -2
  174. package/src/prompts/tools/computer.md +1 -1
  175. package/src/registry/agent-lifecycle.ts +35 -10
  176. package/src/registry/agent-registry.ts +65 -2
  177. package/src/registry/persisted-agents.ts +341 -16
  178. package/src/sdk.ts +11 -5
  179. package/src/session/agent-session-types.ts +8 -0
  180. package/src/session/agent-session.ts +177 -188
  181. package/src/session/session-entries.ts +10 -0
  182. package/src/session/session-listing.ts +1 -1
  183. package/src/session/session-loader.ts +74 -8
  184. package/src/session/session-manager.ts +22 -2
  185. package/src/session/session-paths.ts +105 -110
  186. package/src/session/session-tools.ts +28 -1
  187. package/src/session/settings-stream-fn.ts +7 -3
  188. package/src/session/turn-recovery.ts +236 -18
  189. package/src/session/unexpected-stop-classifier.ts +12 -3
  190. package/src/slash-commands/acp-builtins.ts +1 -1
  191. package/src/slash-commands/available-commands.ts +1 -1
  192. package/src/slash-commands/helpers/stats-dashboard.ts +2 -1
  193. package/src/task/agents.ts +1 -1
  194. package/src/task/executor.ts +173 -47
  195. package/src/task/index.ts +9 -28
  196. package/src/task/isolation-runner.ts +43 -12
  197. package/src/task/persisted-revive.ts +12 -5
  198. package/src/task/read-only-policy.ts +27 -0
  199. package/src/task/structured-subagent.ts +31 -6
  200. package/src/task/types.ts +4 -0
  201. package/src/tiny/worker.ts +1 -1
  202. package/src/tools/auto-generated-guard.ts +1 -1
  203. package/src/tools/bash-interactive.ts +4 -4
  204. package/src/tools/bash-interceptor.ts +8 -4
  205. package/src/tools/bash.ts +6 -6
  206. package/src/tools/browser/cmux/cmux-tab.ts +66 -18
  207. package/src/tools/browser/launch.ts +76 -19
  208. package/src/tools/browser/readable.ts +9 -9
  209. package/src/tools/browser/tab-supervisor.ts +12 -3
  210. package/src/tools/browser/tab-worker-entry.ts +1 -1
  211. package/src/tools/browser/tab-worker.ts +101 -12
  212. package/src/tools/fetch.ts +1 -1
  213. package/src/tools/glob.ts +4 -2
  214. package/src/tools/path-utils.ts +22 -5
  215. package/src/tools/read.ts +22 -5
  216. package/src/tools/run-scope.ts +290 -4
  217. package/src/tools/shell-tokenize.ts +38 -9
  218. package/src/tools/terminal-output.ts +1 -1
  219. package/src/tools/todo.ts +1 -1
  220. package/src/utils/changelog.ts +5 -4
  221. package/src/utils/clipboard.ts +10 -2
  222. package/src/utils/external-editor.ts +4 -2
  223. package/src/utils/jj.ts +1 -1
  224. package/src/utils/late-cleanup.ts +17 -0
  225. package/src/utils/turndown.ts +1 -2
  226. package/src/vibe/runtime.ts +202 -58
  227. package/src/web/scrapers/arxiv.ts +1 -1
  228. package/src/web/scrapers/go-pkg.ts +1 -1
  229. package/src/web/scrapers/hackage.ts +1 -12
  230. package/src/web/scrapers/iacr.ts +1 -1
  231. package/src/web/scrapers/readthedocs.ts +2 -1
  232. package/src/web/scrapers/twitter.ts +2 -2
  233. package/src/web/scrapers/types.ts +1 -1
  234. package/src/web/scrapers/wikipedia.ts +1 -1
  235. package/src/web/search/providers/browser-headers.ts +12 -39
  236. package/src/web/search/providers/codex.ts +3 -26
  237. package/src/web/search/providers/duckduckgo.ts +1 -1
  238. package/src/web/search/providers/ecosia.ts +2 -2
  239. package/src/web/search/providers/exa.ts +1 -1
  240. package/src/web/search/providers/firecrawl.ts +1 -1
  241. package/src/web/search/providers/google.ts +1 -1
  242. package/src/web/search/providers/mojeek.ts +2 -2
  243. package/src/web/search/providers/perplexity.ts +1 -1
  244. package/src/web/search/providers/public.ts +1 -1
  245. package/src/web/search/providers/startpage.ts +2 -2
  246. package/src/markit/converters/mammoth.d.ts +0 -24
package/src/index.ts CHANGED
@@ -2,12 +2,12 @@ import { HookEditorComponent, HookInputComponent, HookSelectorComponent } from "
2
2
 
3
3
  // Core session management
4
4
 
5
+ export * as zod from "@oh-my-pi/omptype/zod";
6
+ export { z } from "@oh-my-pi/omptype/zod";
5
7
  // Re-export TUI components for custom tool rendering
6
8
  export { Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
7
9
  // Logging
8
10
  export { getAgentDir, logger, VERSION } from "@oh-my-pi/pi-utils";
9
- export * as zod from "zod/v4";
10
- export { z } from "zod/v4";
11
11
  export * from "./config/keybindings";
12
12
  export * from "./config/model-registry";
13
13
  // Prompt templates
@@ -222,6 +222,18 @@ function mnemopiSessionStatesFromRegistry(): MnemopiSessionState[] {
222
222
  return states;
223
223
  }
224
224
 
225
+ function memoryBackendFromContext(context?: ResolveContext): string | undefined {
226
+ if (!context?.settings || typeof context.settings !== "object") return undefined;
227
+ try {
228
+ const get = Reflect.get(context.settings, "get");
229
+ if (typeof get !== "function") return undefined;
230
+ const backend = Reflect.apply(get, context.settings, ["memory.backend"]);
231
+ return typeof backend === "string" ? backend : undefined;
232
+ } catch {
233
+ return undefined;
234
+ }
235
+ }
236
+
225
237
  /**
226
238
  * Look up a mnemopi memory row by id across every live session's scoped banks.
227
239
  * First hit wins; returns `null` when the id is not stored anywhere in scope.
@@ -278,6 +290,10 @@ export class MemoryProtocolHandler implements ProtocolHandler {
278
290
  readonly immutable = true;
279
291
 
280
292
  async resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
293
+ const backend = memoryBackendFromContext(context);
294
+ if (backend === "off") {
295
+ throw new Error("Unknown protocol: memory://");
296
+ }
281
297
  const namespace = url.rawHost || url.hostname;
282
298
  if (!namespace) {
283
299
  throw new Error("memory:// URL requires a namespace: memory://root or memory://<memory-id>");
@@ -290,6 +306,23 @@ export class MemoryProtocolHandler implements ProtocolHandler {
290
306
  // clipped recall preview before overwriting it (issue #4443).
291
307
  if (namespace !== MEMORY_NAMESPACE) {
292
308
  const mnemopiStates = mnemopiSessionStatesFromRegistry();
309
+ const hindsightActive =
310
+ backend === "hindsight" ||
311
+ (mnemopiStates.length === 0 &&
312
+ AgentRegistry.global()
313
+ .list()
314
+ .some(ref => ref.session?.getHindsightSessionState?.()));
315
+ if (hindsightActive) {
316
+ // Hindsight keeps memories server-side and exposes no
317
+ // `memory://<id>` addressing, yet the shared `recall` tool
318
+ // description still steers a follow-up `read memory://<id>`.
319
+ // Return a corrective pointer so that stray read self-corrects in
320
+ // one turn instead of derailing on the generic namespace error
321
+ // (issue #7587).
322
+ throw new Error(
323
+ "Hindsight memories are not addressable via memory://. Recall results are final — use `recall` to search or `reflect` to synthesize. `read memory://<id>` is only available with memory.backend=mnemopi.",
324
+ );
325
+ }
293
326
  if (mnemopiStates.length === 0) {
294
327
  throw new Error(
295
328
  `Unknown memory namespace: ${namespace}. Supported: ${MEMORY_NAMESPACE} (file-backed memory summary), or a mnemopi memory id when memory.backend=mnemopi is active.`,
@@ -1,5 +1,5 @@
1
1
  import { logger } from "@oh-my-pi/pi-utils";
2
- import xterm, { type Terminal as XtermTerminal } from "@xterm/headless";
2
+ import xterm, { type Terminal as XtermTerminal } from "@oh-my-pi/pi-utils/vterm";
3
3
  import { readTerminalRows } from "../tools/terminal-output";
4
4
  import { DAEMON_PTY_COLUMNS, DAEMON_PTY_ROWS } from "./protocol";
5
5
 
package/src/lsp/client.ts CHANGED
@@ -17,7 +17,7 @@ import type {
17
17
  ServerConfig,
18
18
  WorkspaceEdit,
19
19
  } from "./types";
20
- import { detectLanguageId, fileToUri } from "./utils";
20
+ import { detectLanguageId, EquivalentUriMap, fileToUri } from "./utils";
21
21
 
22
22
  // =============================================================================
23
23
  // Client State
@@ -787,7 +787,7 @@ export async function getOrCreateClient(
787
787
  proc,
788
788
  config,
789
789
  requestId: 0,
790
- diagnostics: new Map(),
790
+ diagnostics: new EquivalentUriMap(),
791
791
  diagnosticsVersion: 0,
792
792
  dynamicCapabilityRegistrations: new Map(),
793
793
  openFiles: new Map(),
package/src/lsp/utils.ts CHANGED
@@ -78,6 +78,35 @@ function laxUriToFile(uri: string): string {
78
78
  return filePath;
79
79
  }
80
80
 
81
+ /** Map that treats equivalent file URI spellings as the same key. */
82
+ export class EquivalentUriMap<Value> extends Map<string, Value> {
83
+ #key(uri: string): string {
84
+ if (!uri.startsWith("file://")) return uri;
85
+ const filePath = path.normalize(uriToFile(uri));
86
+ return process.platform === "win32" ? filePath.toLowerCase() : filePath;
87
+ }
88
+
89
+ override delete(uri: string): boolean {
90
+ const key = this.#key(uri);
91
+ return super.delete(key);
92
+ }
93
+
94
+ override get(uri: string): Value | undefined {
95
+ const key = this.#key(uri);
96
+ return super.get(key);
97
+ }
98
+
99
+ override has(uri: string): boolean {
100
+ const key = this.#key(uri);
101
+ return super.has(key);
102
+ }
103
+
104
+ override set(uri: string, value: Value): this {
105
+ const key = this.#key(uri);
106
+ return super.set(key, value);
107
+ }
108
+ }
109
+
81
110
  // =============================================================================
82
111
  // Diagnostic Formatting
83
112
  // =============================================================================
package/src/main.ts CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  setProjectDir,
22
22
  VERSION,
23
23
  } from "@oh-my-pi/pi-utils";
24
- import chalk from "chalk";
24
+ import chalk from "@oh-my-pi/pi-utils/chalk";
25
25
  import { reset as resetCapabilities } from "./capability";
26
26
  import { type Args, reportUnrecognizedFlags } from "./cli/args";
27
27
  import { applyExtensionFlags, type ExtensionFlagSink } from "./cli/extension-flags";
@@ -52,6 +52,7 @@ import {
52
52
  } from "./discovery/helpers";
53
53
  import { injectOmpExtensionCliRoots } from "./discovery/omp-extension-roots";
54
54
  import { formatExtensionLoadNotifications } from "./extensibility/extensions/load-errors";
55
+ import { loadExtensions } from "./extensibility/extensions/loader";
55
56
  import { ExtensionRunner } from "./extensibility/extensions/runner";
56
57
  import type { ExtensionUIContext } from "./extensibility/extensions/types";
57
58
  import { scheduleMarketplaceAutoUpdate } from "./extensibility/plugins/marketplace-auto-update";
@@ -358,11 +359,31 @@ export interface AcpSessionFactoryOptions {
358
359
  sessionDir?: string;
359
360
  authStorage: AuthStorage;
360
361
  modelRegistry: ModelRegistry;
361
- parsedArgs: Pick<Args, "apiKey">;
362
+ parsedArgs: Pick<Args, "apiKey" | "trustedExtensions">;
362
363
  rawArgs: string[];
363
364
  createSession: (options: CreateAgentSessionOptions) => Promise<CreateAgentSessionResult>;
364
365
  }
365
366
 
367
+ async function loadTrustedSessionExtensions(
368
+ options: Pick<CreateAgentSessionOptions, "additionalExtensionPaths">,
369
+ cwd: string,
370
+ eventBus: EventBus,
371
+ ) {
372
+ const paths = options.additionalExtensionPaths ?? [];
373
+ for (const trustedPath of paths) {
374
+ let stat: fsSync.Stats;
375
+ try {
376
+ stat = fsSync.statSync(trustedPath);
377
+ } catch {
378
+ throw new Error(`Trusted extension must be an existing module file: ${trustedPath}`);
379
+ }
380
+ if (!stat.isFile()) {
381
+ throw new Error(`Trusted extension must be a module file, not a directory: ${trustedPath}`);
382
+ }
383
+ }
384
+ return loadExtensions(paths, cwd, eventBus);
385
+ }
386
+
366
387
  /**
367
388
  * Build the per-`session/new` factory used by ACP mode.
368
389
  *
@@ -385,6 +406,16 @@ export function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSess
385
406
  // policy (PR #3736 follow-up).
386
407
  const titleSystemPromptSource = discoverTitleSystemPromptFile(cwd);
387
408
  const titleSystemPrompt = await resolvePromptInput(titleSystemPromptSource, "title system prompt");
409
+ const eventBus = new EventBus();
410
+ const trustedExtensions =
411
+ args.parsedArgs.trustedExtensions && args.parsedArgs.trustedExtensions.length > 0
412
+ ? await loadTrustedSessionExtensions(args.baseOptions, cwd, eventBus)
413
+ : undefined;
414
+ if (trustedExtensions && trustedExtensions.errors.length > 0) {
415
+ throw new Error(
416
+ `Trusted extension failed to load: ${trustedExtensions.errors.map(item => item.error).join("; ")}`,
417
+ );
418
+ }
388
419
  const { session: nextSession } = await args.createSession({
389
420
  ...args.baseOptions,
390
421
  cwd,
@@ -398,6 +429,8 @@ export function createAcpSessionFactory(args: AcpSessionFactoryOptions): AcpSess
398
429
  deferUsageReserveConfirmation: true,
399
430
  enableMCP: false,
400
431
  titleSystemPrompt,
432
+ eventBus,
433
+ preloadedExtensions: trustedExtensions,
401
434
  });
402
435
  if (args.parsedArgs.apiKey && !args.baseOptions.model && nextSession.model) {
403
436
  args.authStorage.setRuntimeApiKey(nextSession.model.provider, args.parsedArgs.apiKey);
@@ -1114,14 +1147,34 @@ export async function buildSessionOptions(
1114
1147
  options.rules = [];
1115
1148
  }
1116
1149
 
1117
- // Additional extension paths from CLI
1118
- const cliExtensionPaths = [...(parsed.extensions ?? []), ...(parsed.hooks ?? [])];
1119
- if (cliExtensionPaths.length > 0) {
1120
- options.additionalExtensionPaths = cliExtensionPaths;
1121
- }
1122
-
1123
- if (parsed.noExtensions) {
1150
+ // Trusted extension paths are an exact allowlist for extension modules.
1151
+ if (parsed.trustedExtensions && parsed.trustedExtensions.length > 0) {
1152
+ const trustedPaths = parsed.trustedExtensions.map(trustedPath => {
1153
+ let resolvedPath: string;
1154
+ let stat: fsSync.Stats;
1155
+ try {
1156
+ resolvedPath = fsSync.realpathSync.native(trustedPath);
1157
+ stat = fsSync.statSync(resolvedPath);
1158
+ } catch {
1159
+ throw new Error(`Trusted extension must be an existing module file: ${trustedPath}`);
1160
+ }
1161
+ if (!stat.isFile()) {
1162
+ throw new Error(`Trusted extension must be a module file, not a directory: ${trustedPath}`);
1163
+ }
1164
+ return resolvedPath;
1165
+ });
1124
1166
  options.disableExtensionDiscovery = true;
1167
+ options.additionalExtensionPaths = trustedPaths;
1168
+ } else {
1169
+ // Additional extension paths from CLI
1170
+ const cliExtensionPaths = [...(parsed.extensions ?? []), ...(parsed.hooks ?? [])];
1171
+ if (cliExtensionPaths.length > 0) {
1172
+ options.additionalExtensionPaths = cliExtensionPaths;
1173
+ }
1174
+
1175
+ if (parsed.noExtensions) {
1176
+ options.disableExtensionDiscovery = true;
1177
+ }
1125
1178
  }
1126
1179
 
1127
1180
  return options;
@@ -1194,16 +1247,20 @@ export async function runRootCommand(
1194
1247
  // warning before we reach the await site below.
1195
1248
  pluginPreloadPromise.catch(() => {});
1196
1249
 
1197
- // Register CLI-provided extension package paths (`--extension`, `--hook`) so
1198
- // the `omp-plugins` discovery provider can surface their `skills/`, `hooks/`,
1199
- // `tools/`, `commands/`, `rules/`, `prompts/`, and `.mcp.json` sub-trees.
1200
- // Explicit roots remain authorized under `--no-extensions`; only ambient
1201
- // extension discovery is disabled.
1202
- const cliExtensions = [...(parsedArgs.extensions ?? []), ...(parsedArgs.hooks ?? [])];
1203
- injectOmpExtensionCliRoots(cliExtensions, home, getProjectDir(), {
1204
- mode: parsedArgs.noExtensions ? "explicit-only" : "merge",
1205
- replace: true,
1206
- });
1250
+ // Trusted files load as exact module paths, never as package roots whose
1251
+ // sibling hooks/tools/commands/MCP content could be discovered implicitly.
1252
+ if (!parsedArgs.trustedExtensions?.length) {
1253
+ // Register CLI-provided extension package paths (`--extension`, `--hook`) so
1254
+ // the `omp-plugins` discovery provider can surface their `skills/`, `hooks/`,
1255
+ // `tools/`, `commands/`, `rules/`, `prompts/`, and `.mcp.json` sub-trees.
1256
+ // Explicit roots remain authorized under `--no-extensions`; only ambient
1257
+ // extension discovery is disabled.
1258
+ const cliExtensions = [...(parsedArgs.extensions ?? []), ...(parsedArgs.hooks ?? [])];
1259
+ injectOmpExtensionCliRoots(cliExtensions, home, getProjectDir(), {
1260
+ mode: parsedArgs.noExtensions ? "explicit-only" : "merge",
1261
+ replace: true,
1262
+ });
1263
+ }
1207
1264
 
1208
1265
  let cwd = getProjectDir();
1209
1266
  // Classify the host before opening auth or settings storage so every
@@ -1542,12 +1599,14 @@ export async function runRootCommand(
1542
1599
  // string-flag value such as `--target @notes.md` is the flag's value, not a
1543
1600
  // file — and the same result is handed to createAgentSession via
1544
1601
  // `preloadedExtensions` so the discovery work is not repeated.
1545
- if (isInteractive) {
1602
+ if (isInteractive && !parsedArgs.trustedExtensions?.length) {
1546
1603
  sessionOptions.extensions = [...(sessionOptions.extensions ?? []), createWarpEventBridgeExtension()];
1547
1604
  }
1548
1605
 
1549
1606
  const eventBus = new EventBus();
1550
- const extensionsResult = await loadSessionExtensions(sessionOptions, cwd, settingsInstance, eventBus);
1607
+ const extensionsResult = parsedArgs.trustedExtensions?.length
1608
+ ? await loadTrustedSessionExtensions(sessionOptions, cwd, eventBus)
1609
+ : await loadSessionExtensions(sessionOptions, cwd, settingsInstance, eventBus);
1551
1610
  const extensionFlagSink: ExtensionFlagSink = {
1552
1611
  getFlags: () => ExtensionRunner.aggregateFlags(extensionsResult.extensions),
1553
1612
  setFlagValue: (name, value) => {
@@ -1556,6 +1615,11 @@ export async function runRootCommand(
1556
1615
  };
1557
1616
  const initialArgs = applyExtensionFlags(extensionFlagSink, rawArgs) ?? parsedArgs;
1558
1617
  normalizeContinueSessionArgs(initialArgs, rawArgs);
1618
+ if ((parsedArgs.trustedExtensions?.length ?? 0) > 0 && extensionsResult.errors.length > 0) {
1619
+ throw new Error(
1620
+ `Trusted extension failed to load: ${extensionsResult.errors.map(item => item.error).join("; ")}`,
1621
+ );
1622
+ }
1559
1623
  for (const message of formatExtensionLoadNotifications(extensionsResult.errors)) {
1560
1624
  if (isInteractive) {
1561
1625
  notifs.push({ kind: "warn", message });
@@ -1,6 +1,6 @@
1
1
  // Adapted from markit-ai (MIT). See ../NOTICE.
2
2
  import * as path from "node:path";
3
- import mammoth from "mammoth";
3
+ import mammoth from "@oh-my-pi/pi-utils/docx";
4
4
  import { createTurndown, normalizeTablesHtml } from "../../utils/turndown";
5
5
  import type { ConversionResult, Converter, StreamInfo } from "../types";
6
6
 
@@ -1,5 +1,5 @@
1
1
  // Adapted from markit-ai (MIT). See ../NOTICE.
2
- import { XMLParser } from "fast-xml-parser";
2
+ import { XMLParser } from "@oh-my-pi/pi-utils/xml";
3
3
  import { createTurndown, normalizeTablesHtml } from "../../utils/turndown";
4
4
  import { unzip, unzipText } from "../../utils/zip";
5
5
  import type { ConversionResult, Converter, StreamInfo } from "../types";
@@ -1,6 +1,6 @@
1
1
  // Adapted from markit-ai (MIT). See ../NOTICE.
2
2
  import * as path from "node:path";
3
- import { XMLParser } from "fast-xml-parser";
3
+ import { XMLParser } from "@oh-my-pi/pi-utils/xml";
4
4
  import { unzip, unzipText } from "../../utils/zip";
5
5
  import type { ConversionResult, Converter, StreamInfo } from "../types";
6
6
 
@@ -1,5 +1,5 @@
1
1
  // Adapted from markit-ai (MIT). See ../NOTICE.
2
- import { XMLParser } from "fast-xml-parser";
2
+ import { XMLParser } from "@oh-my-pi/pi-utils/xml";
3
3
  import { unzip, unzipText } from "../../utils/zip";
4
4
  import type { ConversionResult, Converter, StreamInfo } from "../types";
5
5
 
@@ -1,5 +1,8 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
+ import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
4
+ import type { AssistantMessage, Model } from "@oh-my-pi/pi-ai";
5
+ import { getBlobsDir, isEnoent, logger, type postmortem, VERSION } from "@oh-my-pi/pi-utils";
3
6
  import {
4
7
  type Agent,
5
8
  type AgentSideConnection,
@@ -39,10 +42,7 @@ import {
39
42
  type SetSessionModeRequest,
40
43
  type SetSessionModeResponse,
41
44
  type Usage,
42
- } from "@agentclientprotocol/sdk";
43
- import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
44
- import type { AssistantMessage, Model } from "@oh-my-pi/pi-ai";
45
- import { getBlobsDir, isEnoent, logger, type postmortem, VERSION } from "@oh-my-pi/pi-utils";
45
+ } from "@oh-my-pi/pi-utils/acp";
46
46
  import { disableProvider, enableProvider, reset as resetCapabilities } from "../../capability";
47
47
  import { Settings } from "../../config/settings";
48
48
  import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
@@ -2312,7 +2312,7 @@ export class AcpAgent implements Agent {
2312
2312
  this.#clientCapabilities,
2313
2313
  );
2314
2314
  if (this.#clientCapabilities?.elicitation?.form != null) {
2315
- record.session.setUsageFallbackConfirmer(confirmation => {
2315
+ record.session.setUsageFallbackConfirmer((confirmation, signal) => {
2316
2316
  const reserve =
2317
2317
  confirmation.remainingPercent === undefined
2318
2318
  ? "inside the configured reserve margin"
@@ -2320,6 +2320,7 @@ export class AcpAgent implements Agent {
2320
2320
  return uiContext.confirm(
2321
2321
  "Coding-plan reserve reached",
2322
2322
  `${confirmation.from} has ${reserve}. Switch to ${confirmation.to}? Choose No to keep using the current plan.`,
2323
+ { signal },
2323
2324
  );
2324
2325
  });
2325
2326
  }
@@ -2347,7 +2348,7 @@ export class AcpAgent implements Agent {
2347
2348
  record.session.sessionManager.appendLabelChange(targetId, label);
2348
2349
  },
2349
2350
  getActiveTools: () => record.session.getEnabledToolNames(),
2350
- getAllTools: () => record.session.getAllToolNames(),
2351
+ getAllTools: () => record.session.getAllToolInfos(),
2351
2352
  setActiveTools: toolNames => record.session.setActiveToolsByName(toolNames),
2352
2353
  getCommands: () => getSessionSlashCommands(record.session),
2353
2354
  setModel: async model => {
@@ -11,7 +11,7 @@ import type {
11
11
  ClientCapabilities,
12
12
  RequestPermissionRequest,
13
13
  ToolCallUpdate,
14
- } from "@agentclientprotocol/sdk";
14
+ } from "@oh-my-pi/pi-utils/acp";
15
15
  import type {
16
16
  ClientBridge,
17
17
  ClientBridgeCapabilities,
@@ -5,7 +5,7 @@ import type {
5
5
  ToolCallContent,
6
6
  ToolCallLocation,
7
7
  ToolKind,
8
- } from "@agentclientprotocol/sdk";
8
+ } from "@oh-my-pi/pi-utils/acp";
9
9
  import { parseXdUrl } from "../../internal-urls/xd-protocol";
10
10
  import type { AgentSessionEvent } from "../../session/agent-session";
11
11
  import { resolveToCwd } from "../../tools/path-utils";
@@ -1,6 +1,6 @@
1
1
  import * as stream from "node:stream";
2
- import { AgentSideConnection, ndJsonStream, type Stream } from "@agentclientprotocol/sdk";
3
2
  import { postmortem } from "@oh-my-pi/pi-utils";
3
+ import { AgentSideConnection, ndJsonStream, type Stream } from "@oh-my-pi/pi-utils/acp";
4
4
  import type { AgentSession } from "../../session/agent-session";
5
5
  import { AcpAgent } from "./acp-agent";
6
6
 
@@ -0,0 +1,248 @@
1
+ import type { AgentMetricsSummary, AgentRef, AgentStatus } from "../../registry/agent-registry";
2
+ import { MAIN_AGENT_ID } from "../../registry/agent-registry";
3
+ import type { ObservableSession } from "../session-observer-registry";
4
+
5
+ export type AgentMetrics = AgentMetricsSummary;
6
+
7
+ export interface AggregateMetrics extends AgentMetrics {
8
+ reportedAgents: number;
9
+ /** Rows whose duration is an observer-measured active runtime. */
10
+ activeDurationAgents: number;
11
+ }
12
+
13
+ export interface AgentTreeProjection {
14
+ rows: AgentRef[];
15
+ depthById: Map<string, number>;
16
+ parentById: Map<string, string>;
17
+ lastSiblingById: Map<string, boolean>;
18
+ }
19
+
20
+ export const STATUS_ORDER: Record<AgentStatus, number> = { running: 0, idle: 1, parked: 2, aborted: 3 };
21
+
22
+ function finiteMetric(value: number | undefined): number {
23
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
24
+ }
25
+
26
+ /** Exact observer usage for one roster entry. */
27
+ export function progressMetrics(observed: ObservableSession | undefined): AgentMetrics | undefined {
28
+ const progress = observed?.progress;
29
+ if (!progress) return undefined;
30
+ const { tokens, requests, toolCount: tools, cost, durationMs } = progress;
31
+ if (
32
+ typeof tokens !== "number" ||
33
+ !Number.isFinite(tokens) ||
34
+ typeof requests !== "number" ||
35
+ !Number.isFinite(requests) ||
36
+ typeof tools !== "number" ||
37
+ !Number.isFinite(tools) ||
38
+ typeof cost !== "number" ||
39
+ !Number.isFinite(cost) ||
40
+ typeof durationMs !== "number" ||
41
+ !Number.isFinite(durationMs)
42
+ ) {
43
+ return undefined;
44
+ }
45
+ return {
46
+ tokens,
47
+ requests,
48
+ tools,
49
+ cost,
50
+ durationMs,
51
+ durationKind: "active",
52
+ contextTokens:
53
+ typeof progress.contextTokens === "number" && Number.isFinite(progress.contextTokens)
54
+ ? progress.contextTokens
55
+ : undefined,
56
+ contextWindow:
57
+ typeof progress.contextWindow === "number" && Number.isFinite(progress.contextWindow)
58
+ ? progress.contextWindow
59
+ : undefined,
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Read direct assistant usage from a live session. SessionStats also includes
65
+ * usage embedded in completed `task` tool results, so using it for a parent
66
+ * row would double-count child rows in the aggregate.
67
+ */
68
+ export function readSessionMetrics(session: NonNullable<AgentRef["session"]>): AgentMetrics | undefined {
69
+ try {
70
+ const stats = session.getSessionStats();
71
+ const messages = session.agent?.state?.messages;
72
+ if (!Array.isArray(messages)) {
73
+ return {
74
+ tokens: stats.tokens.input + stats.tokens.output + stats.tokens.cacheWrite,
75
+ requests: stats.assistantMessages,
76
+ tools: stats.toolCalls,
77
+ cost: stats.cost,
78
+ durationMs: 0,
79
+ durationKind: "unknown",
80
+ contextTokens: stats.contextUsage?.tokens,
81
+ contextWindow: stats.contextUsage?.contextWindow,
82
+ };
83
+ }
84
+
85
+ let tokens = 0;
86
+ let requests = 0;
87
+ let tools = 0;
88
+ let cost = 0;
89
+ for (const message of messages) {
90
+ if (message.role !== "assistant") continue;
91
+ requests++;
92
+ tokens += message.usage.input + message.usage.output + message.usage.cacheWrite;
93
+ tools += message.content.filter(content => content.type === "toolCall").length;
94
+ cost += message.usage.cost.total;
95
+ }
96
+ return {
97
+ tokens,
98
+ requests,
99
+ tools,
100
+ cost,
101
+ durationMs: 0,
102
+ durationKind: "unknown",
103
+ contextTokens: stats.contextUsage?.tokens,
104
+ contextWindow: stats.contextUsage?.contextWindow,
105
+ };
106
+ } catch {
107
+ // Render-only doubles and sessions being torn down may not expose a
108
+ // complete statistics host. Missing metrics are preferable to a broken hub.
109
+ return undefined;
110
+ }
111
+ }
112
+
113
+ export function aggregateMetrics(args: {
114
+ rows: readonly AgentRef[];
115
+ observedById: ReadonlyMap<string, ObservableSession>;
116
+ metricsFor: (ref: AgentRef, observed: ObservableSession | undefined) => AgentMetrics | undefined;
117
+ fallbackStatsSession: (
118
+ ref: AgentRef,
119
+ observed: ObservableSession | undefined,
120
+ ) => NonNullable<AgentRef["session"]> | undefined;
121
+ sessionMetrics: WeakMap<object, { metrics: AgentMetrics | undefined }>;
122
+ refreshFallback: boolean;
123
+ }): { metrics: AggregateMetrics; hasFallbackLiveSessions: boolean } {
124
+ const total: AggregateMetrics = {
125
+ tokens: 0,
126
+ requests: 0,
127
+ tools: 0,
128
+ cost: 0,
129
+ durationMs: 0,
130
+ durationKind: "active",
131
+ reportedAgents: 0,
132
+ activeDurationAgents: 0,
133
+ };
134
+ let hasFallbackLiveSessions = false;
135
+ const countedFallbackSessions = new Set<NonNullable<AgentRef["session"]>>();
136
+ for (const ref of args.rows) {
137
+ const observed = args.observedById.get(ref.id);
138
+ const fallbackSession = args.fallbackStatsSession(ref, observed);
139
+ if (fallbackSession) {
140
+ hasFallbackLiveSessions = true;
141
+ if (args.refreshFallback || !args.sessionMetrics.has(fallbackSession)) {
142
+ args.sessionMetrics.set(fallbackSession, { metrics: readSessionMetrics(fallbackSession) });
143
+ }
144
+ }
145
+ const metrics = args.metricsFor(ref, observed);
146
+ if (!metrics || (fallbackSession && countedFallbackSessions.has(fallbackSession))) continue;
147
+ if (fallbackSession) countedFallbackSessions.add(fallbackSession);
148
+ total.reportedAgents++;
149
+ total.tokens += finiteMetric(metrics.tokens);
150
+ total.requests += finiteMetric(metrics.requests);
151
+ total.tools += finiteMetric(metrics.tools);
152
+ total.cost += finiteMetric(metrics.cost);
153
+ if (metrics.durationKind === "active") {
154
+ total.durationMs += finiteMetric(metrics.durationMs);
155
+ total.activeDurationAgents++;
156
+ }
157
+ }
158
+ return { metrics: total, hasFallbackLiveSessions };
159
+ }
160
+
161
+ /** Parent-before-child projection preserving the roster's stable sibling order. */
162
+ export function projectAgentTree(refs: readonly AgentRef[]): AgentTreeProjection {
163
+ const ids = new Set<string>();
164
+ const operationalIndex = new Map<string, number>();
165
+ for (let i = 0; i < refs.length; i++) {
166
+ ids.add(refs[i].id);
167
+ operationalIndex.set(refs[i].id, i);
168
+ }
169
+
170
+ const parentById = new Map<string, string>();
171
+ const children = new Map<string, AgentRef[]>();
172
+ for (const ref of refs) {
173
+ const parent =
174
+ ref.parentId && ref.parentId !== MAIN_AGENT_ID && ids.has(ref.parentId) ? ref.parentId : MAIN_AGENT_ID;
175
+ parentById.set(ref.id, parent);
176
+ const siblings = children.get(parent);
177
+ if (siblings) siblings.push(ref);
178
+ else children.set(parent, [ref]);
179
+ }
180
+
181
+ // A tree group occupies the position of its earliest operational row.
182
+ // Compute subtree minima iteratively so pathological lineage depth remains stack-safe.
183
+ const subtreeOrder = new Map<string, number>();
184
+ const visiting = new Set<string>();
185
+ const ranked = new Set<string>();
186
+ for (const start of refs) {
187
+ if (ranked.has(start.id)) continue;
188
+ const stack: Array<{ ref: AgentRef; expanded: boolean }> = [{ ref: start, expanded: false }];
189
+ while (stack.length > 0) {
190
+ const current = stack.pop();
191
+ if (!current) continue;
192
+ if (current.expanded) {
193
+ let order = operationalIndex.get(current.ref.id) ?? Number.MAX_SAFE_INTEGER;
194
+ for (const child of children.get(current.ref.id) ?? []) {
195
+ order = Math.min(order, subtreeOrder.get(child.id) ?? Number.MAX_SAFE_INTEGER);
196
+ }
197
+ subtreeOrder.set(current.ref.id, order);
198
+ visiting.delete(current.ref.id);
199
+ ranked.add(current.ref.id);
200
+ continue;
201
+ }
202
+ if (ranked.has(current.ref.id) || visiting.has(current.ref.id)) continue;
203
+ visiting.add(current.ref.id);
204
+ stack.push({ ref: current.ref, expanded: true });
205
+ const descendants = children.get(current.ref.id);
206
+ if (!descendants) continue;
207
+ for (let i = descendants.length - 1; i >= 0; i--) {
208
+ const child = descendants[i];
209
+ if (!ranked.has(child.id) && !visiting.has(child.id)) stack.push({ ref: child, expanded: false });
210
+ }
211
+ }
212
+ }
213
+ for (const siblings of children.values()) {
214
+ siblings.sort(
215
+ (a, b) =>
216
+ (subtreeOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (subtreeOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER) ||
217
+ (operationalIndex.get(a.id) ?? Number.MAX_SAFE_INTEGER) -
218
+ (operationalIndex.get(b.id) ?? Number.MAX_SAFE_INTEGER),
219
+ );
220
+ }
221
+
222
+ const lastSiblingById = new Map<string, boolean>();
223
+ for (const siblings of children.values()) {
224
+ for (let i = 0; i < siblings.length; i++) lastSiblingById.set(siblings[i].id, i === siblings.length - 1);
225
+ }
226
+
227
+ const rows: AgentRef[] = [];
228
+ const visited = new Set<string>();
229
+ const depthById = new Map<string, number>();
230
+ const visit = (root: AgentRef, rootDepth: number): void => {
231
+ const stack: Array<{ ref: AgentRef; depth: number }> = [{ ref: root, depth: rootDepth }];
232
+ while (stack.length > 0) {
233
+ const current = stack.pop();
234
+ if (!current || visited.has(current.ref.id)) continue;
235
+ visited.add(current.ref.id);
236
+ depthById.set(current.ref.id, current.depth);
237
+ rows.push(current.ref);
238
+ const descendants = children.get(current.ref.id);
239
+ if (!descendants) continue;
240
+ for (let i = descendants.length - 1; i >= 0; i--)
241
+ stack.push({ ref: descendants[i], depth: current.depth + 1 });
242
+ }
243
+ };
244
+ for (const root of children.get(MAIN_AGENT_ID) ?? []) visit(root, 0);
245
+ // Corrupt persisted parent cycles remain visible as roots instead of disappearing.
246
+ for (const ref of refs) visit(ref, 0);
247
+ return { rows, depthById, parentById, lastSiblingById };
248
+ }