@oh-my-pi/pi-coding-agent 17.0.8 → 17.1.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 (340) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.md +1 -1
  3. package/dist/cli.js +5495 -5021
  4. package/dist/types/async/job-manager.d.ts +37 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/bench-cli.d.ts +41 -0
  7. package/dist/types/commands/bench.d.ts +15 -3
  8. package/dist/types/commands/launch.d.ts +4 -0
  9. package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
  10. package/dist/types/config/model-registry.d.ts +30 -1
  11. package/dist/types/config/models-config-schema.d.ts +18 -6
  12. package/dist/types/config/models-config.d.ts +14 -5
  13. package/dist/types/config/provider-globals.d.ts +2 -2
  14. package/dist/types/config/service-tier.d.ts +5 -1
  15. package/dist/types/config/settings-schema.d.ts +350 -50
  16. package/dist/types/config/settings.d.ts +7 -0
  17. package/dist/types/dap/client.d.ts +20 -0
  18. package/dist/types/exec/bash-executor.d.ts +38 -0
  19. package/dist/types/exec/direnv.d.ts +33 -0
  20. package/dist/types/export/html/args.d.ts +15 -0
  21. package/dist/types/export/html/index.d.ts +11 -26
  22. package/dist/types/export/html/web-palette.d.ts +55 -116
  23. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  24. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  25. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
  26. package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
  27. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +14 -1
  28. package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
  29. package/dist/types/live/controller.d.ts +40 -0
  30. package/dist/types/live/protocol.d.ts +85 -0
  31. package/dist/types/live/protocol.test.d.ts +1 -0
  32. package/dist/types/live/transport.d.ts +48 -0
  33. package/dist/types/live/visualizer.d.ts +33 -0
  34. package/dist/types/lsp/client.d.ts +18 -1
  35. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  36. package/dist/types/mcp/manager.d.ts +6 -2
  37. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  38. package/dist/types/mcp/render.d.ts +2 -2
  39. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  40. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  41. package/dist/types/mcp/tool-bridge.d.ts +7 -3
  42. package/dist/types/mcp/types.d.ts +6 -0
  43. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  44. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  45. package/dist/types/modes/components/oauth-selector.d.ts +8 -0
  46. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  47. package/dist/types/modes/components/settings-defs.d.ts +8 -1
  48. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  49. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  50. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  51. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  52. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  53. package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
  54. package/dist/types/modes/interactive-mode.d.ts +5 -0
  55. package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
  56. package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
  57. package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
  58. package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
  59. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
  60. package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
  61. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
  62. package/dist/types/modes/theme/theme.d.ts +1 -1
  63. package/dist/types/modes/types.d.ts +3 -1
  64. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  65. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  66. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  67. package/dist/types/registry/agent-registry.d.ts +11 -4
  68. package/dist/types/sdk.d.ts +15 -1
  69. package/dist/types/secrets/index.d.ts +15 -2
  70. package/dist/types/secrets/obfuscator.d.ts +113 -15
  71. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  72. package/dist/types/session/agent-session-events.d.ts +84 -0
  73. package/dist/types/session/agent-session-types.d.ts +314 -0
  74. package/dist/types/session/agent-session.d.ts +120 -750
  75. package/dist/types/session/async-job-delivery.d.ts +28 -0
  76. package/dist/types/session/bash-runner.d.ts +75 -0
  77. package/dist/types/session/blob-store.d.ts +11 -1
  78. package/dist/types/session/blob-store.test.d.ts +1 -0
  79. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  80. package/dist/types/session/eval-runner.d.ts +49 -0
  81. package/dist/types/session/irc-bridge.d.ts +46 -0
  82. package/dist/types/session/messages.d.ts +22 -0
  83. package/dist/types/session/model-controls.d.ts +180 -0
  84. package/dist/types/session/prewalk.d.ts +46 -0
  85. package/dist/types/session/queued-messages.d.ts +22 -0
  86. package/dist/types/session/redis-session-storage.d.ts +1 -0
  87. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  88. package/dist/types/session/role-models.d.ts +15 -0
  89. package/dist/types/session/session-advisors.d.ts +228 -0
  90. package/dist/types/session/session-entries.d.ts +8 -0
  91. package/dist/types/session/session-handoff.d.ts +70 -0
  92. package/dist/types/session/session-history-format.d.ts +6 -1
  93. package/dist/types/session/session-loader.d.ts +6 -4
  94. package/dist/types/session/session-maintenance.d.ts +246 -0
  95. package/dist/types/session/session-manager.d.ts +41 -0
  96. package/dist/types/session/session-memory.d.ts +56 -0
  97. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  98. package/dist/types/session/session-stats.d.ts +48 -0
  99. package/dist/types/session/session-tools.d.ts +143 -0
  100. package/dist/types/session/session-workspace.d.ts +32 -0
  101. package/dist/types/session/stream-guards.d.ts +44 -0
  102. package/dist/types/session/todo-tracker.d.ts +56 -0
  103. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  104. package/dist/types/session/turn-recovery.d.ts +225 -0
  105. package/dist/types/system-prompt.d.ts +2 -0
  106. package/dist/types/task/executor.d.ts +4 -1
  107. package/dist/types/task/output-manager.d.ts +3 -1
  108. package/dist/types/task/types.d.ts +14 -0
  109. package/dist/types/tools/approval.d.ts +1 -0
  110. package/dist/types/tools/browser/launch.d.ts +5 -0
  111. package/dist/types/tools/hub/jobs.d.ts +1 -1
  112. package/dist/types/tools/image-gen.d.ts +4 -4
  113. package/dist/types/tools/image-providers.d.ts +38 -0
  114. package/dist/types/tools/index.d.ts +8 -0
  115. package/dist/types/tools/path-utils.d.ts +8 -0
  116. package/dist/types/tools/report-tool-issue.d.ts +24 -9
  117. package/dist/types/tools/todo.d.ts +10 -4
  118. package/dist/types/tools/xdev.d.ts +5 -1
  119. package/dist/types/tts/streaming-player.d.ts +14 -0
  120. package/dist/types/tts/vocalizer.d.ts +5 -0
  121. package/dist/types/utils/jj.d.ts +29 -0
  122. package/dist/types/utils/lang-from-path.d.ts +1 -0
  123. package/dist/types/utils/title-generator.d.ts +30 -0
  124. package/dist/types/vibe/runtime.d.ts +36 -8
  125. package/dist/types/web/search/index.d.ts +1 -1
  126. package/dist/types/web/search/provider.d.ts +15 -5
  127. package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
  128. package/dist/types/web/search/types.d.ts +95 -1
  129. package/package.json +13 -12
  130. package/scripts/generate-share-viewer.ts +4 -6
  131. package/scripts/legacy-pi-virtual-module.ts +1 -1
  132. package/src/advisor/__tests__/advisor.test.ts +643 -0
  133. package/src/advisor/runtime.ts +144 -43
  134. package/src/async/job-manager.ts +90 -2
  135. package/src/cli/args.ts +2 -0
  136. package/src/cli/auth-gateway-cli.ts +18 -3
  137. package/src/cli/bench-cli.ts +395 -29
  138. package/src/cli/flag-tables.ts +3 -0
  139. package/src/cli/gc-cli.ts +2 -2
  140. package/src/cli/grievances-cli.ts +2 -2
  141. package/src/cli/usage-cli.ts +11 -2
  142. package/src/collab/host.ts +3 -2
  143. package/src/commands/bench.ts +18 -4
  144. package/src/commands/launch.ts +4 -0
  145. package/src/config/__tests__/model-registry.test.ts +147 -0
  146. package/src/config/model-registry.ts +50 -2
  147. package/src/config/model-resolver.ts +0 -1
  148. package/src/config/models-config-schema.ts +15 -4
  149. package/src/config/provider-globals.ts +9 -9
  150. package/src/config/service-tier.ts +26 -1
  151. package/src/config/settings-schema.ts +250 -54
  152. package/src/config/settings.ts +81 -2
  153. package/src/dap/client.ts +37 -4
  154. package/src/edit/index.ts +49 -12
  155. package/src/eval/py/executor.ts +103 -33
  156. package/src/exec/bash-executor.ts +89 -6
  157. package/src/exec/direnv.ts +145 -0
  158. package/src/export/html/args.ts +20 -0
  159. package/src/export/html/index.ts +45 -47
  160. package/src/export/html/template.css +19 -1
  161. package/src/export/html/template.html +6 -0
  162. package/src/export/html/template.js +21 -0
  163. package/src/export/html/tool-views.generated.js +31 -31
  164. package/src/export/html/web-palette.ts +116 -132
  165. package/src/export/share.ts +249 -49
  166. package/src/extensibility/extensions/loader.ts +29 -1
  167. package/src/extensibility/extensions/runner.ts +6 -0
  168. package/src/extensibility/extensions/types.ts +33 -2
  169. package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
  170. package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
  171. package/src/extensibility/plugins/legacy-pi-compat.ts +870 -311
  172. package/src/hindsight/state.ts +64 -6
  173. package/src/internal-urls/local-protocol.ts +2 -1
  174. package/src/internal-urls/memory-protocol.ts +2 -2
  175. package/src/internal-urls/registry-helpers.ts +40 -0
  176. package/src/internal-urls/skill-protocol.ts +2 -2
  177. package/src/internal-urls/ssh-protocol.ts +2 -1
  178. package/src/internal-urls/vault-protocol.ts +2 -1
  179. package/src/live/audio-worklet.txt +59 -0
  180. package/src/live/browser-runtime.txt +221 -0
  181. package/src/live/controller.ts +464 -0
  182. package/src/live/prompts/agent-final-message.md +3 -0
  183. package/src/live/prompts/live-instructions.md +23 -0
  184. package/src/live/protocol.test.ts +140 -0
  185. package/src/live/protocol.ts +233 -0
  186. package/src/live/transport.ts +490 -0
  187. package/src/live/visualizer.ts +234 -0
  188. package/src/lsp/client.ts +52 -9
  189. package/src/lsp/clients/biome-client.ts +3 -4
  190. package/src/lsp/index.ts +44 -11
  191. package/src/main.ts +9 -1
  192. package/src/mcp/config-writer.test.ts +43 -0
  193. package/src/mcp/config-writer.ts +109 -82
  194. package/src/mcp/manager.ts +39 -8
  195. package/src/mcp/oauth-discovery.ts +10 -2
  196. package/src/mcp/render.ts +94 -35
  197. package/src/mcp/smithery-auth.test.ts +29 -0
  198. package/src/mcp/smithery-auth.ts +3 -2
  199. package/src/mcp/tool-bridge.ts +107 -11
  200. package/src/mcp/types.ts +7 -0
  201. package/src/memories/index.ts +40 -20
  202. package/src/memory-backend/tool-names.ts +2 -0
  203. package/src/mnemopi/backend.ts +24 -7
  204. package/src/modes/acp/acp-agent.ts +56 -10
  205. package/src/modes/acp/acp-event-mapper.ts +8 -1
  206. package/src/modes/components/agent-hub.ts +27 -10
  207. package/src/modes/components/login-dialog.ts +14 -4
  208. package/src/modes/components/oauth-selector.ts +24 -7
  209. package/src/modes/components/plan-review-overlay.ts +350 -35
  210. package/src/modes/components/settings-defs.ts +28 -3
  211. package/src/modes/components/settings-selector.ts +178 -6
  212. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  213. package/src/modes/components/status-line/component.ts +123 -2
  214. package/src/modes/components/tool-execution.test.ts +63 -2
  215. package/src/modes/components/tool-execution.ts +11 -2
  216. package/src/modes/controllers/command-controller.ts +70 -22
  217. package/src/modes/controllers/event-controller.ts +111 -16
  218. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  219. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  220. package/src/modes/controllers/live-command-controller.ts +178 -0
  221. package/src/modes/controllers/mcp-command-controller.ts +80 -41
  222. package/src/modes/controllers/omfg-controller.ts +44 -40
  223. package/src/modes/controllers/selector-controller.ts +18 -10
  224. package/src/modes/controllers/session-focus-controller.ts +6 -1
  225. package/src/modes/interactive-mode.ts +160 -38
  226. package/src/modes/rpc/rpc-client.ts +94 -3
  227. package/src/modes/rpc/rpc-frame.ts +164 -4
  228. package/src/modes/rpc/rpc-messages.ts +127 -0
  229. package/src/modes/rpc/rpc-mode.ts +79 -7
  230. package/src/modes/rpc/rpc-types.ts +34 -2
  231. package/src/modes/runtime-init.ts +2 -0
  232. package/src/modes/setup-wizard/scenes/model.ts +5 -7
  233. package/src/modes/setup-wizard/scenes/providers.ts +3 -2
  234. package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
  235. package/src/modes/setup-wizard/scenes/theme.ts +13 -3
  236. package/src/modes/setup-wizard/scenes/types.ts +9 -1
  237. package/src/modes/setup-wizard/scenes/web-search.ts +23 -10
  238. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  239. package/src/modes/theme/theme.ts +1 -1
  240. package/src/modes/types.ts +3 -1
  241. package/src/modes/utils/context-usage.ts +23 -7
  242. package/src/modes/utils/ui-helpers.ts +10 -5
  243. package/src/plan-mode/plan-files.ts +40 -0
  244. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  245. package/src/prompts/bench/cache-prefix.md +3 -0
  246. package/src/prompts/bench/cache-suffix.md +1 -0
  247. package/src/prompts/goals/guided-goal-system.md +24 -3
  248. package/src/prompts/system/custom-system-prompt.md +1 -1
  249. package/src/prompts/system/project-prompt.md +8 -1
  250. package/src/prompts/system/subagent-async-pending.md +6 -0
  251. package/src/prompts/system/system-prompt.md +1 -1
  252. package/src/prompts/system/workflow-notice.md +48 -40
  253. package/src/prompts/system/xdev-mount-notice.md +4 -0
  254. package/src/prompts/tools/task.md +12 -2
  255. package/src/prompts/tools/todo.md +3 -1
  256. package/src/registry/agent-lifecycle.ts +116 -46
  257. package/src/registry/agent-registry.ts +35 -8
  258. package/src/registry/persisted-agents.ts +26 -2
  259. package/src/sdk.ts +255 -139
  260. package/src/secrets/index.ts +127 -4
  261. package/src/secrets/obfuscator.ts +2328 -105
  262. package/src/session/acp-permission-gate.ts +165 -0
  263. package/src/session/agent-session-error-log.test.ts +1 -1
  264. package/src/session/agent-session-events.ts +66 -0
  265. package/src/session/agent-session-types.ts +334 -0
  266. package/src/session/agent-session.ts +1910 -12213
  267. package/src/session/artifacts.ts +6 -5
  268. package/src/session/async-job-delivery.ts +74 -0
  269. package/src/session/bash-runner.ts +326 -0
  270. package/src/session/blob-store.test.ts +56 -0
  271. package/src/session/blob-store.ts +18 -2
  272. package/src/session/checkpoint-entries.ts +81 -0
  273. package/src/session/eval-runner.ts +212 -0
  274. package/src/session/indexed-session-storage.ts +9 -2
  275. package/src/session/irc-bridge.ts +203 -0
  276. package/src/session/messages.ts +263 -1
  277. package/src/session/model-controls.ts +714 -0
  278. package/src/session/prewalk.ts +252 -0
  279. package/src/session/queued-messages.ts +93 -0
  280. package/src/session/redis-session-storage.ts +51 -11
  281. package/src/session/retry-fallback-chains.ts +455 -0
  282. package/src/session/role-models.ts +85 -0
  283. package/src/session/session-advisors.ts +1679 -0
  284. package/src/session/session-context.test.ts +224 -1
  285. package/src/session/session-context.ts +47 -5
  286. package/src/session/session-entries.ts +8 -0
  287. package/src/session/session-handoff.ts +308 -0
  288. package/src/session/session-history-format.ts +20 -9
  289. package/src/session/session-loader.ts +10 -51
  290. package/src/session/session-maintenance.ts +2983 -0
  291. package/src/session/session-manager.ts +390 -30
  292. package/src/session/session-memory.ts +222 -0
  293. package/src/session/session-provider-boundary.ts +307 -0
  294. package/src/session/session-stats.ts +293 -0
  295. package/src/session/session-tools.ts +943 -0
  296. package/src/session/session-workspace.ts +53 -0
  297. package/src/session/stream-guards.ts +417 -0
  298. package/src/session/todo-tracker.ts +380 -0
  299. package/src/session/ttsr-coordinator.ts +496 -0
  300. package/src/session/turn-recovery.ts +1629 -0
  301. package/src/slash-commands/builtin-registry.ts +92 -11
  302. package/src/slash-commands/helpers/usage-report.ts +25 -4
  303. package/src/system-prompt.ts +41 -17
  304. package/src/task/executor.ts +288 -70
  305. package/src/task/index.ts +148 -111
  306. package/src/task/output-manager.ts +25 -3
  307. package/src/task/persisted-revive.ts +4 -3
  308. package/src/task/structured-subagent.ts +4 -1
  309. package/src/task/types.ts +16 -0
  310. package/src/tools/approval.ts +55 -10
  311. package/src/tools/bash-interactive.ts +26 -0
  312. package/src/tools/bash-skill-urls.ts +28 -2
  313. package/src/tools/bash.ts +339 -123
  314. package/src/tools/browser/launch.ts +9 -1
  315. package/src/tools/browser/tab-supervisor.ts +8 -5
  316. package/src/tools/browser.ts +94 -47
  317. package/src/tools/hub/index.ts +1 -1
  318. package/src/tools/hub/jobs.ts +67 -8
  319. package/src/tools/image-gen.ts +20 -28
  320. package/src/tools/image-providers.ts +50 -0
  321. package/src/tools/index.ts +12 -0
  322. package/src/tools/path-utils.ts +49 -2
  323. package/src/tools/read.ts +156 -88
  324. package/src/tools/renderers.ts +7 -1
  325. package/src/tools/report-tool-issue.ts +79 -28
  326. package/src/tools/todo.ts +101 -11
  327. package/src/tools/vibe.ts +1 -2
  328. package/src/tools/write.ts +51 -1
  329. package/src/tools/xdev.ts +67 -3
  330. package/src/tts/streaming-player.ts +62 -4
  331. package/src/tts/vocalizer.ts +18 -1
  332. package/src/utils/jj.ts +125 -4
  333. package/src/utils/lang-from-path.ts +7 -0
  334. package/src/utils/title-generator.ts +137 -1
  335. package/src/vibe/runtime.ts +852 -72
  336. package/src/web/search/index.ts +3 -6
  337. package/src/web/search/provider.ts +31 -16
  338. package/src/web/search/providers/firecrawl.ts +46 -13
  339. package/src/web/search/providers/xai.ts +7 -1
  340. package/src/web/search/types.ts +8 -1
