@gajae-code/coding-agent 0.14.2 → 0.15.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 (345) hide show
  1. package/CHANGELOG.md +120 -1
  2. package/dist/types/async/job-manager.d.ts +4 -1
  3. package/dist/types/autoresearch/dashboard.d.ts +29 -4
  4. package/dist/types/autoresearch/data-context.d.ts +25 -0
  5. package/dist/types/autoresearch/git.d.ts +65 -7
  6. package/dist/types/autoresearch/harness.d.ts +58 -0
  7. package/dist/types/autoresearch/prompts.d.ts +51 -0
  8. package/dist/types/autoresearch/runs.d.ts +173 -0
  9. package/dist/types/autoresearch/types.d.ts +9 -139
  10. package/dist/types/capability/types.d.ts +20 -0
  11. package/dist/types/cli/accounts-cli.d.ts +1 -0
  12. package/dist/types/commands/{rlm.d.ts → autoresearch.d.ts} +2 -4
  13. package/dist/types/config/autorouting-contract.d.ts +146 -0
  14. package/dist/types/config/autorouting-generator.d.ts +40 -0
  15. package/dist/types/config/autorouting-tier-map.d.ts +16844 -0
  16. package/dist/types/config/autorouting.d.ts +35 -0
  17. package/dist/types/config/model-discovery-manager.d.ts +8 -1
  18. package/dist/types/config/model-registry.d.ts +30 -1
  19. package/dist/types/config/provider-selection-policy.d.ts +23 -0
  20. package/dist/types/config/settings-schema.d.ts +61 -3
  21. package/dist/types/config/settings.d.ts +3 -0
  22. package/dist/types/coordinator/contract.d.ts +1 -1
  23. package/dist/types/coordinator-mcp/durability.d.ts +29 -0
  24. package/dist/types/coordinator-mcp/event-webhook.d.ts +50 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +1 -1
  26. package/dist/types/coordinator-mcp/server.d.ts +14 -0
  27. package/dist/types/crash/index-store.d.ts +3 -0
  28. package/dist/types/crash/record-loader.d.ts +2 -0
  29. package/dist/types/crash/upstream/relay.d.ts +5 -3
  30. package/dist/types/cursor.d.ts +12 -0
  31. package/dist/types/defaults/gjc-defaults.d.ts +35 -1
  32. package/dist/types/defaults/gjc-skills.generated.d.ts +1 -1
  33. package/dist/types/discovery/helpers.d.ts +1 -1
  34. package/dist/types/edit/index.d.ts +2 -1
  35. package/dist/types/eval/py/executor.d.ts +2 -0
  36. package/dist/types/extensibility/extensions/loader.d.ts +32 -0
  37. package/dist/types/extensibility/gjc-plugins/types.d.ts +1 -1
  38. package/dist/types/extensibility/skills.d.ts +2 -0
  39. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +194 -0
  40. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  41. package/dist/types/gjc-runtime/psmux-detect.d.ts +1 -1
  42. package/dist/types/gjc-runtime/python-transcript.d.ts +19 -0
  43. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +2 -8
  44. package/dist/types/gjc-runtime/session-layout.d.ts +6 -3
  45. package/dist/types/gjc-runtime/state-renderer.d.ts +0 -22
  46. package/dist/types/gjc-runtime/state-schema.d.ts +7 -7
  47. package/dist/types/gjc-runtime/tmux-common.d.ts +2 -2
  48. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +38 -1
  49. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -24
  50. package/dist/types/gjc-runtime/ultragoal-validation-policy.d.ts +73 -0
  51. package/dist/types/gjc-runtime/workflow-recovery-projection.d.ts +100 -0
  52. package/dist/types/hooks/skill-keywords.d.ts +1 -1
  53. package/dist/types/main.d.ts +0 -10
  54. package/dist/types/modes/acp/acp-agent.d.ts +13 -6
  55. package/dist/types/modes/components/model-selector.d.ts +14 -0
  56. package/dist/types/modes/components/skill-hud/render.d.ts +6 -1
  57. package/dist/types/modes/components/smart-routing-panel.d.ts +82 -0
  58. package/dist/types/modes/controllers/selector-controller.d.ts +14 -0
  59. package/dist/types/modes/interactive-mode.d.ts +3 -0
  60. package/dist/types/modes/types.d.ts +9 -0
  61. package/dist/types/rlm/artifacts.d.ts +4 -4
  62. package/dist/types/runtime/optional-runtime-services.d.ts +2 -1
  63. package/dist/types/runtime/workspace-tree-service.d.ts +5 -1
  64. package/dist/types/runtime-mcp/config.d.ts +6 -0
  65. package/dist/types/runtime-mcp/manager.d.ts +10 -0
  66. package/dist/types/sdk/acp/adapter.d.ts +0 -4
  67. package/dist/types/sdk/broker/broker.d.ts +1 -1
  68. package/dist/types/sdk/broker/lifecycle.d.ts +57 -2
  69. package/dist/types/sdk/broker/session-index.d.ts +15 -0
  70. package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -0
  71. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -0
  72. package/dist/types/sdk/bus/index.d.ts +8 -0
  73. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +2 -0
  74. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -2
  75. package/dist/types/sdk/client/client.d.ts +39 -4
  76. package/dist/types/sdk/host/host.d.ts +1 -0
  77. package/dist/types/sdk/host/internal-autorouting-state.d.ts +3 -0
  78. package/dist/types/sdk/host/model-pin.d.ts +29 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +2 -1
  80. package/dist/types/sdk/lifecycle-session.d.ts +13 -0
  81. package/dist/types/sdk/router/session-router.d.ts +27 -1
  82. package/dist/types/sdk/session.d.ts +11 -1
  83. package/dist/types/session/agent-session.d.ts +36 -6
  84. package/dist/types/session/artifacts.d.ts +16 -1
  85. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  86. package/dist/types/session/session-manager.d.ts +84 -2
  87. package/dist/types/session/session-staging-paths.d.ts +3 -0
  88. package/dist/types/skill-state/canonical-skills.d.ts +1 -1
  89. package/dist/types/skill-state/workflow-hud.d.ts +12 -21
  90. package/dist/types/skill-state/workflow-mutation-guard.d.ts +1 -0
  91. package/dist/types/slash-commands/helpers/autorouting-status.d.ts +8 -0
  92. package/dist/types/system-prompt.d.ts +23 -0
  93. package/dist/types/task/executor.d.ts +35 -4
  94. package/dist/types/task/index.d.ts +17 -3
  95. package/dist/types/task/receipt.d.ts +3 -1
  96. package/dist/types/task/types.d.ts +144 -0
  97. package/dist/types/tools/atomic-file-write.d.ts +72 -0
  98. package/dist/types/tools/descriptors.d.ts +1 -0
  99. package/dist/types/tools/index.d.ts +36 -1
  100. package/dist/types/tools/move-session.d.ts +40 -0
  101. package/dist/types/tools/python.d.ts +12 -0
  102. package/dist/types/tools/resource-gc.d.ts +0 -3
  103. package/dist/types/tools/search.d.ts +8 -1
  104. package/dist/types/tools/skill.d.ts +9 -4
  105. package/dist/types/tools/subagent.d.ts +33 -3
  106. package/dist/types/utils/xml-escape.d.ts +2 -0
  107. package/dist/types/workflow/workflow-intent-diff.d.ts +2 -2
  108. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +1 -1
  109. package/package.json +11 -12
  110. package/scripts/build-sdk-package-smoke.ts +7 -5
  111. package/scripts/check-autorouting-tier-map.ts +183 -0
  112. package/scripts/generate-sdk-operation-inventory.ts +12 -0
  113. package/scripts/generate-tool-catalog.ts +40 -5
  114. package/src/async/job-manager.ts +5 -2
  115. package/src/autoresearch/dashboard.ts +134 -314
  116. package/src/autoresearch/data-context.ts +35 -0
  117. package/src/autoresearch/git.ts +186 -26
  118. package/src/autoresearch/{helpers.ts → harness.ts} +65 -40
  119. package/src/autoresearch/{prompt-setup.md → prompts/prompt-setup.md} +7 -7
  120. package/src/autoresearch/prompts/prompt.md +103 -0
  121. package/src/autoresearch/prompts.ts +126 -0
  122. package/src/autoresearch/runs.ts +708 -0
  123. package/src/autoresearch/types.ts +9 -150
  124. package/src/capability/index.ts +4 -4
  125. package/src/capability/types.ts +20 -0
  126. package/src/cli/accounts-cli.ts +21 -5
  127. package/src/cli/config-cli.ts +25 -1
  128. package/src/cli/fast-help.ts +1 -2
  129. package/src/cli/list-models.ts +5 -8
  130. package/src/cli/setup-cli.ts +8 -0
  131. package/src/cli.ts +2 -21
  132. package/src/commands/autoresearch.ts +33 -0
  133. package/src/commands/coordinator.ts +15 -0
  134. package/src/commands/launch.ts +2 -4
  135. package/src/commands/quick-lane.ts +1 -1
  136. package/src/commands/sdk.ts +24 -0
  137. package/src/commands/state.ts +1 -1
  138. package/src/config/autorouting-contract.ts +474 -0
  139. package/src/config/autorouting-generator.ts +238 -0
  140. package/src/config/autorouting-tier-map.ts +6108 -0
  141. package/src/config/autorouting.ts +120 -0
  142. package/src/config/model-discovery-manager.ts +42 -11
  143. package/src/config/model-registry.ts +348 -68
  144. package/src/config/provider-selection-policy.ts +60 -9
  145. package/src/config/settings-schema.ts +195 -6
  146. package/src/config/settings.ts +62 -1
  147. package/src/config.ts +2 -3
  148. package/src/coordinator/contract.ts +0 -1
  149. package/src/coordinator-mcp/codex-handoff.ts +68 -42
  150. package/src/coordinator-mcp/durability.ts +200 -0
  151. package/src/coordinator-mcp/event-webhook.ts +278 -0
  152. package/src/coordinator-mcp/question-state.ts +109 -46
  153. package/src/coordinator-mcp/server.ts +932 -504
  154. package/src/crash/index-store.ts +126 -22
  155. package/src/crash/record-loader.ts +7 -0
  156. package/src/crash/upstream/relay.ts +82 -34
  157. package/src/cursor.ts +84 -1
  158. package/src/debug/index.ts +26 -0
  159. package/src/defaults/gjc/skills/autoresearch/SKILL.md +116 -0
  160. package/src/defaults/gjc/skills/autoresearch/auto-critic.md +46 -0
  161. package/src/defaults/gjc/skills/autoresearch/auto-iterate.md +42 -0
  162. package/src/defaults/gjc/skills/deep-interview/SKILL.md +17 -37
  163. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -18
  164. package/src/defaults/gjc/skills/ultragoal/SKILL.md +6 -17
  165. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +2 -2
  166. package/src/defaults/gjc-defaults.ts +89 -2
  167. package/src/defaults/gjc-skills.generated.ts +24 -17
  168. package/src/discovery/builtin.ts +18 -5
  169. package/src/discovery/helpers.ts +6 -6
  170. package/src/edit/index.ts +5 -2
  171. package/src/eval/py/executor.ts +8 -0
  172. package/src/extensibility/extensions/loader.ts +135 -13
  173. package/src/extensibility/extensions/wrapper.ts +1 -2
  174. package/src/extensibility/gjc-plugins/schema.ts +1 -1
  175. package/src/extensibility/runtime-skill-discovery.ts +2 -2
  176. package/src/extensibility/skill-management.ts +2 -3
  177. package/src/extensibility/skills.ts +16 -8
  178. package/src/gjc-runtime/autoresearch-runtime.ts +1509 -0
  179. package/src/gjc-runtime/gc-render.ts +0 -1
  180. package/src/gjc-runtime/gc-runtime.ts +1 -11
  181. package/src/gjc-runtime/launch-tmux.ts +3 -3
  182. package/src/gjc-runtime/psmux-detect.ts +2 -2
  183. package/src/gjc-runtime/python-transcript.ts +58 -0
  184. package/src/gjc-runtime/ralplan-runtime.ts +6 -18
  185. package/src/gjc-runtime/session-layout.ts +18 -7
  186. package/src/gjc-runtime/session-state-sidecar.ts +58 -9
  187. package/src/gjc-runtime/state-renderer.ts +0 -38
  188. package/src/gjc-runtime/state-runtime.ts +43 -22
  189. package/src/gjc-runtime/state-schema.ts +1 -1
  190. package/src/gjc-runtime/tmux-common.ts +7 -7
  191. package/src/gjc-runtime/ultragoal-change-set.ts +232 -11
  192. package/src/gjc-runtime/ultragoal-runtime.ts +309 -114
  193. package/src/gjc-runtime/ultragoal-validation-policy.ts +247 -0
  194. package/src/gjc-runtime/workflow-command-ref.ts +15 -11
  195. package/src/gjc-runtime/workflow-manifest.generated.json +413 -532
  196. package/src/gjc-runtime/workflow-manifest.ts +43 -83
  197. package/src/gjc-runtime/workflow-recovery-projection.ts +616 -0
  198. package/src/hooks/skill-keywords.ts +3 -3
  199. package/src/hooks/skill-state.ts +2 -2
  200. package/src/internal-urls/docs-index.generated.ts +26 -24
  201. package/src/lsp/index.ts +25 -5
  202. package/src/main.ts +2 -27
  203. package/src/modes/acp/acp-agent.ts +200 -75
  204. package/src/modes/components/model-selector.ts +216 -13
  205. package/src/modes/components/oauth-selector.ts +5 -5
  206. package/src/modes/components/skill-hud/render.ts +173 -35
  207. package/src/modes/components/smart-routing-panel.ts +531 -0
  208. package/src/modes/components/tool-status-header.ts +6 -3
  209. package/src/modes/controllers/command-controller.ts +17 -7
  210. package/src/modes/controllers/input-controller.ts +44 -9
  211. package/src/modes/controllers/runtime-mcp-command-controller.ts +4 -0
  212. package/src/modes/controllers/selector-controller.ts +298 -65
  213. package/src/modes/interactive-mode.ts +8 -1
  214. package/src/modes/shared/agent-wire/workflow-gate-types.ts +1 -1
  215. package/src/modes/types.ts +9 -1
  216. package/src/prompts/agents/planner.md +1 -1
  217. package/src/prompts/system/system-prompt.md +1 -1
  218. package/src/prompts/system/volatile-project-context.md +2 -1
  219. package/src/prompts/tools/move-session.md +15 -0
  220. package/src/prompts/tools/python.md +12 -0
  221. package/src/prompts/tools/skill-discovery.md +1 -1
  222. package/src/prompts/tools/skill.md +2 -2
  223. package/src/prompts/tools/task-summary.md +1 -0
  224. package/src/prompts/tools/task.md +6 -0
  225. package/src/rlm/artifacts.ts +12 -10
  226. package/src/runtime/optional-runtime-services.ts +3 -2
  227. package/src/runtime/workspace-tree-service.ts +11 -3
  228. package/src/runtime-mcp/config.ts +8 -1
  229. package/src/runtime-mcp/manager.ts +33 -0
  230. package/src/sdk/acp/adapter.ts +48 -1
  231. package/src/sdk/broker/broker.ts +30 -2
  232. package/src/sdk/broker/discovery.ts +217 -2
  233. package/src/sdk/broker/lifecycle.ts +385 -59
  234. package/src/sdk/broker/session-index.ts +243 -48
  235. package/src/sdk/broker/transport.ts +1 -0
  236. package/src/sdk/bus/chat-daemon-control.ts +8 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +5 -0
  238. package/src/sdk/bus/index.ts +208 -11
  239. package/src/sdk/bus/kind-aware-reconciliation.ts +16 -0
  240. package/src/sdk/bus/reconciliation-store.ts +27 -5
  241. package/src/sdk/bus/telegram-daemon-contract.ts +12 -2
  242. package/src/sdk/client/client.ts +204 -22
  243. package/src/sdk/host/host.ts +16 -0
  244. package/src/sdk/host/internal-autorouting-state.ts +13 -0
  245. package/src/sdk/host/model-pin.ts +101 -0
  246. package/src/sdk/host/reverse-leases.ts +2 -1
  247. package/src/sdk/host/session-runtime.test.ts +224 -1
  248. package/src/sdk/host/session-runtime.ts +125 -37
  249. package/src/sdk/lifecycle/service.ts +3 -1
  250. package/src/sdk/lifecycle-session.ts +42 -1
  251. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  252. package/src/sdk/router/session-router.ts +375 -36
  253. package/src/sdk/session.ts +547 -47
  254. package/src/session/account-inventory.ts +1 -1
  255. package/src/session/agent-session.ts +765 -189
  256. package/src/session/artifacts.ts +234 -4
  257. package/src/session/internal/managed-session-storage.ts +91 -4
  258. package/src/session/provider-safety-stop-hint.ts +3 -2
  259. package/src/session/session-manager.ts +775 -16
  260. package/src/session/session-staging-paths.ts +8 -0
  261. package/src/setup/hermes/templates/operator-instructions.v1.md +0 -1
  262. package/src/setup/host-plugin-setup.ts +2 -2
  263. package/src/skill-state/active-state.ts +2 -2
  264. package/src/skill-state/canonical-skills.ts +1 -1
  265. package/src/skill-state/initial-phase.ts +1 -1
  266. package/src/skill-state/workflow-hud.ts +41 -41
  267. package/src/skill-state/workflow-mutation-guard.ts +26 -7
  268. package/src/slash-commands/builtin-registry.ts +77 -0
  269. package/src/slash-commands/helpers/autorouting-status.ts +55 -0
  270. package/src/system-prompt.ts +102 -5
  271. package/src/task/discovery.ts +2 -3
  272. package/src/task/executor.ts +617 -43
  273. package/src/task/index.ts +262 -15
  274. package/src/task/receipt.ts +30 -0
  275. package/src/task/render.ts +11 -1
  276. package/src/task/types.ts +316 -0
  277. package/src/tools/atomic-file-write.ts +594 -0
  278. package/src/tools/bisect.ts +3 -2
  279. package/src/tools/descriptors.test.ts +3 -0
  280. package/src/tools/descriptors.ts +24 -0
  281. package/src/tools/image-gen.ts +5 -1
  282. package/src/tools/index.ts +62 -5
  283. package/src/tools/move-session.ts +107 -0
  284. package/src/tools/python.ts +205 -0
  285. package/src/tools/read.ts +88 -15
  286. package/src/tools/renderers.ts +2 -0
  287. package/src/tools/resource-gc.ts +8 -177
  288. package/src/tools/search.ts +30 -6
  289. package/src/tools/skill-discovery.ts +1 -0
  290. package/src/tools/skill.ts +37 -7
  291. package/src/tools/subagent-render.ts +119 -13
  292. package/src/tools/subagent.ts +91 -74
  293. package/src/tools/tool-catalog.generated.ts +64 -2
  294. package/src/tools/write.ts +16 -12
  295. package/src/utils/xml-escape.ts +14 -0
  296. package/src/web/search/providers/codex.ts +2 -2
  297. package/src/workflow/workflow-intent-diff.ts +13 -12
  298. package/src/workflow/workflow-intent-report-consumer.ts +6 -6
  299. package/dist/types/autoresearch/helpers.d.ts +0 -24
  300. package/dist/types/autoresearch/index.d.ts +0 -2
  301. package/dist/types/autoresearch/state.d.ts +0 -17
  302. package/dist/types/autoresearch/storage.d.ts +0 -142
  303. package/dist/types/autoresearch/tools/init-experiment.d.ts +0 -31
  304. package/dist/types/autoresearch/tools/log-experiment.d.ts +0 -23
  305. package/dist/types/autoresearch/tools/run-experiment.d.ts +0 -8
  306. package/dist/types/autoresearch/tools/update-notes.d.ts +0 -12
  307. package/dist/types/commands/team.d.ts +0 -24
  308. package/dist/types/gjc-runtime/team-gc.d.ts +0 -7
  309. package/dist/types/gjc-runtime/team-launch.d.ts +0 -61
  310. package/dist/types/gjc-runtime/team-notify.d.ts +0 -45
  311. package/dist/types/gjc-runtime/team-runtime.d.ts +0 -336
  312. package/dist/types/gjc-runtime/team-store.d.ts +0 -168
  313. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +0 -43
  314. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +0 -92
  315. package/dist/types/gjc-runtime/team-workers.d.ts +0 -83
  316. package/dist/types/rlm/index.d.ts +0 -47
  317. package/dist/types/rlm/preset.d.ts +0 -23
  318. package/dist/types/rlm/python-tool.d.ts +0 -16
  319. package/src/autoresearch/command-resume.md +0 -14
  320. package/src/autoresearch/index.ts +0 -536
  321. package/src/autoresearch/prompt.md +0 -103
  322. package/src/autoresearch/resume-message.md +0 -10
  323. package/src/autoresearch/state.ts +0 -273
  324. package/src/autoresearch/storage.ts +0 -699
  325. package/src/autoresearch/tools/init-experiment.ts +0 -272
  326. package/src/autoresearch/tools/log-experiment.ts +0 -524
  327. package/src/autoresearch/tools/run-experiment.ts +0 -475
  328. package/src/autoresearch/tools/update-notes.ts +0 -109
  329. package/src/commands/rlm.ts +0 -19
  330. package/src/commands/team.ts +0 -238
  331. package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +0 -42
  332. package/src/defaults/gjc/skills/team/SKILL.md +0 -467
  333. package/src/gjc-runtime/team-gc.ts +0 -49
  334. package/src/gjc-runtime/team-launch.ts +0 -404
  335. package/src/gjc-runtime/team-notify.ts +0 -326
  336. package/src/gjc-runtime/team-runtime.ts +0 -5923
  337. package/src/gjc-runtime/team-store.ts +0 -878
  338. package/src/gjc-runtime/team-worker-heartbeat.ts +0 -149
  339. package/src/gjc-runtime/team-worker-memory-guard.ts +0 -363
  340. package/src/gjc-runtime/team-workers.ts +0 -1123
  341. package/src/prompts/system/rlm-report-command.md +0 -1
  342. package/src/prompts/system/rlm-research.md +0 -23
  343. package/src/rlm/index.ts +0 -365
  344. package/src/rlm/preset.ts +0 -76
  345. package/src/rlm/python-tool.ts +0 -68
