@oh-my-pi/pi-coding-agent 17.2.12 → 17.2.14

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 (143) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/{CHANGELOG-k9ghy5sn.md → CHANGELOG-za420td8.md} +63 -0
  3. package/dist/cli.js +3658 -3627
  4. package/dist/types/advisor/delta-split.d.ts +24 -0
  5. package/dist/types/advisor/runtime.d.ts +2 -2
  6. package/dist/types/async/job-manager.d.ts +8 -1
  7. package/dist/types/cli/update-cli.d.ts +53 -1
  8. package/dist/types/config/keybindings.d.ts +10 -0
  9. package/dist/types/config/model-resolver.d.ts +15 -2
  10. package/dist/types/config/settings-schema.d.ts +14 -0
  11. package/dist/types/discovery/agents-md.d.ts +10 -1
  12. package/dist/types/eval/runner-cache.d.ts +12 -0
  13. package/dist/types/extensibility/extensions/runner.d.ts +12 -3
  14. package/dist/types/extensibility/extensions/types.d.ts +15 -4
  15. package/dist/types/extensibility/plugins/marketplace/manager.d.ts +4 -1
  16. package/dist/types/lib/xai-http.d.ts +0 -1
  17. package/dist/types/mcp/tool-bridge.d.ts +8 -5
  18. package/dist/types/modes/components/agent-hub-renderer.d.ts +6 -1
  19. package/dist/types/modes/components/status-line/types.d.ts +4 -0
  20. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -4
  21. package/dist/types/modes/interactive-mode.d.ts +17 -8
  22. package/dist/types/modes/types.d.ts +7 -10
  23. package/dist/types/modes/utils/hotkeys-markdown.d.ts +1 -1
  24. package/dist/types/session/agent-session-types.d.ts +4 -0
  25. package/dist/types/session/agent-session.d.ts +24 -3
  26. package/dist/types/session/messages.d.ts +20 -0
  27. package/dist/types/session/retry-fallback-chains.d.ts +13 -0
  28. package/dist/types/session/session-advisors.d.ts +1 -1
  29. package/dist/types/session/session-history-format.d.ts +10 -0
  30. package/dist/types/session/session-maintenance.d.ts +1 -1
  31. package/dist/types/session/session-tools.d.ts +35 -5
  32. package/dist/types/session/turn-recovery.d.ts +34 -5
  33. package/dist/types/slash-commands/types.d.ts +5 -1
  34. package/dist/types/task/executor.d.ts +1 -1
  35. package/dist/types/tools/approval.d.ts +7 -0
  36. package/dist/types/tools/builtin-names.d.ts +1 -2
  37. package/dist/types/tools/index.d.ts +1 -0
  38. package/dist/types/tools/think.d.ts +38 -0
  39. package/dist/types/tools/todo.d.ts +14 -15
  40. package/dist/types/tools/write.d.ts +2 -2
  41. package/dist/types/utils/local-date.d.ts +2 -0
  42. package/dist/types/vibe/runtime.d.ts +1 -1
  43. package/dist/types/web/parallel.d.ts +1 -0
  44. package/dist/types/web/search/providers/brave.d.ts +8 -3
  45. package/dist/types/web/search/providers/codex.d.ts +6 -0
  46. package/dist/types/web/search/providers/firecrawl.d.ts +3 -2
  47. package/dist/types/web/search/providers/jina.d.ts +3 -3
  48. package/dist/types/web/search/providers/parallel.d.ts +1 -0
  49. package/dist/types/web/search/providers/perplexity.d.ts +4 -0
  50. package/dist/types/web/search/providers/tinyfish.d.ts +2 -0
  51. package/package.json +13 -13
  52. package/src/advisor/delta-split.ts +98 -0
  53. package/src/advisor/runtime.ts +321 -69
  54. package/src/async/job-manager.ts +14 -3
  55. package/src/cli/gallery-fixtures/agentic.ts +16 -0
  56. package/src/cli/plugin-cli.ts +30 -2
  57. package/src/cli/update-cli.ts +259 -24
  58. package/src/config/keybindings.ts +52 -9
  59. package/src/config/model-resolver.ts +19 -3
  60. package/src/config/settings-schema.ts +16 -0
  61. package/src/cursor.ts +10 -5
  62. package/src/discovery/agents-md.ts +61 -23
  63. package/src/eval/jl/kernel.ts +2 -20
  64. package/src/eval/py/kernel.ts +2 -20
  65. package/src/eval/rb/kernel.ts +2 -20
  66. package/src/eval/runner-cache.ts +41 -0
  67. package/src/exec/non-interactive-env.ts +14 -3
  68. package/src/extensibility/extensions/loader.ts +5 -2
  69. package/src/extensibility/extensions/runner.ts +184 -66
  70. package/src/extensibility/extensions/types.ts +26 -2
  71. package/src/extensibility/extensions/wrapper.ts +13 -7
  72. package/src/extensibility/plugins/marketplace/manager.ts +6 -2
  73. package/src/hindsight/client.ts +1 -1
  74. package/src/lib/xai-http.ts +0 -4
  75. package/src/lsp/client.ts +2 -0
  76. package/src/lsp/servers.ts +1 -1
  77. package/src/mcp/tool-bridge.ts +15 -6
  78. package/src/modes/components/agent-hub-renderer.ts +9 -3
  79. package/src/modes/components/agent-hub.ts +2 -1
  80. package/src/modes/components/status-line/component.ts +58 -6
  81. package/src/modes/components/status-line/segments.ts +12 -1
  82. package/src/modes/components/status-line/types.ts +1 -0
  83. package/src/modes/components/user-message.ts +20 -5
  84. package/src/modes/controllers/event-controller.ts +48 -0
  85. package/src/modes/controllers/extension-ui-controller.ts +14 -7
  86. package/src/modes/controllers/input-controller.ts +25 -6
  87. package/src/modes/controllers/selector-controller.ts +5 -0
  88. package/src/modes/interactive-mode.ts +315 -129
  89. package/src/modes/rpc/rpc-frame.ts +13 -5
  90. package/src/modes/theme/tui-adapters.ts +4 -5
  91. package/src/modes/types.ts +13 -7
  92. package/src/modes/utils/hotkeys-markdown.ts +10 -6
  93. package/src/prompts/system/system-prompt.md +10 -1
  94. package/src/registry/persisted-agents.ts +43 -8
  95. package/src/sdk.ts +146 -9
  96. package/src/session/agent-session-types.ts +4 -0
  97. package/src/session/agent-session.ts +91 -10
  98. package/src/session/messages.ts +98 -28
  99. package/src/session/retry-fallback-chains.ts +14 -0
  100. package/src/session/session-advisors.ts +32 -15
  101. package/src/session/session-history-format.ts +15 -1
  102. package/src/session/session-maintenance.ts +8 -8
  103. package/src/session/session-manager.ts +6 -2
  104. package/src/session/session-tools.ts +355 -183
  105. package/src/session/turn-recovery.ts +225 -47
  106. package/src/slash-commands/builtin-modes.ts +41 -12
  107. package/src/slash-commands/types.ts +5 -1
  108. package/src/task/executor.ts +92 -45
  109. package/src/task/structured-subagent.ts +5 -5
  110. package/src/tools/approval.ts +44 -10
  111. package/src/tools/builtin-names.ts +1 -2
  112. package/src/tools/fetch.ts +21 -2
  113. package/src/tools/image-gen.ts +6 -8
  114. package/src/tools/index.ts +8 -0
  115. package/src/tools/renderers.ts +2 -0
  116. package/src/tools/think.ts +62 -0
  117. package/src/tools/todo.ts +70 -26
  118. package/src/tools/tts.ts +3 -2
  119. package/src/tools/write.ts +7 -3
  120. package/src/utils/local-date.ts +13 -0
  121. package/src/utils/tools-manager.ts +2 -2
  122. package/src/vibe/runtime.ts +22 -14
  123. package/src/web/kagi.ts +91 -34
  124. package/src/web/parallel.ts +11 -2
  125. package/src/web/scrapers/crates-io.ts +2 -2
  126. package/src/web/scrapers/discogs.ts +2 -2
  127. package/src/web/scrapers/docs-rs.ts +2 -2
  128. package/src/web/scrapers/github.ts +2 -2
  129. package/src/web/scrapers/musicbrainz.ts +1 -2
  130. package/src/web/scrapers/pubmed.ts +2 -2
  131. package/src/web/scrapers/sec-edgar.ts +2 -2
  132. package/src/web/search/providers/brave.ts +121 -46
  133. package/src/web/search/providers/codex.ts +88 -12
  134. package/src/web/search/providers/exa.ts +45 -10
  135. package/src/web/search/providers/firecrawl.ts +53 -11
  136. package/src/web/search/providers/gemini.ts +139 -27
  137. package/src/web/search/providers/jina.ts +48 -25
  138. package/src/web/search/providers/parallel.ts +23 -9
  139. package/src/web/search/providers/perplexity.ts +24 -7
  140. package/src/web/search/providers/searxng.ts +77 -1
  141. package/src/web/search/providers/tavily.ts +23 -22
  142. package/src/web/search/providers/tinyfish.ts +44 -10
  143. package/src/web/search/providers/xai.ts +85 -14