package/src/dap/client.ts CHANGED
@@ -260,7 +260,7 @@ export class DapClient {
260
260
  // socket connect fails, we must not leak the detached adapter process.
261
261
  try {
262
262
  await waitForCondition(() => isUnixSocketReady(socketPath), timeoutMs, proc);
263
- const { readable, writeSink, socket } = await connectSocket({ unix: socketPath });
263
+ const { readable, writeSink, socket } = await connectSocket({ unix: socketPath }, timeoutMs);
264
264
  const client = new DapClient(adapter, cwd, proc, { readable, writeSink, socket });
265
265
  proc.exited.then(() => client.#handleProcessExit());
266
266
  void client.#startMessageReader();
@@ -924,10 +924,20 @@ function socketToSink(socket: Bun.Socket<undefined>): DapWriteSink {
924
924
  };
925
925
  }
926
926
 
927
- /** Connect to a unix domain socket and return DAP transport streams. */
928
- async function connectSocket(options: { unix: string }): Promise<SocketTransport> {
929
- const { promise, resolve } = Promise.withResolvers<SocketTransport>();
927
+ /**
928
+ * Connect to a unix domain socket and return DAP transport streams.
929
+ *
930
+ * Rejects (rather than hanging) when the connect fails — a stat-ready but dead
931
+ * socket returns ECONNREFUSED, a socket removed between the readiness stat and
932
+ * the connect returns ENOENT, a permission mismatch returns EACCES — and when
933
+ * neither `open` nor an error arrives within `timeoutMs` (e.g. a TOCTOU stall).
934
+ * `#spawnSocketUnix`'s catch then kills the detached adapter instead of leaking
935
+ * it. Exported so tests can drive the reject path deterministically.
936
+ */
937
+ export async function connectSocket(options: { unix: string }, timeoutMs: number): Promise<SocketTransport> {
938
+ const { promise, resolve, reject } = Promise.withResolvers<SocketTransport>();
930
939
  let streamController: ReadableStreamDefaultController<Uint8Array>;
940
+ let opened = false;
931
941
 
932
942
  const readable = new ReadableStream<Uint8Array>({
933
943
  start(controller) {
@@ -935,10 +945,21 @@ async function connectSocket(options: { unix: string }): Promise<SocketTransport
935
945
  },
936
946
  });
937
947
 
948
+ const timer = setTimeout(() => {
949
+ reject(new Error(`Timed out connecting to unix socket ${options.unix} after ${timeoutMs}ms`));
950
+ }, timeoutMs);
951
+ // A late socket callback after settle is a no-op; clearing the timer just
952
+ // stops it from keeping the event loop alive past the connect.
953
+ void promise.then(
954
+ () => clearTimeout(timer),
955
+ () => clearTimeout(timer),
956
+ );
957
+
938
958
  Bun.connect({
939
959
  unix: options.unix,
940
960
  socket: {
941
961
  open(socket) {
962
+ opened = true;
942
963
  resolve({
943
964
  readable,
944
965
  writeSink: socketToSink(socket),
@@ -949,6 +970,9 @@ async function connectSocket(options: { unix: string }): Promise<SocketTransport
949
970
  streamController.enqueue(new Uint8Array(data));
950
971
  },
951
972
  close() {
973
+ if (!opened) {
974
+ reject(new Error(`Unix socket ${options.unix} closed before opening`));
975
+ }
952
976
  try {
953
977
  streamController.close();
954
978
  } catch {
@@ -956,6 +980,9 @@ async function connectSocket(options: { unix: string }): Promise<SocketTransport
956
980
  }
957
981
  },
958
982
  error(_socket, err) {
983
+ if (!opened) {
984
+ reject(err);
985
+ }
959
986
  try {
960
987
  streamController.error(err);
961
988
  } catch {
@@ -963,6 +990,12 @@ async function connectSocket(options: { unix: string }): Promise<SocketTransport
963
990
  }
964
991
  },
965
992
  },
993
+ }).catch(err => {
994
+ // Bun.connect rejects the returned promise on synchronous connect
995
+ // failures (e.g. ENOENT) without always firing the `error` handler.
996
+ if (!opened) {
997
+ reject(err);
998
+ }
966
999
  });
967
1000
 
968
1001
  return promise;
package/src/edit/index.ts CHANGED
@@ -3,7 +3,7 @@ import hashlineGrammar from "@oh-my-pi/hashline/grammar.lark" with { type: "text
3
3
  import hashlineDescription from "@oh-my-pi/hashline/prompt.md" with { type: "text" };
4
4
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
5
5
  import type { ToolExample } from "@oh-my-pi/pi-ai";
6
- import { prompt } from "@oh-my-pi/pi-utils";
6
+ import { isEnoent, isEnotdir, prompt } from "@oh-my-pi/pi-utils";
7
7
  import { createLspWritethrough, flushLspWritethroughBatch, type WritethroughCallback, writethroughNoop } from "../lsp";
8
8
  import { DeferredDiagnostics } from "../lsp/deferred-diagnostics";
9
9
  import { getDiagnosticsLedger } from "../lsp/diagnostics-ledger";
@@ -12,7 +12,8 @@ import patchDescription from "../prompts/tools/patch.md" with { type: "text" };
12
12
  import replaceDescription from "../prompts/tools/replace.md" with { type: "text" };
13
13
  import type { ToolSession } from "../tools";
14
14
  import { truncateForPrompt } from "../tools/approval";
15
- import { isInternalUrlPath } from "../tools/path-utils";
15
+ import { findUniqueWorkspaceSuffix, isInternalUrlPath } from "../tools/path-utils";
16
+ import { resolvePlanPath } from "../tools/plan-mode-guard";
16
17
  import { type EditMode, normalizeEditMode, resolveEditMode } from "../utils/edit-mode";
17
18
  import { executeHashlineSingle, hashlineEditParamsSchema } from "./hashline";
18
19
  import { type ApplyPatchParams, applyPatchSchema, expandApplyPatchToEntries } from "./modes/apply-patch";
@@ -73,6 +74,24 @@ function resolveConfiguredEditMode(rawEditMode: string): EditMode | undefined {
73
74
  return editMode;
74
75
  }
75
76
 
77
+ async function resolveEditPath(
78
+ session: ToolSession,
79
+ authoredPath: string,
80
+ options: { mustExist: boolean; signal?: AbortSignal },
81
+ ): Promise<string> {
82
+ if (!options.mustExist || isInternalUrlPath(authoredPath)) return authoredPath;
83
+
84
+ try {
85
+ await Bun.file(resolvePlanPath(session, authoredPath)).stat();
86
+ return authoredPath;
87
+ } catch (error) {
88
+ if (!isEnoent(error) && !isEnotdir(error)) throw error;
89
+ }
90
+
91
+ const match = await findUniqueWorkspaceSuffix(authoredPath, session.cwd, options.signal);
92
+ return match?.displayPath ?? authoredPath;
93
+ }
94
+
76
95
  function resolveAllowFuzzy(session: ToolSession, rawValue: string): boolean {
77
96
  switch (rawValue) {
78
97
  case "true":
@@ -517,7 +536,7 @@ export class EditTool implements AgentTool<TInput> {
517
536
  note: "All entries in one call apply to the top-level `path`; use separate calls for different files.",
518
537
  },
519
538
  ] satisfies readonly ToolExample<PatchParams>[],
520
- execute: (
539
+ execute: async (
521
540
  tool: EditTool,
522
541
  params: EditParams,
523
542
  signal: AbortSignal | undefined,
@@ -525,11 +544,15 @@ export class EditTool implements AgentTool<TInput> {
525
544
  onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
526
545
  ) => {
527
546
  const { edits, path } = params as PatchParams;
547
+ const targetPath = await resolveEditPath(tool.session, path, {
548
+ mustExist: (edits[0]?.op ?? "update") !== "create",
549
+ signal,
550
+ });
528
551
  const runs = (edits as PatchEditEntry[]).map(
529
552
  entry => (br: LspBatchRequest | undefined) =>
530
553
  executePatchSingle({
531
554
  session: tool.session,
532
- path,
555
+ path: targetPath,
533
556
  params: entry,
534
557
  signal,
535
558
  batchRequest: br,
@@ -542,7 +565,7 @@ export class EditTool implements AgentTool<TInput> {
542
565
  beginDeferredDiagnosticsForPath: p => tool.#deferredDiagnostics.begin(p),
543
566
  }),
544
567
  );
545
- return executeSinglePathEntries(path, runs, batchRequest, onUpdate, tool.session.cwd, signal);
568
+ return executeSinglePathEntries(targetPath, runs, batchRequest, onUpdate, tool.session.cwd, signal);
546
569
  },
547
570
  },
548
571
  apply_patch: {
@@ -564,14 +587,26 @@ export class EditTool implements AgentTool<TInput> {
564
587
  onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
565
588
  ) => {
566
589
  const entries = expandApplyPatchToEntries(params as ApplyPatchParams);
590
+ // Resolve each authored path once per patch so paired hunks (e.g. delete
591
+ // then re-add of the same file) share the same workspace target.
592
+ const resolvedTargets = new Map<string, Promise<string>>();
593
+ const resolveOnce = (path: string, mustExist: boolean): Promise<string> => {
594
+ let pending = resolvedTargets.get(path);
595
+ if (!pending) {
596
+ pending = resolveEditPath(tool.session, path, { mustExist, signal });
597
+ resolvedTargets.set(path, pending);
598
+ }
599
+ return pending;
600
+ };
567
601
  const perFile = entries.map(entry => {
568
602
  const { path, ...patchParams } = entry;
569
603
  return {
570
604
  path,
571
- run: (br: LspBatchRequest | undefined) =>
572
- executePatchSingle({
605
+ run: async (br: LspBatchRequest | undefined) => {
606
+ const targetPath = await resolveOnce(path, patchParams.op !== "create");
607
+ return executePatchSingle({
573
608
  session: tool.session,
574
- path,
609
+ path: targetPath,
575
610
  params: patchParams,
576
611
  signal,
577
612
  batchRequest: br,
@@ -579,7 +614,8 @@ export class EditTool implements AgentTool<TInput> {
579
614
  fuzzyThreshold: tool.#fuzzyThreshold,
580
615
  writethrough: tool.#writethrough,
581
616
  beginDeferredDiagnosticsForPath: p => tool.#deferredDiagnostics.begin(p),
582
- }),
617
+ });
618
+ },
583
619
  };
584
620
  });
585
621
  return executeApplyPatchPerFile(perFile, batchRequest, tool.session.cwd, signal, onUpdate);
@@ -609,7 +645,7 @@ export class EditTool implements AgentTool<TInput> {
609
645
  replace: {
610
646
  description: () => prompt.render(replaceDescription),
611
647
  parameters: replaceEditSchema,
612
- execute: (
648
+ execute: async (
613
649
  tool: EditTool,
614
650
  params: EditParams,
615
651
  signal: AbortSignal | undefined,
@@ -617,11 +653,12 @@ export class EditTool implements AgentTool<TInput> {
617
653
  onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
618
654
  ) => {
619
655
  const { edits, path } = params as ReplaceParams;
656
+ const targetPath = await resolveEditPath(tool.session, path, { mustExist: true, signal });
620
657
  const runs = (edits as ReplaceEditEntry[]).map(
621
658
  entry => (br: LspBatchRequest | undefined) =>
622
659
  executeReplaceSingle({
623
660
  session: tool.session,
624
- path,
661
+ path: targetPath,
625
662
  params: entry,
626
663
  signal,
627
664
  batchRequest: br,
@@ -631,7 +668,7 @@ export class EditTool implements AgentTool<TInput> {
631
668
  beginDeferredDiagnosticsForPath: p => tool.#deferredDiagnostics.begin(p),
632
669
  }),
633
670
  );
634
- return executeSinglePathEntries(path, runs, batchRequest, onUpdate, tool.session.cwd, signal);
671
+ return executeSinglePathEntries(targetPath, runs, batchRequest, onUpdate, tool.session.cwd, signal);
635
672
  },
636
673
  },
637
674
  }[this.mode];
@@ -21,6 +21,7 @@ import {
21
21
  type KernelDisplayOutput,
22
22
  type KernelExecuteOptions,
23
23
  type KernelExecuteResult,
24
+ type KernelShutdownResult,
24
25
  PythonKernel,
25
26
  } from "./kernel";
26
27
  import { resolveExplicitPythonRuntime } from "./runtime";
@@ -136,11 +137,19 @@ export interface PythonResult {
136
137
  // the same tuple; the kernel stays alive until the last owner detaches.
137
138
  // ---------------------------------------------------------------------------
138
139
 
140
+ interface SessionKernelReplacement {
141
+ generation: number;
142
+ deadlineMs?: number;
143
+ promise: Promise<PythonKernel>;
144
+ }
145
+
139
146
  interface PythonSession {
140
147
  sessionKey: string;
141
148
  sessionId: string;
142
149
  cwd: string;
143
150
  kernel: PythonKernel;
151
+ generation: number;
152
+ replacement?: SessionKernelReplacement;
144
153
  ownerIds: Set<string>;
145
154
  hasFallbackOwner: boolean;
146
155
  }
@@ -254,6 +263,7 @@ async function acquireSession(
254
263
  sessionId,
255
264
  cwd,
256
265
  kernel,
266
+ generation: 0,
257
267
  ownerIds: new Set(),
258
268
  hasFallbackOwner: false,
259
269
  };
@@ -272,31 +282,99 @@ async function acquireSession(
272
282
 
273
283
  async function replaceSessionKernel(
274
284
  session: PythonSession,
285
+ kernel: PythonKernel,
286
+ generation: number,
275
287
  cwd: string,
276
288
  options: PythonExecutorOptions,
277
- ): Promise<void> {
278
- const old = session.kernel;
279
- const remaining = getRemainingTimeoutMs(options.deadlineMs);
280
- await old
281
- .shutdown(remaining !== undefined ? { timeoutMs: Math.max(0, remaining) } : undefined)
282
- .catch(() => undefined);
283
- if (sessions.get(session.sessionKey) !== session) {
284
- throw new PythonExecutionCancelledError(false);
289
+ ): Promise<PythonKernel> {
290
+ const inFlight = session.replacement;
291
+ if (inFlight?.generation === generation) {
292
+ if (
293
+ inFlight.deadlineMs !== undefined &&
294
+ (options.deadlineMs === undefined || options.deadlineMs > inFlight.deadlineMs)
295
+ ) {
296
+ inFlight.deadlineMs = options.deadlineMs;
297
+ }
298
+ return await waitForPromiseWithCancellation(inFlight.promise, options, PythonExecutionCancelledError);
285
299
  }
286
- requireRemainingTimeoutMs(options.deadlineMs);
287
- const next = await startKernel(cwd, options);
288
- if (sessions.get(session.sessionKey) !== session) {
289
- await next.shutdown().catch(() => undefined);
300
+ if (sessions.get(session.sessionKey) !== session || session.generation !== generation || session.kernel !== kernel) {
290
301
  throw new PythonExecutionCancelledError(false);
291
302
  }
292
- session.kernel = next;
303
+
304
+ const deferred = Promise.withResolvers<PythonKernel>();
305
+ const replacement: SessionKernelReplacement = {
306
+ generation,
307
+ deadlineMs: options.deadlineMs,
308
+ promise: deferred.promise,
309
+ };
310
+ session.replacement = replacement;
311
+ void (async () => {
312
+ try {
313
+ const remaining = getRemainingTimeoutMs(options.deadlineMs);
314
+ await kernel
315
+ .shutdown(remaining !== undefined ? { timeoutMs: Math.max(0, remaining) } : undefined)
316
+ .catch(() => undefined);
317
+ if (replacement.deadlineMs !== undefined && replacement.deadlineMs <= Date.now()) {
318
+ throw new PythonExecutionCancelledError(true);
319
+ }
320
+ if (
321
+ sessions.get(session.sessionKey) !== session ||
322
+ session.generation !== generation ||
323
+ session.kernel !== kernel
324
+ ) {
325
+ throw new PythonExecutionCancelledError(false);
326
+ }
327
+ const next = await startKernel(cwd, {
328
+ ...options,
329
+ signal: undefined,
330
+ deadlineMs: undefined,
331
+ });
332
+ if (
333
+ sessions.get(session.sessionKey) !== session ||
334
+ session.generation !== generation ||
335
+ session.kernel !== kernel
336
+ ) {
337
+ await next.shutdown().catch(() => undefined);
338
+ throw new PythonExecutionCancelledError(false);
339
+ }
340
+ session.kernel = next;
341
+ session.generation += 1;
342
+ deferred.resolve(next);
343
+ } catch (err) {
344
+ deferred.reject(err);
345
+ } finally {
346
+ if (session.replacement === replacement) session.replacement = undefined;
347
+ }
348
+ })();
349
+ return await waitForPromiseWithCancellation(deferred.promise, options, PythonExecutionCancelledError);
350
+ }
351
+
352
+ async function shutdownInvalidatedSession(session: PythonSession): Promise<KernelShutdownResult> {
353
+ const replacement = session.replacement;
354
+ if (replacement) await replacement.promise.catch(() => undefined);
355
+ return await session.kernel.shutdown();
356
+ }
357
+
358
+ async function acquireLiveSessionKernel(
359
+ session: PythonSession,
360
+ cwd: string,
361
+ options: PythonExecutorOptions,
362
+ ): Promise<PythonKernel> {
363
+ while (sessions.get(session.sessionKey) === session) {
364
+ const kernel = session.kernel;
365
+ const generation = session.generation;
366
+ if (kernel.isAlive()) return kernel;
367
+ await replaceSessionKernel(session, kernel, generation, cwd, options);
368
+ }
369
+ throw new PythonExecutionCancelledError(false);
293
370
  }
294
371
 
295
372
  async function resetSession(sessionKey: string): Promise<void> {
296
373
  const existing = sessions.get(sessionKey) ?? (await startingSessions.get(sessionKey)?.catch(() => undefined));
297
374
  if (!existing) return;
375
+ existing.generation += 1;
298
376
  sessions.delete(sessionKey);
299
- await existing.kernel.shutdown().catch(() => undefined);
377
+ await shutdownInvalidatedSession(existing).catch(() => undefined);
300
378
  }
301
379
 
302
380
  // ---------------------------------------------------------------------------
@@ -315,9 +393,10 @@ export async function disposeAllKernelSessions(): Promise<void> {
315
393
  }
316
394
  }
317
395
  for (const [id, session] of all) {
396
+ session.generation += 1;
318
397
  if (sessions.get(id) === session) sessions.delete(id);
319
398
  }
320
- const results = await Promise.allSettled(all.map(([, session]) => session.kernel.shutdown()));
399
+ const results = await Promise.allSettled(all.map(([, session]) => shutdownInvalidatedSession(session)));
321
400
  for (let i = 0; i < all.length; i += 1) {
322
401
  const [id, session] = all[i];
323
402
  const result = results[i];
@@ -344,9 +423,10 @@ export async function disposeKernelSessionsByOwner(ownerId: string): Promise<voi
344
423
  session.ownerIds.delete(ownerId);
345
424
  }
346
425
  for (const session of toShutdown) {
426
+ session.generation += 1;
347
427
  if (sessions.get(session.sessionKey) === session) sessions.delete(session.sessionKey);
348
428
  }
349
- const results = await Promise.allSettled(toShutdown.map(session => session.kernel.shutdown()));
429
+ const results = await Promise.allSettled(toShutdown.map(session => shutdownInvalidatedSession(session)));
350
430
  for (let i = 0; i < toShutdown.length; i += 1) {
351
431
  const session = toShutdown[i];
352
432
  const result = results[i];
@@ -458,31 +538,21 @@ async function executeOnSession(code: string, cwd: string, options: PythonExecut
458
538
  isTimedOutCancellation(options.signal.reason, PythonExecutionCancelledError, options.signal),
459
539
  );
460
540
  }
461
- if (sessions.get(session.sessionKey) !== session) {
541
+ const kernel = await acquireLiveSessionKernel(session, cwd, options);
542
+ if (sessions.get(session.sessionKey) !== session || session.kernel !== kernel) {
462
543
  throw new PythonExecutionCancelledError(false);
463
544
  }
464
- if (!session.kernel.isAlive()) {
465
- await replaceSessionKernel(session, cwd, options);
466
- if (sessions.get(session.sessionKey) !== session) {
467
- throw new PythonExecutionCancelledError(false);
468
- }
469
- }
470
545
  const runOptions = { ...options, cwd };
471
546
  try {
472
- return await executeWithKernel(session.kernel, code, runOptions);
547
+ return await executeWithKernel(kernel, code, runOptions);
473
548
  } catch (err) {
474
549
  if (isCancellationError(err, PythonExecutionCancelledError) || options.signal?.aborted) throw err;
475
- if (session.kernel.isAlive()) throw err;
476
- if (sessions.get(session.sessionKey) !== session) {
477
- throw new PythonExecutionCancelledError(false);
478
- }
479
- // Shared kernels are keyed by cwd, so a dead kernel can be recreated in place
480
- // without risking cross-directory state bleed.
481
- await replaceSessionKernel(session, cwd, options);
482
- if (sessions.get(session.sessionKey) !== session) {
550
+ if (kernel.isAlive()) throw err;
551
+ const retryKernel = await acquireLiveSessionKernel(session, cwd, options);
552
+ if (sessions.get(session.sessionKey) !== session || session.kernel !== retryKernel) {
483
553
  throw new PythonExecutionCancelledError(false);
484
554
  }
485
- return await executeWithKernel(session.kernel, code, runOptions);
555
+ return await executeWithKernel(retryKernel, code, runOptions);
486
556
  }
487
557
  }
488
558
 
@@ -10,6 +10,7 @@ import { Settings, type ShellMinimizerSettings } from "../config/settings";
10
10
  import { OutputSink } from "../session/streaming-output";
11
11
  import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta";
12
12
  import { getOrCreateSnapshot } from "../utils/shell-snapshot";
13
+ import { loadDirenvEnv } from "./direnv";
13
14
  import { buildNonInteractiveEnv } from "./non-interactive-env";
14
15
 
15
16
  export interface BashExecutorOptions {
@@ -56,6 +57,78 @@ export interface BashResult {
56
57
  workingDir?: string;
57
58
  }
58
59
 
60
+ /** POSIX-safe variable name — gates which direnv unsets we inject into the
61
+ * command line, so a hostile `.envrc` can't smuggle shell syntax through
62
+ * `unset`. `.envrc` never produces non-identifier names in practice. */
63
+ const SAFE_ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
64
+
65
+ export interface DirenvPreflightOptions {
66
+ /** Caller-supplied env overlay; these values win over direnv-provided ones. */
67
+ callerEnv?: Record<string, string>;
68
+ signal?: AbortSignal;
69
+ /** Full direnv-load budget (`bash.direnvLoadTimeoutMs`). A positive
70
+ * `callerTimeoutMs` clamps the effective load below this; `0`/undefined
71
+ * leaves the full budget. */
72
+ timeoutMs?: number;
73
+ /** The caller's command deadline (ms). A positive value clamps the direnv
74
+ * load so a cold `.envrc` can't outlast a short-timeout command; `0` or
75
+ * undefined means "no caller clamp" — the load keeps its full `timeoutMs`
76
+ * budget (a disabled command deadline is NOT a 0 ms load). Centralizing the
77
+ * clamp here keeps every backend (executeBash, ACP terminal, PTY) on one
78
+ * contract instead of each re-deriving it. */
79
+ callerTimeoutMs?: number;
80
+ /** `bash.direnv` setting — `"off"` skips the load entirely. */
81
+ direnvSetting: "auto" | "off";
82
+ /** Shell wrapper prefix (profiler/strace) to place *after* the unset prefix,
83
+ * matching `executeBash`'s ordering. Backends that apply their own shell
84
+ * wrapping (ACP `wrapShellLineForClientTerminal`) omit this. */
85
+ commandPrefix?: string | undefined;
86
+ }
87
+
88
+ /**
89
+ * Load the repo's direnv/devenv env and fold it into a `(command, env)` pair so
90
+ * every bash backend (one-shot `executeBash`, ACP client terminal, PTY) exposes
91
+ * the same devenv tools. Encapsulates: load the diff, merge `set` under the
92
+ * caller's overlay (caller wins), and prepend a regex-gated `unset -v` for
93
+ * variables the `.envrc` removes (skipping any the caller re-supplied).
94
+ *
95
+ * Returns the possibly-prefixed command plus the merged env, or the inputs
96
+ * unchanged (`env` = `callerEnv`) when direnv is off, absent, or has no `.envrc`.
97
+ * Pure transform: does NOT layer non-interactive env defaults — that stays the
98
+ * caller's job (so interactive PTY/ACP paths keep their own env shape).
99
+ */
100
+ export async function applyDirenvPreflight(
101
+ command: string,
102
+ cwd: string,
103
+ opts: DirenvPreflightOptions,
104
+ ): Promise<{ command: string; env: Record<string, string> | undefined }> {
105
+ const withPrefix = (line: string): string => (opts.commandPrefix ? `${opts.commandPrefix} ${line}` : line);
106
+ // A positive caller deadline clamps the direnv load below its full budget so
107
+ // a cold `.envrc` can't outlast a short-timeout command; `0`/undefined means
108
+ // "no caller clamp" (a disabled command deadline is not a 0 ms load). Every
109
+ // backend routes through here, so the clamp lives in one place.
110
+ const loadTimeoutMs =
111
+ opts.callerTimeoutMs !== undefined && opts.callerTimeoutMs > 0
112
+ ? Math.min(opts.timeoutMs ?? opts.callerTimeoutMs, opts.callerTimeoutMs)
113
+ : opts.timeoutMs;
114
+ const direnvDiff =
115
+ opts.direnvSetting === "off" ? null : await loadDirenvEnv(cwd, { timeoutMs: loadTimeoutMs, signal: opts.signal });
116
+ if (!direnvDiff) {
117
+ return { command: withPrefix(command), env: opts.callerEnv };
118
+ }
119
+ // The caller's explicit env still wins over direnv-provided values.
120
+ const mergedEnv = { ...direnvDiff.set, ...opts.callerEnv };
121
+ // direnv can also *remove* inherited variables (a `.envrc` doing
122
+ // `unset AWS_PROFILE`). An env overlay can only add/override, so prepend a
123
+ // real `unset` for those — unless the caller re-supplied the same var
124
+ // explicitly, in which case the caller wins.
125
+ const direnvUnsets = direnvDiff.unset.filter(
126
+ name => !(opts.callerEnv && name in opts.callerEnv) && SAFE_ENV_NAME.test(name),
127
+ );
128
+ const unsetPrefix = direnvUnsets.length > 0 ? `unset -v ${direnvUnsets.join(" ")}; ` : "";
129
+ return { command: `${unsetPrefix}${withPrefix(command)}`, env: mergedEnv };
130
+ }
131
+
59
132
  const shellSessions = new Map<string, Shell>();
60
133
  const brokenShellSessions = new Set<string>();
61
134
  const shellSessionQuarantines = new Map<string, Promise<unknown>>();
@@ -279,15 +352,25 @@ export async function executeBash(command: string, options?: BashExecutorOptions
279
352
  const minimizer = buildMinimizerOptions(settings.getGroup("shellMinimizer"));
280
353
 
281
354
  const commandCwd = resolveShellCwd(options?.cwd);
282
- const commandEnv = buildNonInteractiveEnv(options?.env);
283
-
284
- // Apply command prefix if configured
285
- const prefixedCommand = prefix ? `${prefix} ${command}` : command;
355
+ // Fold the repo's direnv/devenv env into the command + env so devenv tools
356
+ // land on PATH; the caller's explicit `env` still wins. Thread the caller's
357
+ // signal + timeout so an aborted / short-timeout call can't hang on a cold
358
+ // `.envrc` load before the abort listener is installed. The helper applies
359
+ // the configured shell `prefix` after any `unset -v` it prepends.
360
+ const preflight = await applyDirenvPreflight(command, commandCwd ?? process.cwd(), {
361
+ callerEnv: options?.env,
362
+ signal: options?.signal,
363
+ timeoutMs: settings.get("bash.direnvLoadTimeoutMs"),
364
+ callerTimeoutMs: options?.timeout,
365
+ direnvSetting: settings.get("bash.direnv"),
366
+ commandPrefix: prefix,
367
+ });
368
+ const commandEnv = buildNonInteractiveEnv(preflight.env);
286
369
  const runCdInPersistentShell = options?.useUserShell === true && !prefix && isPersistentShellCdCommand(command);
287
370
  const finalCommand =
288
371
  options?.useUserShell === true && !bashShell && !runCdInPersistentShell
289
- ? buildUserShellCommand(shell, args, prefixedCommand)
290
- : prefixedCommand;
372
+ ? buildUserShellCommand(shell, args, preflight.command)
373
+ : preflight.command;
291
374
 
292
375
  // Create output sink for truncation and artifact handling
293
376
  const sink = new OutputSink({