@@ -244,6 +244,7 @@ function defaultAvailabilityContext(session: ToolSession): ToolAvailabilityConte
244
244
 
245
245
  function availableFor(name: string, session: ToolSession, context = defaultAvailabilityContext(session)): boolean {
246
246
  if (name === "goal") return context.goalEnabled === true;
247
+ if (name === "move_session") return typeof session.rescopeSessionCwd === "function";
247
248
  if (context.goalStateToolNames?.includes(name)) return context.goalEnabled === true;
248
249
  if (name === "lsp") return (context.enableLsp ?? true) && Boolean(session.settings.get("lsp.enabled"));
249
250
  if (name === "eval") return context.allowEval ?? true;
@@ -296,6 +297,9 @@ function cached(key: string, load: () => Promise<any>): Promise<any> {
296
297
  }
297
298
  return promise;
298
299
  }
300
+ export function evictCachedTool(key: string): void {
301
+ moduleCache.delete(key);
302
+ }
299
303
 
300
304
  const loaders: Record<string, Loader> = {
301
305
  read: session => cached("read", () => import("./read")).then(module => new module.ReadTool(session)),
@@ -324,6 +328,17 @@ const loaders: Record<string, Loader> = {
324
328
  cached("checkpoint", () => import("./checkpoint")).then(module => module.RewindTool.createIf(session)),
325
329
  task: session => cached("task", () => import("../task")).then(module => module.TaskTool.create(session)),
326
330
  subagent: session => cached("subagent", () => import("./subagent")).then(module => new module.SubagentTool(session)),
331
+ python: session =>
332
+ cached("python", () => import("./python")).then(module =>
333
+ module.createSessionPythonTool({
334
+ cwd: session.cwd,
335
+ getCwd: () => session.cwd,
336
+ getSessionId: () => session.getSessionId?.() ?? null,
337
+ registerSessionCleanup: (cleanup: () => Promise<void> | void) => {
338
+ session.registerSessionCleanup?.(cleanup);
339
+ },
340
+ }),
341
+ ),
327
342
  job: session => cached("job", () => import("./job")).then(module => module.JobTool.createIf(session)),
328
343
  monitor: session =>
329
344
  cached("monitor", () => import("./monitor")).then(module => module.MonitorTool.createIf(session)),
@@ -350,6 +365,8 @@ const loaders: Record<string, Loader> = {
350
365
  skill: session => cached("skill", () => import("./skill")).then(module => module.SkillTool.createIf(session)),
351
366
  goal: session =>
352
367
  cached("goal", () => import("../goals/tools/goal-tool")).then(module => new module.GoalTool(session)),
368
+ move_session: session =>
369
+ cached("move_session", () => import("./move-session")).then(module => new module.MoveSessionTool(session)),
353
370
  yield: session => cached("yield", () => import("./yield")).then(module => new module.YieldTool(session)),
354
371
  report_finding: _session => cached("review", () => import("./review")).then(module => module.reportFindingTool),
355
372
  resolve: session => cached("resolve", () => import("./resolve")).then(module => new module.ResolveTool(session)),
@@ -404,6 +421,12 @@ const names: Array<[name: string, label: string, summary: string | undefined, lo
404
421
  ["debug", "Debug", "Debug a running process with DAP (debugger adapter protocol)", "discoverable"],
405
422
  ["bisect", "Bisect", "Find the commit that introduced a regression", "discoverable"],
406
423
  ["eval", "Eval", "Execute Python or JavaScript code in an in-process eval backend", "discoverable"],
424
+ [
425
+ "python",
426
+ "Python",
427
+ "Execute Python in a persistent per-session REPL kernel (every call is appended to the session transcript)",
428
+ "discoverable",
429
+ ],
407
430
  ["calc", "Calc", "Evaluate a mathematical expression", "discoverable"],
408
431
  ["ssh", "SSH", "Execute a command on a remote host over SSH", "discoverable"],
409
432
  ["github", "GitHub", "Interact with GitHub issues, pull requests, and repositories", "discoverable"],
@@ -429,6 +452,7 @@ const names: Array<[name: string, label: string, summary: string | undefined, lo
429
452
  ["write", "Write", "Write content to a file", "discoverable"],
430
453
  ["skill", "Skill", "Chain into another available skill", "essential"],
431
454
  ["goal", "Goal", undefined, "essential"],
455
+ ["move_session", "Move Session", undefined, "essential"],
432
456
  ];
433
457
 
434
458
  const descriptorRawArgumentValidations: Readonly<Record<string, ToolDescriptorMetadata["rawArgumentValidation"]>> = {
@@ -19,6 +19,7 @@ import {
19
19
  ptree,
20
20
  readSseJson,
21
21
  Snowflake,
22
+ sanitizeHeaderComponent,
22
23
  untilAborted,
23
24
  } from "@gajae-code/utils";
24
25
  import * as z from "zod/v4";
@@ -1259,7 +1260,10 @@ function buildOpenAIImageHeaders(model: Model, apiKey: string, sessionId: string
1259
1260
  headers.set(OPENAI_HEADERS.ACCOUNT_ID, accountId);
1260
1261
  headers.set(OPENAI_HEADERS.BETA, OPENAI_HEADER_VALUES.BETA_RESPONSES);
1261
1262
  headers.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX);
1262
- headers.set("User-Agent", `pi/${packageJson.version} (${os.platform()} ${os.release()}; ${os.arch()})`);
1263
+ headers.set(
1264
+ "User-Agent",
1265
+ `pi/${packageJson.version} (${sanitizeHeaderComponent(os.platform())} ${sanitizeHeaderComponent(os.release())}; ${sanitizeHeaderComponent(os.arch())})`,
1266
+ );
1263
1267
  if (sessionId) {
1264
1268
  headers.set(OPENAI_HEADERS.CONVERSATION_ID, sessionId);
1265
1269
  headers.set(OPENAI_HEADERS.SESSION_ID, sessionId);
@@ -32,6 +32,7 @@ import {
32
32
  BUILTIN_TOOLS,
33
33
  HIDDEN_TOOL_DESCRIPTORS,
34
34
  LazyAgentTool,
35
+ PLATFORM_EXCLUDED_TOOL_DESCRIPTORS,
35
36
  resolveEffectiveDiscoveryMode,
36
37
  type ToolAvailabilityContext,
37
38
  } from "./descriptors";
@@ -50,6 +51,18 @@ export type { WriteToolDetails, WriteToolInput } from "./write";
50
51
  /** Tool type (AgentTool from pi-ai) */
51
52
  export type Tool = AgentTool<any, any, any>;
52
53
 
54
+ /** Built-in automation surfaces that an SDK host may back with its own transport. */
55
+ export type AutomationToolName = "browser" | "computer";
56
+
57
+ /**
58
+ * Host-owned implementations for the built-in automation surfaces.
59
+ *
60
+ * Each implementation must use the matching built-in name. The normal
61
+ * AgentTool execute signature carries cancellation through its AbortSignal,
62
+ * while the implementation owns the model-facing schema and description.
63
+ */
64
+ export type AutomationTools = Partial<Record<AutomationToolName, Tool>>;
65
+
53
66
  export type ContextFileEntry = {
54
67
  path: string;
55
68
  content: string;
@@ -179,6 +192,16 @@ export interface ToolSession {
179
192
  /** Pre-loaded workspace tree (forwarded to subagents to skip re-scanning) */
180
193
  workspaceTree?: WorkspaceTree;
181
194
  /** Pre-loaded skills */
195
+ /**
196
+ * Explicit user home for runtime skill discovery. Tests construct sessions
197
+ * against an isolated home because the trusted-home resolver deliberately
198
+ * ignores `$HOME` on Linux (it reads the NSS account database instead), so a
199
+ * `process.env.HOME` override cannot steer user-scope discovery there.
200
+ *
201
+ * Production sessions leave this unset and the trusted OS home governs.
202
+ * Runtime discovery only; never threaded into capability loading.
203
+ */
204
+ home?: string;
182
205
  skills?: Skill[];
183
206
  /** Currently executing skill prompt, when this tool session is inside one. */
184
207
  getActiveSkillState?: () => Pick<SkillActiveEntry, "skill" | "session_id"> | undefined;
@@ -309,6 +332,18 @@ export interface ToolSession {
309
332
  goalToolAllowedOps?: readonly ("create" | "get" | "complete" | "resume" | "drop" | "pause")[];
310
333
  /** Goal runtime for the active agent session. */
311
334
  getGoalRuntime?: () => GoalRuntime | undefined;
335
+ /**
336
+ * Agent-invokable session rescope (issue #4629): move the whole session to
337
+ * an existing directory, running the same sequence as the `/move` handler
338
+ * (flush → moveTo → setProjectDir → plugin/capability cache reset). Absent
339
+ * in contexts where relocation must not happen: subagent sessions
340
+ * (taskDepth > 0) and read-only/restricted bash profiles. Bound to one
341
+ * successful move per session, rejects re-entrant calls, and only narrows:
342
+ * the canonical (realpath) target must be a strict descendant of the
343
+ * canonical current cwd. Throws when the target does not exist, is not a
344
+ * directory, or escapes the current scope.
345
+ */
346
+ rescopeSessionCwd?: (path: string) => Promise<{ from: string; to: string }>;
312
347
  /** Bridge to the connected client (e.g. ACP editor host). Tools should route fs/terminal/permission requests through this when available. */
313
348
  getClientBridge?: () => ClientBridge | undefined;
314
349
  /** Get compact conversation context for subagents (excludes tool results, system prompts) */
@@ -533,7 +568,11 @@ export function resolveEvalBackends(session: ToolSession): EvalBackendsAllowance
533
568
  /**
534
569
  * Create tools from the descriptor registry.
535
570
  */
536
- export async function createTools(session: ToolSession, toolNames?: string[]): Promise<Tool[]> {
571
+ export async function createTools(
572
+ session: ToolSession,
573
+ toolNames?: string[],
574
+ automationTools: AutomationTools = {},
575
+ ): Promise<Tool[]> {
537
576
  const includeYield = session.requireYieldTool === true;
538
577
  const enableLsp = session.enableLsp ?? true;
539
578
  let requestedTools =
@@ -614,8 +653,14 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
614
653
  allowEval,
615
654
  discoveryActive,
616
655
  };
617
- const allToolDescriptors: Record<string, (typeof BUILTIN_TOOL_DESCRIPTORS)[string]> = {
656
+ const builtinToolDescriptors: Record<string, (typeof BUILTIN_TOOL_DESCRIPTORS)[string]> = {
618
657
  ...BUILTIN_TOOL_DESCRIPTORS,
658
+ ...(automationTools.computer && !BUILTIN_TOOL_DESCRIPTORS.computer
659
+ ? { computer: PLATFORM_EXCLUDED_TOOL_DESCRIPTORS.computer }
660
+ : {}),
661
+ };
662
+ const allToolDescriptors: Record<string, (typeof BUILTIN_TOOL_DESCRIPTORS)[string]> = {
663
+ ...builtinToolDescriptors,
619
664
  ...HIDDEN_TOOL_DESCRIPTORS,
620
665
  };
621
666
  const allToolDescriptorEntries = Object.entries(allToolDescriptors) as Array<
@@ -640,13 +685,21 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
640
685
  const filteredRequestedTools = requestedTools
641
686
  ?.map(name => allToolsByRequestName.get(name))
642
687
  .filter((entry): entry is [string, (typeof BUILTIN_TOOL_DESCRIPTORS)[string]] => entry !== undefined)
643
- .filter(([, descriptor]) => descriptor.isAvailable(session, availabilityContext));
688
+ .filter(
689
+ ([name, descriptor]) =>
690
+ automationTools[name as AutomationToolName] !== undefined ||
691
+ descriptor.isAvailable(session, availabilityContext),
692
+ );
644
693
  const baseEntries =
645
694
  filteredRequestedTools !== undefined
646
695
  ? filteredRequestedTools.filter(([name]) => name !== "resolve")
647
696
  : [
648
- ...Object.entries(BUILTIN_TOOL_DESCRIPTORS)
649
- .filter(([, descriptor]) => descriptor.isAvailable(session, availabilityContext))
697
+ ...Object.entries(builtinToolDescriptors)
698
+ .filter(
699
+ ([name, descriptor]) =>
700
+ automationTools[name as AutomationToolName] !== undefined ||
701
+ descriptor.isAvailable(session, availabilityContext),
702
+ )
650
703
  .map(([name, descriptor]) => [name, descriptor] as const),
651
704
  ...(includeYield ? ([["yield", HIDDEN_TOOL_DESCRIPTORS.yield]] as const) : []),
652
705
  ];
@@ -657,6 +710,10 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
657
710
  ),
658
711
  );
659
712
  const materialize = async ([name, descriptor]: readonly [string, (typeof BUILTIN_TOOL_DESCRIPTORS)[string]]) => {
713
+ const externalAutomationTool = automationTools[name as AutomationToolName];
714
+ if (externalAutomationTool) {
715
+ return wrapToolWithMetaNotice(new LazyAgentTool(descriptor, externalAutomationTool, undefined, session));
716
+ }
660
717
  const defer =
661
718
  effectiveDiscoveryMode !== "off" &&
662
719
  descriptor.metadata.loadMode === "discoverable" &&
@@ -0,0 +1,107 @@
1
+ import type { AgentTool, AgentToolResult } from "@gajae-code/agent-core";
2
+ import type { Component } from "@gajae-code/tui";
3
+ import { Text } from "@gajae-code/tui";
4
+ import { prompt, sanitizeText } from "@gajae-code/utils";
5
+ import * as z from "zod/v4";
6
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
7
+ import type { Theme } from "../modes/theme/theme";
8
+ import moveSessionDescription from "../prompts/tools/move-session.md" with { type: "text" };
9
+ import type { ToolSession } from "./index";
10
+ import { Ellipsis, replaceTabs, shortenPath, truncateToWidth } from "./render-utils";
11
+ import { ToolError } from "./tool-errors";
12
+
13
+ const moveSessionSchema = z.object({
14
+ path: z.string().describe("target directory: absolute, or relative to the current session cwd"),
15
+ });
16
+
17
+ export type MoveSessionToolInput = z.infer<typeof moveSessionSchema>;
18
+
19
+ export interface MoveSessionToolDetails {
20
+ from: string;
21
+ to: string;
22
+ }
23
+
24
+ export class MoveSessionTool implements AgentTool<typeof moveSessionSchema, MoveSessionToolDetails> {
25
+ readonly name = "move_session";
26
+ readonly label = "Move Session";
27
+ readonly loadMode = "essential" as const;
28
+ readonly description = prompt.render(moveSessionDescription);
29
+ readonly parameters = moveSessionSchema;
30
+ readonly strict = true;
31
+ readonly intent = "omit" as const;
32
+ // The move chdirs the process and resets global caches; it must not
33
+ // interleave with other tool executions across flush→moveTo→chdir.
34
+ readonly concurrency = "exclusive" as const;
35
+ // The move commits session-file relocation; aborting mid-sequence would
36
+ // leave a half-moved session with no rollback path.
37
+ readonly nonAbortable = true;
38
+ readonly #session: ToolSession;
39
+
40
+ constructor(session: ToolSession) {
41
+ this.#session = session;
42
+ }
43
+
44
+ async execute(_toolCallId: string, params: MoveSessionToolInput): Promise<AgentToolResult<MoveSessionToolDetails>> {
45
+ const rescope = this.#session.rescopeSessionCwd;
46
+ if (!rescope) {
47
+ throw new ToolError(
48
+ "This session cannot rescope its working directory; only top-level unrestrained sessions can move.",
49
+ );
50
+ }
51
+ if (typeof params.path !== "string" || params.path.trim() === "") {
52
+ throw new ToolError("path is required and must be a directory path.");
53
+ }
54
+ let moved: { from: string; to: string };
55
+ try {
56
+ moved = await rescope(params.path);
57
+ } catch (error) {
58
+ throw new ToolError(error instanceof Error ? error.message : String(error));
59
+ }
60
+ return {
61
+ content: [{ type: "text", text: `Session moved to ${moved.to} (from ${moved.from}).` }],
62
+ details: { from: moved.from, to: moved.to },
63
+ };
64
+ }
65
+ }
66
+
67
+ interface MoveSessionRenderArgs {
68
+ from: string;
69
+ to: string;
70
+ }
71
+
72
+ const MOVE_SESSION_PREVIEW_WIDTH = 120;
73
+
74
+ export const moveSessionToolRenderer = {
75
+ renderCall: (args: unknown): Component => {
76
+ const target =
77
+ typeof args === "object" && args !== null && typeof (args as { path?: unknown }).path === "string"
78
+ ? (args as { path: string }).path
79
+ : "";
80
+ const text = truncateToWidth(
81
+ replaceTabs(sanitizeText(`move_session ${target}`)),
82
+ MOVE_SESSION_PREVIEW_WIDTH,
83
+ Ellipsis.Omit,
84
+ );
85
+ return new Text(text, 1, 1);
86
+ },
87
+ renderResult: (
88
+ result: { details?: unknown; isError?: boolean },
89
+ _options: RenderResultOptions & { renderContext?: Record<string, unknown> },
90
+ theme: Theme,
91
+ ): Component => {
92
+ const details = (result.details ?? {}) as Partial<MoveSessionRenderArgs>;
93
+ const from = typeof details.from === "string" ? details.from : "";
94
+ const to = typeof details.to === "string" ? details.to : "";
95
+ const body = result.isError
96
+ ? "move_session failed"
97
+ : `Session moved: ${shortenPath(sanitizeText(from))} → ${shortenPath(sanitizeText(to))}`;
98
+ return new Text(
99
+ theme.fg(
100
+ result.isError ? "error" : "accent",
101
+ truncateToWidth(replaceTabs(sanitizeText(body)), MOVE_SESSION_PREVIEW_WIDTH, Ellipsis.Omit),
102
+ ),
103
+ 1,
104
+ 1,
105
+ );
106
+ },
107
+ };
@@ -0,0 +1,205 @@
1
+ import type { AgentTool, AgentToolResult } from "@gajae-code/agent-core";
2
+ import { type Static, z } from "@gajae-code/ai/core";
3
+ import { logger } from "@gajae-code/utils";
4
+ import { disposeKernelSessionsByOwner, executePython } from "../eval/py/executor";
5
+ import type { ToolDefinition } from "../extensibility/extensions/types";
6
+ import { applyToolProxy } from "../extensibility/tool-proxy";
7
+ import {
8
+ openPythonKernelTranscript,
9
+ type PythonKernelTranscript,
10
+ type PythonTranscriptRecord,
11
+ } from "../gjc-runtime/python-transcript";
12
+ import { sessionIpykernelsArtifactsDir } from "../gjc-runtime/session-layout";
13
+ import pythonToolDescription from "../prompts/tools/python.md" with { type: "text" };
14
+
15
+ export const PYTHON_TOOL_NAME = "python";
16
+
17
+ export function pythonKernelOwnerId(sessionId: string): string {
18
+ return `python:${sessionId}`;
19
+ }
20
+
21
+ export interface SessionPythonToolInput {
22
+ /** Working directory for kernel execution (session cwd). */
23
+ cwd: string;
24
+ /** Resolve the GJC session id used for the kernel owner and transcript paths. */
25
+ getSessionId: () => string | null;
26
+ /** Register cleanup with the current logical session lifecycle. */
27
+ registerSessionCleanup: (cleanup: () => Promise<void> | void) => void;
28
+ }
29
+
30
+ const paramsSchema = z.object({
31
+ action: z
32
+ .enum(["execute", "clear"])
33
+ .default("execute")
34
+ .describe(
35
+ '"execute" runs `code` in the persistent per-session REPL and is the default. "clear" disposes this session\'s kernel; the next execute starts a fresh kernel.',
36
+ ),
37
+ code: z
38
+ .string()
39
+ .optional()
40
+ .describe('Python source to execute when action is "execute" (required then, ignored for "clear").'),
41
+ });
42
+
43
+ const NO_SESSION_ERROR = "Python requires a GJC session id. Start or resume a session before using this tool.";
44
+
45
+ interface TranscriptExecutionResult {
46
+ output: string;
47
+ exitCode: number | null;
48
+ cancelled: boolean;
49
+ truncated: boolean;
50
+ }
51
+
52
+ function errorMessage(error: unknown): string {
53
+ return error instanceof Error ? error.message : String(error);
54
+ }
55
+
56
+ function isCancellationError(error: unknown, signal: AbortSignal | undefined): boolean {
57
+ return (
58
+ signal?.aborted === true ||
59
+ (error instanceof DOMException && (error.name === "AbortError" || error.name === "TimeoutError")) ||
60
+ (error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError"))
61
+ );
62
+ }
63
+
64
+ function appendFailureTrailer(output: string, appendFailure: string | undefined): string {
65
+ if (appendFailure === undefined) return output;
66
+ const trailer = `[transcript append failed: ${appendFailure}]`;
67
+ return output.length > 0 ? `${output}\n${trailer}` : trailer;
68
+ }
69
+
70
+ export function createSessionPythonTool(input: SessionPythonToolInput): AgentTool {
71
+ let armedForSession: string | null = null;
72
+ const seenOwnerIds = new Set<string>();
73
+ let currentTranscript: PythonKernelTranscript | null = null;
74
+
75
+ const armCleanupForSession = (sessionId: string): void => {
76
+ if (armedForSession === sessionId) return;
77
+ input.registerSessionCleanup(async () => {
78
+ await Promise.all([...seenOwnerIds].map(ownerId => disposeKernelSessionsByOwner(ownerId)));
79
+ seenOwnerIds.clear();
80
+ currentTranscript = null;
81
+ armedForSession = null;
82
+ });
83
+ armedForSession = sessionId;
84
+ };
85
+
86
+ const appendTranscript = async (
87
+ sessionId: string,
88
+ code: string,
89
+ result: TranscriptExecutionResult,
90
+ ): Promise<string | undefined> => {
91
+ if (currentTranscript === null) {
92
+ currentTranscript = openPythonKernelTranscript({
93
+ cwd: input.cwd,
94
+ sessionId,
95
+ kernelInstanceId: crypto.randomUUID(),
96
+ });
97
+ }
98
+ const record: PythonTranscriptRecord = {
99
+ timestamp: new Date().toISOString(),
100
+ code,
101
+ output: result.output,
102
+ exitCode: result.exitCode,
103
+ cancelled: result.cancelled,
104
+ truncated: result.truncated,
105
+ };
106
+ try {
107
+ await currentTranscript.append(record);
108
+ return undefined;
109
+ } catch (error) {
110
+ const message = errorMessage(error);
111
+ logger.warn("Python transcript append failed", { sessionId, error: message });
112
+ return message;
113
+ }
114
+ };
115
+
116
+ const definition: ToolDefinition<typeof paramsSchema> = {
117
+ name: PYTHON_TOOL_NAME,
118
+ label: "Python",
119
+ description: pythonToolDescription,
120
+ parameters: paramsSchema,
121
+ defaultInactive: true,
122
+ concurrency: "exclusive",
123
+ async execute(
124
+ _toolCallId: string,
125
+ params: Static<typeof paramsSchema>,
126
+ signal?: AbortSignal,
127
+ ): Promise<AgentToolResult> {
128
+ const sessionId = input.getSessionId();
129
+ if (sessionId === null) {
130
+ return {
131
+ content: [{ type: "text", text: NO_SESSION_ERROR }],
132
+ isError: true,
133
+ };
134
+ }
135
+ armCleanupForSession(sessionId);
136
+ const ownerId = pythonKernelOwnerId(sessionId);
137
+ if (params.action === "clear") {
138
+ await disposeKernelSessionsByOwner(ownerId);
139
+ currentTranscript = null;
140
+ return {
141
+ content: [{ type: "text", text: "Python kernel cleared; the next execute starts a fresh kernel." }],
142
+ };
143
+ }
144
+ const code = params.code;
145
+ if (code === undefined) {
146
+ return {
147
+ content: [{ type: "text", text: 'Missing required "code" parameter for action "execute".' }],
148
+ isError: true,
149
+ };
150
+ }
151
+
152
+ seenOwnerIds.add(ownerId);
153
+ try {
154
+ const result = await executePython(code, {
155
+ cwd: input.cwd,
156
+ kernelMode: "session",
157
+ sessionId: ownerId,
158
+ kernelOwnerId: ownerId,
159
+ artifactsDir: sessionIpykernelsArtifactsDir(input.cwd, sessionId),
160
+ signal,
161
+ onKernelStart: kernelInstanceId => {
162
+ if (currentTranscript?.kernelInstanceId !== kernelInstanceId) {
163
+ currentTranscript = openPythonKernelTranscript({
164
+ cwd: input.cwd,
165
+ sessionId,
166
+ kernelInstanceId,
167
+ });
168
+ }
169
+ },
170
+ });
171
+ const appendFailure = await appendTranscript(sessionId, code, {
172
+ output: result.output,
173
+ exitCode: result.exitCode ?? null,
174
+ cancelled: result.cancelled,
175
+ truncated: result.truncated,
176
+ });
177
+ const output = result.output.length > 0 ? result.output : "(no output)";
178
+ return { content: [{ type: "text", text: appendFailureTrailer(output, appendFailure) }] };
179
+ } catch (error) {
180
+ const output = errorMessage(error);
181
+ const appendFailure = await appendTranscript(sessionId, code, {
182
+ output,
183
+ exitCode: null,
184
+ cancelled: isCancellationError(error, signal),
185
+ truncated: false,
186
+ });
187
+ return {
188
+ content: [{ type: "text", text: appendFailureTrailer(output, appendFailure) }],
189
+ isError: true,
190
+ };
191
+ }
192
+ },
193
+ };
194
+ const agentTool = {
195
+ async execute(
196
+ toolCallId: string,
197
+ params: Static<typeof paramsSchema>,
198
+ signal?: AbortSignal,
199
+ ): Promise<AgentToolResult> {
200
+ return definition.execute(toolCallId, params, signal, undefined, undefined as never);
201
+ },
202
+ } as AgentTool;
203
+ applyToolProxy(definition, agentTool);
204
+ return agentTool;
205
+ }
package/src/tools/read.ts CHANGED
@@ -1429,18 +1429,28 @@ interface ResolvedSqliteReadPath {
1429
1429
  * Directories return a formatted listing with modification times.
1430
1430
  */
1431
1431
  /**
1432
- * A client-authority denial is a decision, not a transport failure: falling back to
1433
- * disk would bypass the permission the client just refused. Availability failures
1434
- * still fall back so an unreachable bridge cannot break local reads.
1432
+ * Only an explicit bridge transport-unavailable marker authorizes falling back to
1433
+ * the agent host's disk. Raw OS errno values, including EACCES/EPERM, are
1434
+ * ambiguous at this boundary: treating them as transport failures can bypass a
1435
+ * remote client's access decision by reading the path locally.
1435
1436
  */
1436
- function isClientAuthorityDenial(error: unknown): boolean {
1437
- const code =
1437
+ function isExplicitTransportUnavailable(error: unknown): boolean {
1438
+ const directCode =
1438
1439
  typeof error === "object" && error !== null && "code" in error ? (error as { code?: unknown }).code : undefined;
1439
- // ACP clients surface refusals as an application error; -32001 is the reserved
1440
- // client-authority denial code and -32603 covers hosts without a dedicated code.
1441
- if (code === "permission_denied" || code === "forbidden" || code === -32001 || code === -32603) return true;
1442
- const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
1443
- return /permission denied|not permitted|access denied|forbidden/i.test(message);
1440
+ const namedCode = error instanceof Error ? error.name : undefined;
1441
+ const nestedCode =
1442
+ typeof error === "object" &&
1443
+ error !== null &&
1444
+ "data" in error &&
1445
+ typeof (error as { data?: unknown }).data === "object"
1446
+ ? ((error as { data?: { code?: unknown } }).data?.code ?? undefined)
1447
+ : undefined;
1448
+ const code = directCode ?? nestedCode ?? namedCode;
1449
+ return code === "transport_unavailable" || code === "bridge_unavailable";
1450
+ }
1451
+
1452
+ function isClientAuthorityDenial(error: unknown): boolean {
1453
+ return !isExplicitTransportUnavailable(error);
1444
1454
  }
1445
1455
 
1446
1456
  export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
@@ -2395,6 +2405,53 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2395
2405
  if (!bridge?.capabilities.readTextFile || !bridge.readTextFile) return undefined;
2396
2406
  return bridge.readTextFile({ path: absolutePath, ...options });
2397
2407
  }
2408
+ async #readMissingPathThroughBridge(
2409
+ absolutePath: string,
2410
+ parsed: ParsedSelector,
2411
+ _localReadPath: string,
2412
+ truncation: TruncationDirection | undefined,
2413
+ signal: AbortSignal | undefined,
2414
+ ): Promise<AgentToolResult<ReadToolDetails> | undefined> {
2415
+ const bridgePromise = this.#routeReadThroughBridge(absolutePath);
2416
+ if (bridgePromise === undefined) return undefined;
2417
+ throwIfAborted(signal);
2418
+ try {
2419
+ const bridgeText = await bridgePromise;
2420
+ if (parsed.kind === "conflicts") return undefined;
2421
+ const bareEligible = parsed.kind === "none";
2422
+ const route = isMultiRange(parsed)
2423
+ ? "local-multi-range"
2424
+ : bareEligible
2425
+ ? "local-bare-stream"
2426
+ : isRawSelector(parsed)
2427
+ ? "local-raw"
2428
+ : "local-range";
2429
+ const direction = resolveEffectiveDirection(truncation, route, this.session.settings);
2430
+ if (isMultiRange(parsed) && parsed.kind === "lines") {
2431
+ return this.#buildInMemoryMultiRangeResult(bridgeText, parsed.ranges, {
2432
+ details: { resolvedPath: absolutePath },
2433
+ sourcePath: absolutePath,
2434
+ entityLabel: "file",
2435
+ raw: isRawSelector(parsed),
2436
+ truncationDirection: direction,
2437
+ cacheLinesFor: absolutePath,
2438
+ });
2439
+ }
2440
+ const { offset, limit } = selToOffsetLimit(parsed);
2441
+ return this.#buildInMemoryTextResult(bridgeText, offset, limit, {
2442
+ details: { resolvedPath: absolutePath },
2443
+ sourcePath: absolutePath,
2444
+ entityLabel: "file",
2445
+ raw: isRawSelector(parsed),
2446
+ cacheLinesFor: absolutePath,
2447
+ truncationDirection: direction,
2448
+ });
2449
+ } catch (error) {
2450
+ if (isClientAuthorityDenial(error)) throw error;
2451
+ logger.warn("ACP fs readTextFile failed for a path missing on disk", { path: absolutePath, error });
2452
+ return undefined;
2453
+ }
2454
+ }
2398
2455
 
2399
2456
  async #trySummarize(absolutePath: string, fileSize: number, signal?: AbortSignal): Promise<SummaryResult | null> {
2400
2457
  if (fileSize > MAX_SUMMARY_BYTES) return null;
@@ -2402,10 +2459,17 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2402
2459
  try {
2403
2460
  throwIfAborted(signal);
2404
2461
  const bridgePromise = this.#routeReadThroughBridge(absolutePath);
2405
- const code =
2406
- bridgePromise !== undefined
2407
- ? await bridgePromise.catch(() => Bun.file(absolutePath).text())
2408
- : await Bun.file(absolutePath).text();
2462
+ let code: string;
2463
+ if (bridgePromise !== undefined) {
2464
+ try {
2465
+ code = await bridgePromise;
2466
+ } catch (error) {
2467
+ if (isClientAuthorityDenial(error)) throw error;
2468
+ code = await Bun.file(absolutePath).text();
2469
+ }
2470
+ } else {
2471
+ code = await Bun.file(absolutePath).text();
2472
+ }
2409
2473
  throwIfAborted(signal);
2410
2474
  if (countTextLines(code) > MAX_SUMMARY_LINES) return null;
2411
2475
 
@@ -2416,7 +2480,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2416
2480
  minBodyLines: this.session.settings.get("read.summarize.minBodyLines"),
2417
2481
  minCommentLines: this.session.settings.get("read.summarize.minCommentLines"),
2418
2482
  });
2419
- } catch {
2483
+ } catch (error) {
2484
+ if (isClientAuthorityDenial(error)) throw error;
2420
2485
  return null;
2421
2486
  }
2422
2487
  }
@@ -2663,6 +2728,14 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2663
2728
  isDirectory = stat.isDirectory();
2664
2729
  } catch (error) {
2665
2730
  if (isNotFoundError(error)) {
2731
+ const bridged = await this.#readMissingPathThroughBridge(
2732
+ absolutePath,
2733
+ parsed,
2734
+ localReadPath,
2735
+ params.truncation,
2736
+ signal,
2737
+ );
2738
+ if (bridged) return bridged;
2666
2739
  // Attempt unique suffix resolution before falling back to fuzzy suggestions
2667
2740
  if (!isRemoteMountPath(absolutePath)) {
2668
2741
  const suffixMatch = await findUniqueSuffixMatch(localReadPath, this.session.cwd, signal);
@@ -23,6 +23,7 @@ import { evalToolRenderer } from "./eval";
23
23
  import { findToolRenderer } from "./find";
24
24
  import { githubToolRenderer } from "./gh-renderer";
25
25
  import { jobToolRenderer } from "./job";
26
+ import { moveSessionToolRenderer } from "./move-session";
26
27
  import { readToolRenderer } from "./read";
27
28
  import { recipeToolRenderer } from "./recipe/render";
28
29
  import { resolveToolRenderer } from "./resolve";
@@ -72,6 +73,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
72
73
  todo_write: todoWriteToolRenderer as ToolRenderer,
73
74
  github: githubToolRenderer as ToolRenderer,
74
75
  goal: goalToolRenderer as ToolRenderer,
76
+ move_session: moveSessionToolRenderer as ToolRenderer,
75
77
  web_search: webSearchToolRenderer as ToolRenderer,
76
78
  write: writeToolRenderer as ToolRenderer,
77
79
  };