package/src/cursor.ts CHANGED
@@ -18,6 +18,7 @@ import type {
18
18
  ToolResultMessage,
19
19
  } from "@oh-my-pi/pi-ai";
20
20
  import {
21
+ omitUndefinedArgs,
21
22
  piEscapeRegexLiteral,
22
23
  piGrepSkip,
23
24
  piJoinPath,
@@ -239,7 +240,11 @@ async function executeTool(
239
240
  return createToolResultMessage(toolCallId, toolName, result, true);
240
241
  }
241
242
 
242
- options.emitEvent?.({ type: "tool_execution_start", toolCallId, toolName, args });
243
+ // Same rule as synthesizeCursorExecToolCall: optional kwargs must be absent,
244
+ // not `undefined`, or ArkType validation rejects the call.
245
+ const toolArgs = omitUndefinedArgs(args);
246
+
247
+ options.emitEvent?.({ type: "tool_execution_start", toolCallId, toolName, args: toolArgs });
243
248
 
244
249
  let result: AgentToolResult<unknown>;
245
250
  let isError = false;
@@ -254,7 +259,7 @@ async function executeTool(
254
259
  type: "tool_execution_update",
255
260
  toolCallId,
256
261
  toolName,
257
- args,
262
+ args: toolArgs,
258
263
  partialResult: sanitizedResult,
259
264
  });
260
265
  }
@@ -263,7 +268,7 @@ async function executeTool(
263
268
  try {
264
269
  result = await tool.execute(
265
270
  toolCallId,
266
- args as Record<string, unknown>,
271
+ toolArgs as Record<string, unknown>,
267
272
  undefined,
268
273
  onUpdate,
269
274
  options.getToolContext?.(),
@@ -509,11 +514,11 @@ export class CursorExecHandlers implements ICursorExecHandlers {
509
514
  }
510
515
 
511
516
  const timeoutSeconds = args.timeout && args.timeout > 0 ? args.timeout : undefined;
512
- const toolArgs: Record<string, unknown> = {
517
+ const toolArgs = omitUndefinedArgs({
513
518
  command: args.command,
514
519
  cwd: args.workingDirectory || undefined,
515
520
  timeout: timeoutSeconds,
516
- };
521
+ });
517
522
 
518
523
  this.options.emitEvent?.({ type: "tool_execution_start", toolCallId, toolName, args: toolArgs });
519
524
 
@@ -15,43 +15,81 @@ import { calculateDepth, createSourceMeta } from "./helpers";
15
15
  const PROVIDER_ID = "agents-md";
16
16
  const DISPLAY_NAME = "AGENTS.md";
17
17
 
18
+ /**
19
+ * Compare paths while tolerating Windows drive casing.
20
+ */
21
+ function samePath(left: string, right: string): boolean {
22
+ const normalizedLeft = path.resolve(left);
23
+ const normalizedRight = path.resolve(right);
24
+ return process.platform === "win32"
25
+ ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
26
+ : normalizedLeft === normalizedRight;
27
+ }
28
+
29
+ /**
30
+ * Return whether `child` is at or below `parent`.
31
+ */
32
+ function isWithin(parent: string, child: string): boolean {
33
+ const normalizedParent = path.resolve(parent);
34
+ const normalizedChild = path.resolve(child);
35
+ const relative = path.relative(
36
+ process.platform === "win32" ? normalizedParent.toLowerCase() : normalizedParent,
37
+ process.platform === "win32" ? normalizedChild.toLowerCase() : normalizedChild,
38
+ );
39
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
40
+ }
41
+
18
42
  /**
19
43
  * Load standalone AGENTS.md files.
44
+ *
45
+ * When a repository is nested below the user's home directory, continue past
46
+ * the Git root to discover workspace-level AGENTS.md files, but stop before
47
+ * loading the home directory's own AGENTS.md as project context.
20
48
  */
21
- async function loadAgentsMd(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
49
+ export async function loadAgentsMd(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
22
50
  const items: ContextFile[] = [];
23
51
  const warnings: string[] = [];
52
+ const home = path.resolve(ctx.home);
53
+ const cwd = path.resolve(ctx.cwd);
54
+ const repoRoot = ctx.repoRoot ? path.resolve(ctx.repoRoot) : null;
55
+ const filesystemRoot = path.parse(cwd).root;
56
+ const cwdIsUnderHome = isWithin(home, cwd);
57
+ const repoIsUnderHome = repoRoot !== null && isWithin(home, repoRoot);
58
+ const scanToHome = repoRoot !== null && cwdIsUnderHome && repoIsUnderHome;
59
+ const boundary = scanToHome ? home : (repoRoot ?? (cwdIsUnderHome ? home : filesystemRoot));
60
+ const includeBoundary = repoRoot === null ? cwdIsUnderHome : !samePath(boundary, home);
61
+ const excludeHome = scanToHome;
24
62
 
25
- // Walk up from cwd looking for AGENTS.md files
26
- let current = ctx.cwd;
27
-
63
+ let current = cwd;
28
64
  while (true) {
29
- const candidate = path.join(current, "AGENTS.md");
30
- const content = await readFile(candidate);
65
+ const atBoundary = samePath(current, boundary);
66
+ const atHome = excludeHome && samePath(current, home);
67
+ if (!(atHome || (atBoundary && !includeBoundary))) {
68
+ const candidate = path.join(current, "AGENTS.md");
69
+ const content = await readFile(candidate);
31
70
 
32
- if (content !== null) {
33
- const parent = path.dirname(candidate);
34
- const baseName = parent.split(path.sep).pop() ?? "";
71
+ if (content !== null) {
72
+ const parent = path.dirname(candidate);
73
+ const baseName = parent.split(path.sep).pop() ?? "";
35
74
 
36
- if (!baseName.startsWith(".")) {
37
- const fileDir = path.dirname(candidate);
38
- const calculatedDepth = calculateDepth(ctx.cwd, fileDir, path.sep);
75
+ if (!baseName.startsWith(".")) {
76
+ const fileDir = path.dirname(candidate);
77
+ const calculatedDepth = calculateDepth(cwd, fileDir, path.sep);
39
78
 
40
- items.push({
41
- path: candidate,
42
- content,
43
- level: "project",
44
- depth: calculatedDepth,
45
- _source: createSourceMeta(PROVIDER_ID, candidate, "project"),
46
- });
79
+ items.push({
80
+ path: candidate,
81
+ content,
82
+ level: "project",
83
+ depth: calculatedDepth,
84
+ _source: createSourceMeta(PROVIDER_ID, candidate, "project"),
85
+ });
86
+ }
47
87
  }
48
88
  }
89
+ if (atBoundary) break;
49
90
 
50
- if (current === (ctx.repoRoot ?? ctx.home)) break; // scanned repo root or home, stop
51
-
52
- // Move to parent directory
53
91
  const parent = path.dirname(current);
54
- if (parent === current) break; // Reached filesystem root
92
+ if (parent === current) break;
55
93
  current = parent;
56
94
  }
57
95
 
@@ -5,8 +5,6 @@
5
5
  * Ruby runners via BaseKernel; this module supplies the Julia binary, runner
6
6
  * script, and the runner's TSV/Base64 wire protocol.
7
7
  */
8
- import * as fs from "node:fs";
9
- import * as os from "node:os";
10
8
  import * as path from "node:path";
11
9
  import { $flag, Snowflake } from "@oh-my-pi/pi-utils";
12
10
  import { $ } from "bun";
@@ -14,6 +12,7 @@ import { Settings } from "../../config/settings";
14
12
  import { BaseKernel, getRemainingTimeMs, type KernelStartOptions } from "../kernel-base";
15
13
  import type { KernelDisplayOutput } from "../py/display";
16
14
  import { hostHasInheritableConsole, shouldDetachKernel, shouldHideKernelWindow } from "../py/spawn-options";
15
+ import { stageRunnerScript } from "../runner-cache";
17
16
  import { JULIA_PRELUDE } from "./prelude";
18
17
  import RUNNER_SCRIPT from "./runner.jl" with { type: "text" };
19
18
  import {
@@ -30,23 +29,6 @@ export type { KernelDisplayOutput };
30
29
 
31
30
  const TRACE_IPC = $flag("PI_JULIA_IPC_TRACE");
32
31
 
33
- // Cache the runner script on disk so the subprocess loads it normally. Cached
34
- // per script hash so installs don't race across versions.
35
- const RUNNER_CACHE_DIR = path.join(os.tmpdir(), "omp-julia-runner");
36
- let RUNNER_SCRIPT_PATH: string | null = null;
37
-
38
- async function ensureRunnerScript(): Promise<string> {
39
- if (RUNNER_SCRIPT_PATH) return RUNNER_SCRIPT_PATH;
40
- await fs.promises.mkdir(RUNNER_CACHE_DIR, { recursive: true });
41
- const hash = Bun.hash(RUNNER_SCRIPT).toString(36);
42
- const target = path.join(RUNNER_CACHE_DIR, `runner-${hash}.jl`);
43
- if (!fs.existsSync(target)) {
44
- await Bun.write(target, RUNNER_SCRIPT);
45
- }
46
- RUNNER_SCRIPT_PATH = target;
47
- return target;
48
- }
49
-
50
32
  const SHUTDOWN_GRACE_MS = 1_000;
51
33
  const STARTUP_TIMEOUT_MS = 15_000; // Julia compile/warmup can be slightly slower
52
34
  const INTERRUPT_ESCALATION_MS = 5_000;
@@ -180,7 +162,7 @@ export class JuliaKernel extends BaseKernel<KernelExecuteOptions> {
180
162
  if (typeof value === "string") spawnEnv[key] = value;
181
163
  }
182
164
 
183
- const scriptPath = await ensureRunnerScript();
165
+ const scriptPath = await stageRunnerScript("omp-julia-runner", "jl", RUNNER_SCRIPT);
184
166
  const kernel = new JuliaKernel(Snowflake.next());
185
167
 
186
168
  const proc = Bun.spawn(
@@ -7,13 +7,12 @@
7
7
  * code. Shutdown writes `{"type":"exit"}` and escalates to SIGTERM/SIGKILL on
8
8
  * timeout.
9
9
  */
10
- import * as fs from "node:fs";
11
- import * as os from "node:os";
12
10
  import * as path from "node:path";
13
11
  import { $flag, isBunTestRuntime, logger, Snowflake } from "@oh-my-pi/pi-utils";
14
12
  import { $ } from "bun";
15
13
  import { Settings } from "../../config/settings";
16
14
  import { BaseKernel, getRemainingTimeMs, type KernelStartOptions } from "../kernel-base";
15
+ import { stageRunnerScript } from "../runner-cache";
17
16
  import { PYTHON_PRELUDE } from "./prelude";
18
17
  import RUNNER_SCRIPT from "./runner.py" with { type: "text" };
19
18
  import {
@@ -38,23 +37,6 @@ export { renderKernelDisplay } from "./display";
38
37
 
39
38
  const TRACE_IPC = $flag("PI_PYTHON_IPC_TRACE");
40
39
 
41
- // Cache the runner script on disk so the subprocess loads it normally. Cached
42
- // per script hash so installs don't race across versions.
43
- const RUNNER_CACHE_DIR = path.join(os.tmpdir(), "omp-python-runner");
44
- let RUNNER_SCRIPT_PATH: string | null = null;
45
-
46
- async function ensureRunnerScript(): Promise<string> {
47
- if (RUNNER_SCRIPT_PATH) return RUNNER_SCRIPT_PATH;
48
- await fs.promises.mkdir(RUNNER_CACHE_DIR, { recursive: true });
49
- const hash = Bun.hash(RUNNER_SCRIPT).toString(36);
50
- const target = path.join(RUNNER_CACHE_DIR, `runner-${hash}.py`);
51
- if (!fs.existsSync(target)) {
52
- await Bun.write(target, RUNNER_SCRIPT);
53
- }
54
- RUNNER_SCRIPT_PATH = target;
55
- return target;
56
- }
57
-
58
40
  const SHUTDOWN_GRACE_MS = 1_000;
59
41
  const STARTUP_TIMEOUT_MS = 10_000;
60
42
  // How long to wait after SIGINT for the runner to emit `done`. If the cell is
@@ -189,7 +171,7 @@ export class PythonKernel extends BaseKernel {
189
171
  spawnEnv.PYTHONUNBUFFERED = "1";
190
172
  spawnEnv.PYTHONIOENCODING = "utf-8";
191
173
 
192
- const scriptPath = await ensureRunnerScript();
174
+ const scriptPath = await stageRunnerScript("omp-python-runner", "py", RUNNER_SCRIPT);
193
175
  const kernel = new PythonKernel(Snowflake.next());
194
176
 
195
177
  const proc = Bun.spawn([runtime.pythonPath, "-u", scriptPath], {
@@ -8,8 +8,6 @@
8
8
  * (eval/py/kernel.ts); the IPC loop, lifecycle, and display rendering are shared
9
9
  * with it via BaseKernel.
10
10
  */
11
- import * as fs from "node:fs";
12
- import * as os from "node:os";
13
11
  import * as path from "node:path";
14
12
  import { $flag, isBunTestRuntime, logger, Snowflake } from "@oh-my-pi/pi-utils";
15
13
  import { $ } from "bun";
@@ -17,6 +15,7 @@ import { Settings } from "../../config/settings";
17
15
  import { BaseKernel, getRemainingTimeMs, type KernelRuntimeEnv, type KernelStartOptions } from "../kernel-base";
18
16
  import type { KernelDisplayOutput } from "../py/display";
19
17
  import { hostHasInheritableConsole, shouldDetachKernel, shouldHideKernelWindow } from "../py/spawn-options";
18
+ import { stageRunnerScript } from "../runner-cache";
20
19
  import { RUBY_PRELUDE } from "./prelude";
21
20
  import RUNNER_SCRIPT from "./runner.rb" with { type: "text" };
22
21
  import {
@@ -33,23 +32,6 @@ export { renderKernelDisplay } from "../py/display";
33
32
 
34
33
  const TRACE_IPC = $flag("PI_RUBY_IPC_TRACE");
35
34
 
36
- // Cache the runner script on disk so the subprocess loads it normally. Cached
37
- // per script hash so installs don't race across versions.
38
- const RUNNER_CACHE_DIR = path.join(os.tmpdir(), "omp-ruby-runner");
39
- let RUNNER_SCRIPT_PATH: string | null = null;
40
-
41
- async function ensureRunnerScript(): Promise<string> {
42
- if (RUNNER_SCRIPT_PATH) return RUNNER_SCRIPT_PATH;
43
- await fs.promises.mkdir(RUNNER_CACHE_DIR, { recursive: true });
44
- const hash = Bun.hash(RUNNER_SCRIPT).toString(36);
45
- const target = path.join(RUNNER_CACHE_DIR, `runner-${hash}.rb`);
46
- if (!fs.existsSync(target)) {
47
- await Bun.write(target, RUNNER_SCRIPT);
48
- }
49
- RUNNER_SCRIPT_PATH = target;
50
- return target;
51
- }
52
-
53
35
  const SHUTDOWN_GRACE_MS = 1_000;
54
36
  const STARTUP_TIMEOUT_MS = 10_000;
55
37
  // How long to wait after SIGINT for the runner to emit `done` before escalating
@@ -181,7 +163,7 @@ export class RubyKernel extends BaseKernel<KernelExecuteOptions> {
181
163
  if (typeof value === "string") spawnEnv[key] = value;
182
164
  }
183
165
 
184
- const scriptPath = await ensureRunnerScript();
166
+ const scriptPath = await stageRunnerScript("omp-ruby-runner", "rb", RUNNER_SCRIPT);
185
167
  const kernel = new RubyKernel(Snowflake.next());
186
168
 
187
169
  const proc = Bun.spawn([runtime.rubyPath, scriptPath], {
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Shared on-disk staging for subprocess kernel runner scripts.
3
+ *
4
+ * Each language kernel (Python/Julia/Ruby) ships its runner as a compiled-in
5
+ * text asset, then stages it under `os.tmpdir()` so the interpreter can load it
6
+ * as a normal file. Staging is cached per language directory so repeated kernel
7
+ * starts within a process avoid redundant writes.
8
+ */
9
+ import * as fs from "node:fs";
10
+ import * as os from "node:os";
11
+ import * as path from "node:path";
12
+
13
+ // Memoized staged path per cache directory. The value is re-validated on every
14
+ // call: a tmpdir sweep (e.g. macOS `periodic daily clean_tmps`) or any external
15
+ // clear must self-heal within a long-lived process, not only across restarts.
16
+ const stagedPaths = new Map<string, string>();
17
+
18
+ /**
19
+ * Stage `script` under `os.tmpdir()/<dirName>` and return the runner path.
20
+ *
21
+ * The staged path is memoized per `dirName` but re-checked with `fs.existsSync`
22
+ * before reuse, so a runner deleted mid-session is re-written on the next call
23
+ * instead of handing back a path to a missing file (issue #8140).
24
+ *
25
+ * @param dirName Cache subdirectory under the OS temp dir (unique per language).
26
+ * @param ext Runner file extension without the dot (e.g. `py`, `jl`, `rb`).
27
+ * @param script Runner source, hashed to key the cached file per version.
28
+ */
29
+ export async function stageRunnerScript(dirName: string, ext: string, script: string): Promise<string> {
30
+ const memoized = stagedPaths.get(dirName);
31
+ if (memoized && fs.existsSync(memoized)) return memoized;
32
+ const dir = path.join(os.tmpdir(), dirName);
33
+ await fs.promises.mkdir(dir, { recursive: true });
34
+ const hash = Bun.hash(script).toString(36);
35
+ const target = path.join(dir, `runner-${hash}.${ext}`);
36
+ if (!fs.existsSync(target)) {
37
+ await Bun.write(target, script);
38
+ }
39
+ stagedPaths.set(dirName, target);
40
+ return target;
41
+ }
@@ -23,7 +23,7 @@ export const NON_INTERACTIVE_ENV: Readonly<Record<string, string>> = {
23
23
  EDITOR: "true",
24
24
  GIT_TERMINAL_PROMPT: "0",
25
25
  SSH_ASKPASS: "/usr/bin/false",
26
- CI: "1",
26
+ CI: "true",
27
27
  AGENT: "1",
28
28
  // Package manager defaults for unattended execution.
29
29
  npm_config_yes: "true",
@@ -96,17 +96,28 @@ function hasEnvGroupValue(
96
96
  return false;
97
97
  }
98
98
 
99
+ /** Copy of the base env with `CI` removed, for the `PI_BASH_NO_CI` opt-out. */
100
+ function withoutCI(env: Readonly<Record<string, string>>): Record<string, string> {
101
+ const { CI: _ci, ...rest } = env;
102
+ return rest;
103
+ }
104
+
99
105
  /** Builds the per-command environment for non-interactive child processes. */
100
106
  export function buildNonInteractiveEnv(
101
107
  overrides?: Record<string, string>,
102
108
  baseEnv: Record<string, string | undefined> = Bun.env,
103
109
  platform: NodeJS.Platform = process.platform,
104
110
  ): Record<string, string> {
111
+ // `PI_BASH_NO_CI` (and its legacy alias) opts out of the automatic `CI=true`
112
+ // injection. Mirrors the session-env gate in `procmgr.ts` so the opt-out
113
+ // reaches the per-command env, which otherwise overrides the session value.
114
+ const base =
115
+ baseEnv.PI_BASH_NO_CI || baseEnv.CLAUDE_BASH_NO_CI ? withoutCI(NON_INTERACTIVE_ENV) : NON_INTERACTIVE_ENV;
105
116
  if (platform !== "win32") {
106
- return overrides ? { ...NON_INTERACTIVE_ENV, ...overrides } : NON_INTERACTIVE_ENV;
117
+ return overrides ? { ...base, ...overrides } : base;
107
118
  }
108
119
 
109
- const env: Record<string, string> = { ...NON_INTERACTIVE_ENV };
120
+ const env: Record<string, string> = { ...base };
110
121
  for (const group of WINDOWS_UTF8_ENV_DEFAULT_GROUPS) {
111
122
  if (hasEnvGroupValue(baseEnv, group, platform) || hasEnvGroupValue(overrides, group, platform)) {
112
123
  continue;
@@ -175,10 +175,12 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
175
175
  }
176
176
 
177
177
  registerTool<TParams extends TSchema = TSchema, TDetails = unknown>(tool: ToolDefinition<TParams, TDetails>): void {
178
- this.extension.tools.set(tool.name, {
178
+ const registered = {
179
179
  definition: tool,
180
180
  extensionPath: this.extension.path,
181
- });
181
+ };
182
+ this.extension.tools.set(tool.name, registered);
183
+ for (const listener of this.extension.toolRegistrationListeners ?? []) listener(tool.name);
182
184
  }
183
185
 
184
186
  registerCommand(
@@ -316,6 +318,7 @@ function createExtension(extensionPath: string, resolvedPath: string): Extension
316
318
  resolvedPath,
317
319
  handlers: new Map(),
318
320
  tools: new Map(),
321
+ toolRegistrationListeners: new Set(),
319
322
  assistantThinkingRenderers: [],
320
323
  messageRenderers: new Map(),
321
324
  commands: new Map(),