@gajae-code/coding-agent 0.18.7 → 0.18.9

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 (344) hide show
  1. package/CHANGELOG.md +377 -1
  2. package/dist/types/async/endpoint-id.d.ts +16 -0
  3. package/dist/types/async/job-manager.d.ts +1 -1
  4. package/dist/types/async/support.d.ts +0 -14
  5. package/dist/types/capability/mcp.d.ts +2 -0
  6. package/dist/types/capability/skill.d.ts +2 -0
  7. package/dist/types/capability/types.d.ts +2 -0
  8. package/dist/types/cli/public-command-errors.d.ts +3 -1
  9. package/dist/types/cli/setup-cli.d.ts +1 -1
  10. package/dist/types/commands/deep-interview.d.ts +3 -0
  11. package/dist/types/commands/mcp-serve.d.ts +4 -0
  12. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  13. package/dist/types/config/file-lock.d.ts +1 -0
  14. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  15. package/dist/types/config/model-profile-activation.d.ts +11 -3
  16. package/dist/types/config/model-registry.d.ts +4 -9
  17. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  18. package/dist/types/cursor-delete-path.d.ts +1 -0
  19. package/dist/types/cursor-mcp-name.d.ts +8 -0
  20. package/dist/types/customization/inventory.d.ts +2 -0
  21. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  22. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  23. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  24. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  25. package/dist/types/discovery/helpers.d.ts +6 -1
  26. package/dist/types/discovery/mcp-json.d.ts +1 -0
  27. package/dist/types/eval/py/kernel.d.ts +18 -0
  28. package/dist/types/export/html/css-color.d.ts +2 -0
  29. package/dist/types/export/html/html-export-destination.d.ts +2 -0
  30. package/dist/types/export/html/template.generated.d.ts +1 -1
  31. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +5 -0
  32. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  33. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  34. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  35. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  36. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  37. package/dist/types/extensibility/skill-management.d.ts +4 -1
  38. package/dist/types/extensibility/skills.d.ts +2 -0
  39. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  40. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  41. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  42. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -3
  43. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  44. package/dist/types/gjc-runtime/sidecar-bootstrap-env.d.ts +6 -0
  45. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  46. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  47. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  48. package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
  49. package/dist/types/gjc-runtime/tmux-common.d.ts +8 -5
  50. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  51. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  52. package/dist/types/lsp/config.d.ts +8 -5
  53. package/dist/types/lsp/workspace-path.d.ts +1 -0
  54. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  55. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  56. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  57. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  58. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  59. package/dist/types/modes/interactive-mode.d.ts +1 -0
  60. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  61. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  62. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  63. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  64. package/dist/types/progress/progress-contract.d.ts +154 -0
  65. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  66. package/dist/types/progress/project-progress.d.ts +141 -0
  67. package/dist/types/progress/render-progress.d.ts +28 -0
  68. package/dist/types/runtime-mcp/manager.d.ts +2 -1
  69. package/dist/types/runtime-mcp/mcp-redirect.d.ts +9 -0
  70. package/dist/types/runtime-mcp/oauth-public-url.d.ts +3 -0
  71. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  72. package/dist/types/runtime-mcp/project-host-values.d.ts +27 -0
  73. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  74. package/dist/types/runtime-mcp/types.d.ts +6 -0
  75. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  76. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  77. package/dist/types/sdk/broker/broker.d.ts +24 -2
  78. package/dist/types/sdk/broker/doctor-restart.d.ts +5 -0
  79. package/dist/types/sdk/broker/ensure.d.ts +14 -1
  80. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  81. package/dist/types/sdk/bus/index.d.ts +12 -1
  82. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  83. package/dist/types/sdk/client/client.d.ts +2 -0
  84. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  85. package/dist/types/sdk/index.d.ts +1 -0
  86. package/dist/types/sdk/router/session-router.d.ts +2 -0
  87. package/dist/types/sdk/session-list.d.ts +1 -1
  88. package/dist/types/sdk/session.d.ts +8 -0
  89. package/dist/types/session/agent-session.d.ts +38 -9
  90. package/dist/types/session/artifacts.d.ts +2 -2
  91. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  92. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  93. package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
  94. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  95. package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
  96. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
  97. package/dist/types/session/session-manager.d.ts +31 -5
  98. package/dist/types/session/session-retirement.d.ts +31 -7
  99. package/dist/types/session/session-storage.d.ts +69 -2
  100. package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
  101. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  102. package/dist/types/setup/credential-import.d.ts +6 -4
  103. package/dist/types/skill-state/active-state.d.ts +38 -0
  104. package/dist/types/skill-state/workflow-mutation-guard.d.ts +3 -0
  105. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  106. package/dist/types/task/executor.d.ts +3 -0
  107. package/dist/types/task/isolation-dir.d.ts +26 -0
  108. package/dist/types/tools/ast-edit-write-path.d.ts +13 -0
  109. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  110. package/dist/types/tools/browser/cdp-url.d.ts +2 -0
  111. package/dist/types/tools/github-side-effect.d.ts +22 -0
  112. package/dist/types/tools/index.d.ts +3 -0
  113. package/dist/types/tools/monitor-mutation.d.ts +9 -0
  114. package/dist/types/tools/python.d.ts +9 -1
  115. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  116. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  117. package/dist/types/tools/todo-write.d.ts +6 -0
  118. package/dist/types/utils/tools-checksum.d.ts +7 -0
  119. package/package.json +7 -7
  120. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  121. package/scripts/verify-managed-empty-stop.ts +28 -0
  122. package/src/async/endpoint-id.ts +26 -0
  123. package/src/async/job-manager.ts +247 -132
  124. package/src/async/support.ts +0 -25
  125. package/src/capability/index.ts +15 -0
  126. package/src/capability/mcp.ts +2 -0
  127. package/src/capability/skill.ts +2 -0
  128. package/src/capability/types.ts +2 -0
  129. package/src/cli/auth-broker-cli.ts +1 -1
  130. package/src/cli/customize-doctor.ts +15 -2
  131. package/src/cli/doctor/repairs.ts +4 -1
  132. package/src/cli/doctor-supervisor.ts +3 -0
  133. package/src/cli/public-command-errors.ts +21 -1
  134. package/src/cli/public-command-registry.ts +28 -6
  135. package/src/cli/setup-cli.ts +4 -4
  136. package/src/cli/skills-cli.ts +5 -0
  137. package/src/commands/deep-interview.ts +47 -8
  138. package/src/commands/mcp-serve.ts +7 -0
  139. package/src/commands/sdk.ts +455 -397
  140. package/src/config/autorouting-tier-map.ts +117 -7
  141. package/src/config/file-lock.ts +316 -79
  142. package/src/config/mcp-schema.json +4 -0
  143. package/src/config/model-bindings-applier.ts +20 -0
  144. package/src/config/model-profile-activation.ts +240 -46
  145. package/src/config/model-registry.ts +47 -153
  146. package/src/config/skill-settings-defaults.ts +7 -0
  147. package/src/coordinator-mcp/policy.ts +3 -2
  148. package/src/coordinator-mcp/server.ts +2 -0
  149. package/src/cursor-delete-path.ts +10 -0
  150. package/src/cursor-mcp-name.ts +15 -0
  151. package/src/cursor.ts +13 -2
  152. package/src/customization/inventory.ts +4 -13
  153. package/src/dap/config.ts +2 -2
  154. package/src/debug/report-bundle-parent.ts +22 -0
  155. package/src/debug/report-bundle.ts +3 -0
  156. package/src/debug/system-info.ts +13 -1
  157. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +10 -6
  158. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  159. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  160. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  161. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  162. package/src/defaults/gjc-defaults.test.ts +18 -1
  163. package/src/defaults/gjc-defaults.ts +3 -0
  164. package/src/defaults/gjc-skills.generated.ts +58 -36
  165. package/src/discovery/agents.ts +6 -1
  166. package/src/discovery/builtin.ts +13 -1
  167. package/src/discovery/helpers.ts +171 -29
  168. package/src/discovery/mcp-json.ts +26 -12
  169. package/src/discovery/ssh.ts +1 -1
  170. package/src/discovery/substitute-plugin-root.ts +3 -1
  171. package/src/edit/file-read-cache.ts +1 -1
  172. package/src/eval/py/executor.ts +796 -154
  173. package/src/eval/py/kernel.ts +79 -13
  174. package/src/eval/py/tool-bridge.ts +4 -0
  175. package/src/export/html/css-color.ts +9 -0
  176. package/src/export/html/html-export-destination.ts +16 -0
  177. package/src/export/html/index.ts +7 -10
  178. package/src/export/html/template.generated.ts +1 -1
  179. package/src/extensibility/extensions/function-hooks-internal.ts +27 -0
  180. package/src/extensibility/extensions/runner.ts +15 -1
  181. package/src/extensibility/extensions/types.ts +7 -0
  182. package/src/extensibility/gjc-plugins/mcp-policy.ts +2 -1
  183. package/src/extensibility/gjc-plugins/runtime-adapters.ts +85 -8
  184. package/src/extensibility/plugins/manager.ts +3 -2
  185. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  186. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  187. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  188. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  189. package/src/extensibility/skill-management.ts +9 -2
  190. package/src/extensibility/skills.ts +6 -1
  191. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  192. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  193. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  194. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  195. package/src/gjc-runtime/gc-runtime.ts +206 -9
  196. package/src/gjc-runtime/launch-worktree.ts +70 -9
  197. package/src/gjc-runtime/psmux-detect.ts +28 -4
  198. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  199. package/src/gjc-runtime/session-state-sidecar.ts +3 -2
  200. package/src/gjc-runtime/sidecar-bootstrap-env.ts +19 -0
  201. package/src/gjc-runtime/state-argv.ts +2 -0
  202. package/src/gjc-runtime/state-migrations.ts +91 -29
  203. package/src/gjc-runtime/state-runtime.ts +3716 -135
  204. package/src/gjc-runtime/state-writer.ts +105 -16
  205. package/src/gjc-runtime/tmux-common.ts +8 -5
  206. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  207. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  208. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  209. package/src/harness-control-plane/operate.ts +12 -4
  210. package/src/harness-control-plane/owner.ts +5 -1
  211. package/src/hashline/parser.ts +48 -9
  212. package/src/hashline/recovery.ts +13 -4
  213. package/src/internal-urls/docs-index.generated.ts +20 -19
  214. package/src/internal-urls/skill-path.ts +19 -0
  215. package/src/internal-urls/skill-protocol.ts +2 -0
  216. package/src/lsp/config.ts +12 -63
  217. package/src/lsp/edits.ts +6 -0
  218. package/src/lsp/workspace-path.ts +51 -0
  219. package/src/main.ts +195 -31
  220. package/src/memories/index.ts +88 -7
  221. package/src/modes/acp/acp-agent.ts +88 -4
  222. package/src/modes/components/custom-editor.ts +14 -0
  223. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  224. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  225. package/src/modes/components/extensions/state-manager.ts +15 -5
  226. package/src/modes/components/hook-editor.ts +1 -1
  227. package/src/modes/components/hook-selector.ts +1 -1
  228. package/src/modes/components/model-selector.ts +1 -4
  229. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  230. package/src/modes/components/status-line/segments.ts +4 -0
  231. package/src/modes/controllers/event-controller.ts +11 -4
  232. package/src/modes/controllers/input-controller.ts +76 -31
  233. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -16
  234. package/src/modes/controllers/selector-controller.ts +24 -15
  235. package/src/modes/controllers/todo-command-controller.ts +1 -1
  236. package/src/modes/interactive-mode.ts +5 -1
  237. package/src/modes/runtime-init.ts +4 -0
  238. package/src/modes/utils/abort-message.ts +3 -1
  239. package/src/modes/utils/interrupt-ownership.ts +12 -0
  240. package/src/modes/utils/keybinding-matchers.ts +4 -0
  241. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  242. package/src/progress/collect-project-progress.ts +153 -0
  243. package/src/progress/progress-contract.ts +172 -0
  244. package/src/progress/progress-snapshot.ts +140 -0
  245. package/src/progress/project-progress.ts +446 -0
  246. package/src/progress/render-progress.ts +209 -0
  247. package/src/prompts/system/master-mode.md +1 -0
  248. package/src/prompts/tools/skill-discovery.md +1 -1
  249. package/src/runtime-mcp/client.ts +4 -2
  250. package/src/runtime-mcp/config-writer.ts +10 -6
  251. package/src/runtime-mcp/config.ts +1 -0
  252. package/src/runtime-mcp/json-rpc.ts +2 -1
  253. package/src/runtime-mcp/manager.ts +29 -11
  254. package/src/runtime-mcp/mcp-redirect.ts +73 -0
  255. package/src/runtime-mcp/oauth-flow.ts +14 -2
  256. package/src/runtime-mcp/oauth-public-url.ts +14 -0
  257. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  258. package/src/runtime-mcp/pool.ts +6 -0
  259. package/src/runtime-mcp/project-host-values.ts +46 -0
  260. package/src/runtime-mcp/smithery-registry.ts +22 -3
  261. package/src/runtime-mcp/transports/http.ts +4 -3
  262. package/src/runtime-mcp/types.ts +6 -0
  263. package/src/sdk/acp/adapter.ts +95 -8
  264. package/src/sdk/broker/broker-exit.ts +52 -23
  265. package/src/sdk/broker/broker.ts +81 -6
  266. package/src/sdk/broker/doctor-restart.ts +4 -0
  267. package/src/sdk/broker/ensure.ts +31 -2
  268. package/src/sdk/broker/lifecycle.ts +711 -66
  269. package/src/sdk/broker/session-index.ts +191 -81
  270. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  271. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  272. package/src/sdk/bus/index.ts +238 -43
  273. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  274. package/src/sdk/cli/session-cli.ts +17 -3
  275. package/src/sdk/client/client.ts +28 -8
  276. package/src/sdk/host/query/handlers.ts +9 -1
  277. package/src/sdk/host/session-runtime.test.ts +2705 -303
  278. package/src/sdk/host/session-runtime.ts +490 -143
  279. package/src/sdk/host/surface-policy.ts +1 -0
  280. package/src/sdk/index.ts +8 -0
  281. package/src/sdk/lifecycle-session.ts +3 -1
  282. package/src/sdk/prompt-deadline-manager.ts +15 -0
  283. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  284. package/src/sdk/protocol/operation-registry.ts +9 -2
  285. package/src/sdk/router/session-router.ts +82 -18
  286. package/src/sdk/session-list.ts +1 -0
  287. package/src/sdk/session.ts +69 -45
  288. package/src/session/agent-session.ts +679 -238
  289. package/src/session/artifacts.ts +176 -71
  290. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  291. package/src/session/internal/managed-session-scope.ts +3779 -462
  292. package/src/session/internal/managed-session-storage.ts +501 -78
  293. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  294. package/src/session/internal/task-artifact-owner-access.ts +19 -3
  295. package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
  296. package/src/session/session-manager.ts +951 -133
  297. package/src/session/session-retirement.ts +573 -14
  298. package/src/session/session-storage.ts +447 -15
  299. package/src/session/task-artifact-owner-codec.ts +71 -8
  300. package/src/session/task-artifact-owner-retirement.ts +21 -19
  301. package/src/setup/credential-auto-import.ts +12 -3
  302. package/src/setup/credential-import.ts +176 -5
  303. package/src/setup/paseo/announce.ts +6 -1
  304. package/src/skill-state/active-state.ts +145 -2
  305. package/src/skill-state/workflow-mutation-guard.ts +102 -10
  306. package/src/slash-commands/builtin-registry.ts +39 -9
  307. package/src/slash-commands/helpers/mcp.ts +8 -4
  308. package/src/slash-commands/helpers/progress-report.ts +23 -0
  309. package/src/stt/setup.ts +3 -5
  310. package/src/task/commands.ts +3 -1
  311. package/src/task/executor.ts +4 -0
  312. package/src/task/index.ts +6 -0
  313. package/src/task/isolation-dir.ts +143 -0
  314. package/src/task/worktree.ts +16 -7
  315. package/src/tools/ask.ts +205 -6
  316. package/src/tools/ast-edit-write-path.ts +68 -0
  317. package/src/tools/ast-edit.ts +10 -1
  318. package/src/tools/bash-allowed-prefixes.ts +15 -0
  319. package/src/tools/bash-skill-urls.ts +56 -3
  320. package/src/tools/bash.ts +48 -79
  321. package/src/tools/browser/cdp-url.ts +18 -0
  322. package/src/tools/browser.ts +8 -1
  323. package/src/tools/descriptors.ts +5 -1
  324. package/src/tools/eval.ts +56 -36
  325. package/src/tools/gh.ts +10 -0
  326. package/src/tools/github-side-effect.ts +112 -0
  327. package/src/tools/image-gen.ts +1 -1
  328. package/src/tools/index.ts +3 -0
  329. package/src/tools/monitor-mutation.ts +27 -0
  330. package/src/tools/monitor.ts +7 -0
  331. package/src/tools/python.ts +188 -66
  332. package/src/tools/recipe/index.ts +9 -0
  333. package/src/tools/recipe/recipe-guard.ts +19 -0
  334. package/src/tools/restricted-cwd.ts +10 -0
  335. package/src/tools/skill-discovery.ts +5 -0
  336. package/src/tools/skill.ts +28 -1
  337. package/src/tools/todo-write.ts +10 -1
  338. package/src/tools/tool-catalog.generated.ts +1 -1
  339. package/src/tools/write.ts +1 -0
  340. package/src/utils/external-editor.ts +3 -2
  341. package/src/utils/tools-checksum.ts +34 -0
  342. package/src/utils/tools-manager.ts +24 -6
  343. package/src/web/insane/url-guard.ts +2 -1
  344. package/src/web/search/providers/codex.ts +3 -2
@@ -0,0 +1,19 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+
4
+ export async function realSkillPath(baseDir: string, targetPath: string): Promise<string> {
5
+ const realBase = await fs.realpath(baseDir);
6
+ let realTarget: string;
7
+ try {
8
+ realTarget = await fs.realpath(targetPath);
9
+ } catch (error) {
10
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
11
+ throw new Error(`File not found: ${targetPath}`);
12
+ }
13
+ throw error;
14
+ }
15
+ if (realTarget !== realBase && !realTarget.startsWith(realBase + path.sep)) {
16
+ throw new Error("Path traversal is not allowed");
17
+ }
18
+ return realTarget;
19
+ }
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import * as path from "node:path";
11
11
  import { getActiveSkills } from "../extensibility/skills";
12
+ import { realSkillPath } from "./skill-path";
12
13
  import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
13
14
 
14
15
  function getContentType(filePath: string): InternalResource["contentType"] {
@@ -70,6 +71,7 @@ export class SkillProtocolHandler implements ProtocolHandler {
70
71
  if (!resolvedPath.startsWith(resolvedBaseDir + path.sep) && resolvedPath !== resolvedBaseDir) {
71
72
  throw new Error("Path traversal is not allowed");
72
73
  }
74
+ targetPath = await realSkillPath(skill.baseDir, targetPath);
73
75
  } else {
74
76
  targetPath = skill.filePath;
75
77
  }
package/src/lsp/config.ts CHANGED
@@ -276,75 +276,19 @@ export function hasRootMarkers(cwd: string, markers: string[]): boolean {
276
276
  }
277
277
 
278
278
  // =============================================================================
279
- // Local Binary Resolution
279
+ // Trusted executable resolution
280
280
  // =============================================================================
281
281
 
282
282
  /**
283
- * Local bin directories to check before $PATH, ordered by priority.
284
- * Each entry maps a root marker to the bin directory to check.
285
- */
286
- const LOCAL_BIN_PATHS: Array<{ markers: string[]; binDir: string }> = [
287
- // Node.js - check node_modules/.bin/
288
- { markers: ["package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml"], binDir: "node_modules/.bin" },
289
- // Python - check virtual environment bin directories
290
- { markers: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], binDir: ".venv/bin" },
291
- { markers: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], binDir: "venv/bin" },
292
- { markers: ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"], binDir: ".env/bin" },
293
- // Ruby - check vendor bundle and binstubs
294
- { markers: ["Gemfile", "Gemfile.lock"], binDir: "vendor/bundle/bin" },
295
- { markers: ["Gemfile", "Gemfile.lock"], binDir: "bin" },
296
- // Go - check project-local bin
297
- { markers: ["go.mod", "go.sum"], binDir: "bin" },
298
- ];
299
-
300
- const WINDOWS_LOCAL_EXECUTABLE_EXTENSIONS = [".exe", ".cmd", ".bat"] as const;
301
-
302
- function resolveLocalCommand(basePath: string): string | null {
303
- if (fs.existsSync(basePath)) return basePath;
304
- if (process.platform !== "win32") return null;
305
-
306
- // Package managers write Windows launchers with executable suffixes in node_modules/.bin.
307
- for (const extension of WINDOWS_LOCAL_EXECUTABLE_EXTENSIONS) {
308
- const candidate = `${basePath}${extension}`;
309
- if (fs.existsSync(candidate)) return candidate;
310
- }
311
-
312
- return null;
313
- }
314
-
315
- /**
316
- * Resolve a command to an executable path.
317
- * Checks project-local bin directories first, then falls back to $PATH.
318
- *
319
- * @param command - The command name (e.g., "typescript-language-server")
320
- * @param cwd - Working directory to search from
321
- * @returns Absolute path to the executable, or null if not found
322
- */
323
- export function resolveCommand(command: string, cwd: string): string | null {
324
- // Check local bin directories based on project markers
325
- for (const { markers, binDir } of LOCAL_BIN_PATHS) {
326
- if (hasRootMarkers(cwd, markers)) {
327
- const localPath = path.join(cwd, binDir, command);
328
- const resolvedLocalPath = resolveLocalCommand(localPath);
329
- if (resolvedLocalPath) {
330
- return resolvedLocalPath;
331
- }
332
- }
333
- }
334
-
335
- // Fall back to $PATH
336
- return piUtils.$which(command);
337
- }
338
-
339
- /**
340
- * Resolve an LSP executable without consulting project-controlled bin directories.
283
+ * Resolve a host-spawn executable from $PATH without consulting project-controlled bin directories.
284
+ * LSP auto-detection and DAP adapter launch both use this rule.
341
285
  *
342
286
  * Trust is evaluated on both the discovered and canonical paths, but the returned
343
287
  * path is the discovered one: multiplexed launchers such as rustup pick the proxied
344
288
  * tool from argv[0], so `~/.cargo/bin/rust-analyzer -> rustup` must be spawned as
345
289
  * `rust-analyzer`, not as the resolved `rustup` binary.
346
290
  */
347
- function resolveTrustedLspCommand(command: string, cwd: string): string | null {
291
+ export function resolveTrustedCommand(command: string, cwd: string): string | null {
348
292
  if (!path.isAbsolute(command) && (command.includes("/") || command.includes("\\"))) return null;
349
293
  const discovered = path.isAbsolute(command) ? command : piUtils.$which(command);
350
294
  if (!discovered) return null;
@@ -353,6 +297,11 @@ function resolveTrustedLspCommand(command: string, cwd: string): string | null {
353
297
  return discovered;
354
298
  }
355
299
 
300
+ /** Same rule as `resolveTrustedCommand`: project-local bin directories are never searched. */
301
+ export function resolveCommand(command: string, cwd: string): string | null {
302
+ return resolveTrustedCommand(command, cwd);
303
+ }
304
+
356
305
  interface ConfigSource {
357
306
  allowProcessOverrides: boolean;
358
307
  read(): LoadedConfigSource | null;
@@ -556,8 +505,8 @@ export function loadConfig(cwd: string): LspConfig {
556
505
  // Check if project has root markers for this language
557
506
  if (!hasRootMarkers(cwd, config.rootMarkers)) continue;
558
507
 
559
- // Check if the language server binary is available (local or $PATH)
560
- const resolved = resolveTrustedLspCommand(config.command, cwd);
508
+ // Check if a trusted language server binary is available on $PATH.
509
+ const resolved = resolveTrustedCommand(config.command, cwd);
561
510
  if (!resolved) continue;
562
511
 
563
512
  detected[name] = { ...config, resolvedCommand: resolved };
@@ -572,7 +521,7 @@ export function loadConfig(cwd: string): LspConfig {
572
521
  for (const [name, config] of Object.entries(mergedWithRuntime)) {
573
522
  if (config.disabled) continue;
574
523
  if (!hasRootMarkers(cwd, config.rootMarkers)) continue;
575
- const resolved = resolveTrustedLspCommand(config.command, cwd);
524
+ const resolved = resolveTrustedCommand(config.command, cwd);
576
525
  if (!resolved) continue;
577
526
  available[name] = { ...config, resolvedCommand: resolved };
578
527
  }
package/src/lsp/edits.ts CHANGED
@@ -13,6 +13,7 @@ import type {
13
13
  WorkspaceEdit,
14
14
  } from "./types";
15
15
  import { uriToFile } from "./utils";
16
+ import { assertInsideWorkspace } from "./workspace-path";
16
17
 
17
18
  // =============================================================================
18
19
  // Text Edit Application
@@ -133,6 +134,7 @@ export async function applyWorkspaceEdit(edit: WorkspaceEdit, cwd: string): Prom
133
134
  const textEditsByUri = flattenWorkspaceTextEdits(edit);
134
135
  for (const [uri, textEdits] of textEditsByUri) {
135
136
  const filePath = uriToFile(uri);
137
+ await assertInsideWorkspace(cwd, filePath);
136
138
  await applyTextEdits(filePath, textEdits);
137
139
  applied.push(`Applied ${textEdits.length} edit(s) to ${formatPathRelativeToCwd(filePath, cwd)}`);
138
140
  }
@@ -144,18 +146,22 @@ export async function applyWorkspaceEdit(edit: WorkspaceEdit, cwd: string): Prom
144
146
  if (change.kind === "create") {
145
147
  const createOp = change as CreateFile;
146
148
  const filePath = uriToFile(createOp.uri);
149
+ await assertInsideWorkspace(cwd, filePath);
147
150
  await Bun.write(filePath, "");
148
151
  applied.push(`Created ${formatPathRelativeToCwd(filePath, cwd)}`);
149
152
  } else if (change.kind === "rename") {
150
153
  const renameOp = change as RenameFile;
151
154
  const oldPath = uriToFile(renameOp.oldUri);
152
155
  const newPath = uriToFile(renameOp.newUri);
156
+ await assertInsideWorkspace(cwd, oldPath);
157
+ await assertInsideWorkspace(cwd, newPath);
153
158
  await fs.mkdir(path.dirname(newPath), { recursive: true });
154
159
  await fs.rename(oldPath, newPath);
155
160
  applied.push(`Renamed ${formatPathRelativeToCwd(oldPath, cwd)} → ${formatPathRelativeToCwd(newPath, cwd)}`);
156
161
  } else if (change.kind === "delete") {
157
162
  const deleteOp = change as DeleteFile;
158
163
  const filePath = uriToFile(deleteOp.uri);
164
+ await assertInsideWorkspace(cwd, filePath);
159
165
  await fs.rm(filePath, { recursive: true });
160
166
  applied.push(`Deleted ${formatPathRelativeToCwd(filePath, cwd)}`);
161
167
  }
@@ -0,0 +1,51 @@
1
+ import * as fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { ToolError } from "../tools/tool-errors";
4
+
5
+ function isEnoent(error: unknown): boolean {
6
+ return (error as NodeJS.ErrnoException).code === "ENOENT";
7
+ }
8
+
9
+ function escapes(root: string, candidate: string): boolean {
10
+ return candidate !== root && !candidate.startsWith(`${root}${path.sep}`);
11
+ }
12
+
13
+ /** Resolve a missing path through the nearest existing ancestor. */
14
+ async function canonicalize(filePath: string): Promise<string> {
15
+ try {
16
+ return await fs.realpath(filePath);
17
+ } catch (error) {
18
+ if (!isEnoent(error)) throw error;
19
+ }
20
+
21
+ const missing: string[] = [];
22
+ let cursor = filePath;
23
+ for (;;) {
24
+ let stat: Awaited<ReturnType<typeof fs.lstat>> | undefined;
25
+ try {
26
+ stat = await fs.lstat(cursor);
27
+ } catch (error) {
28
+ if (!isEnoent(error)) throw error;
29
+ }
30
+ if (stat) {
31
+ try {
32
+ return path.join(await fs.realpath(cursor), ...missing);
33
+ } catch (error) {
34
+ if (isEnoent(error)) throw new ToolError(`LSP edit escapes the workspace: ${filePath}`);
35
+ throw error;
36
+ }
37
+ }
38
+ const parent = path.dirname(cursor);
39
+ if (parent === cursor) throw new ToolError(`LSP edit escapes the workspace: ${filePath}`);
40
+ missing.unshift(path.basename(cursor));
41
+ cursor = parent;
42
+ }
43
+ }
44
+
45
+ export async function assertInsideWorkspace(cwd: string, filePath: string): Promise<void> {
46
+ const root = await fs.realpath(cwd);
47
+ const candidate = await canonicalize(filePath);
48
+ if (escapes(root, candidate)) {
49
+ throw new ToolError(`LSP edit escapes the workspace: ${filePath}`);
50
+ }
51
+ }
package/src/main.ts CHANGED
@@ -475,6 +475,11 @@ type StartupModelProfileArgs = {
475
475
  preferCachedDefaultProfile?: boolean;
476
476
  };
477
477
 
478
+ type StartupModelProfileFailure = {
479
+ profileName: string;
480
+ error: ModelProfileCredentialError | UnknownModelProfileError;
481
+ };
482
+
478
483
  function staleDefaultProfileMessage(error: UnknownModelProfileError, profileCatalogRefreshUnavailable = false): string {
479
484
  const refreshNotice = profileCatalogRefreshUnavailable
480
485
  ? " The online profile catalog could not be refreshed; continuing with the last accepted catalog."
@@ -488,6 +493,8 @@ async function applyStartupModelProfilesWithPolicy(
488
493
  onUnknownDefault?: (error: UnknownModelProfileError, profileCatalogRefreshUnavailable: boolean) => void,
489
494
  ): Promise<void> {
490
495
  let profileCatalogRefreshUnavailable = false;
496
+ let profilePassFailures: StartupModelProfileFailure[] = [];
497
+ let unhandledProfileName: string | undefined;
491
498
  const applyProfile = async (
492
499
  profileName: string,
493
500
  persistDefault: boolean,
@@ -496,37 +503,66 @@ async function applyStartupModelProfilesWithPolicy(
496
503
  tolerateCredentialError?: boolean;
497
504
  tolerateUnknownDefault?: boolean;
498
505
  runtimeBindingsOnly?: boolean;
506
+ userSelectionRevision?: number;
499
507
  } = {},
500
508
  ): Promise<boolean> => {
501
- try {
509
+ const isCurrent =
510
+ options.userSelectionRevision === undefined
511
+ ? undefined
512
+ : () => args.session.getUserModelSelectionRevision() === options.userSelectionRevision;
513
+ if (isCurrent && !isCurrent()) return false;
514
+ const apply = async (): Promise<boolean> => {
515
+ if (isCurrent && !isCurrent()) return false;
502
516
  if (options.runtimeBindingsOnly) {
503
- await applyModelProfileRuntimeBindings({
504
- session: args.session,
505
- modelRegistry: args.modelRegistry,
506
- settings: args.settings,
507
- profileName,
508
- });
517
+ await applyModelProfileRuntimeBindings(
518
+ {
519
+ session: args.session,
520
+ modelRegistry: args.modelRegistry,
521
+ settings: args.settings,
522
+ profileName,
523
+ },
524
+ isCurrent,
525
+ );
509
526
  } else {
510
527
  await activateModelProfile(
511
528
  { session: args.session, modelRegistry: args.modelRegistry, settings: args.settings, profileName },
512
- { persistDefault, thinkingLevelOverride: options.thinkingLevelOverride },
529
+ {
530
+ persistDefault,
531
+ thinkingLevelOverride: options.thinkingLevelOverride,
532
+ isCurrent,
533
+ },
513
534
  );
514
535
  }
515
536
  return true;
537
+ };
538
+ try {
539
+ return isCurrent ? await args.session.withSdkControlMutation(apply) : await apply();
516
540
  } catch (error) {
517
541
  if (error instanceof ModelProfileCredentialError && (onCredentialError || options.tolerateCredentialError)) {
518
- if (onCredentialError) onCredentialError(error);
519
- else process.stderr.write(`${chalk.yellow(`Warning: ${error.message}`)}\n`);
542
+ profilePassFailures.push({ profileName, error });
543
+ logger.warn("Startup model profile not applied: missing provider credentials", {
544
+ profile: profileName,
545
+ errorClass: error.name,
546
+ providers: error.providers,
547
+ role: error.role,
548
+ });
549
+ if (!onCredentialError) process.stderr.write(`${chalk.yellow(`Warning: ${error.message}`)}\n`);
520
550
  return false;
521
551
  }
522
552
  if (error instanceof UnknownModelProfileError && options.tolerateUnknownDefault) {
523
- if (onUnknownDefault) onUnknownDefault(error, profileCatalogRefreshUnavailable);
524
- else
553
+ profilePassFailures.push({ profileName, error });
554
+ logger.warn("Startup model profile not applied: unknown profile", {
555
+ profile: profileName,
556
+ errorClass: error.name,
557
+ catalogRefreshUnavailable: profileCatalogRefreshUnavailable,
558
+ });
559
+ if (!onUnknownDefault)
525
560
  process.stderr.write(
526
561
  `${chalk.yellow(`Warning: ${staleDefaultProfileMessage(error, profileCatalogRefreshUnavailable)}`)}\n`,
527
562
  );
528
563
  return false;
529
564
  }
565
+ unhandledProfileName = profileName;
530
566
  throw error;
531
567
  }
532
568
  };
@@ -546,9 +582,16 @@ async function applyStartupModelProfilesWithPolicy(
546
582
  const preferCachedProfiles =
547
583
  (args.preferCachedModels === true && args.parsedArgs.mpreset !== undefined) ||
548
584
  (args.preferCachedDefaultProfile === true && defaultProfile !== undefined);
549
- const applyConfiguredProfiles = async (allowMissingDefault: boolean): Promise<boolean> => {
585
+ let modelSelectionChangedDuringRecovery = false;
586
+ const applyConfiguredProfiles = async (
587
+ allowMissingDefault: boolean,
588
+ profileNames?: ReadonlySet<string>,
589
+ userSelectionRevision?: number,
590
+ ): Promise<boolean> => {
591
+ profilePassFailures = [];
592
+ unhandledProfileName = undefined;
550
593
  let applied = true;
551
- if (defaultProfile) {
594
+ if (defaultProfile && (!profileNames || profileNames.has(defaultProfile))) {
552
595
  applied =
553
596
  (await applyProfile(defaultProfile, false, {
554
597
  thinkingLevelOverride: args.settings.has("defaultThinkingLevel")
@@ -556,37 +599,141 @@ async function applyStartupModelProfilesWithPolicy(
556
599
  : undefined,
557
600
  tolerateCredentialError: tolerateDefaultProfileFailure,
558
601
  tolerateUnknownDefault:
559
- allowMissingDefault && (onUnknownDefault !== undefined || tolerateDefaultProfileFailure),
602
+ onUnknownDefault !== undefined || (allowMissingDefault && tolerateDefaultProfileFailure),
560
603
  runtimeBindingsOnly: args.session.hasRecoveredDefaultFallbackChain(),
604
+ userSelectionRevision,
561
605
  })) && applied;
562
606
  }
563
- if (args.parsedArgs.mpreset) {
564
- applied = (await applyProfile(args.parsedArgs.mpreset, args.parsedArgs.default === true)) && applied;
607
+ if (args.parsedArgs.mpreset && (!profileNames || profileNames.has(args.parsedArgs.mpreset))) {
608
+ applied =
609
+ (await applyProfile(args.parsedArgs.mpreset, args.parsedArgs.default === true, {
610
+ userSelectionRevision,
611
+ })) && applied;
565
612
  }
566
613
  return applied;
567
614
  };
615
+ const profilesFromFirstFailure = (failedProfiles: ReadonlySet<string>): ReadonlySet<string> => {
616
+ const orderedProfiles = [defaultProfile, args.parsedArgs.mpreset].filter(
617
+ (name): name is string => name !== undefined,
618
+ );
619
+ const firstFailureIndex = orderedProfiles.findIndex(name => failedProfiles.has(name));
620
+ return firstFailureIndex === -1 ? failedProfiles : new Set(orderedProfiles.slice(firstFailureIndex));
621
+ };
622
+ const applyFailedProfiles = async (
623
+ failedProfiles: ReadonlySet<string>,
624
+ userSelectionRevision: number,
625
+ ): Promise<boolean> => {
626
+ const applied = await applyConfiguredProfiles(true, failedProfiles, userSelectionRevision);
627
+ if (
628
+ applied &&
629
+ defaultProfile !== undefined &&
630
+ failedProfiles.has(defaultProfile) &&
631
+ args.parsedArgs.mpreset !== undefined &&
632
+ !failedProfiles.has(args.parsedArgs.mpreset)
633
+ ) {
634
+ return applyProfile(args.parsedArgs.mpreset, args.parsedArgs.default === true, { userSelectionRevision });
635
+ }
636
+ return applied;
637
+ };
638
+ const refreshAuthAndCatalog = async (continueOnRefreshError: boolean): Promise<void> => {
639
+ try {
640
+ await args.modelRegistry.authStorage?.reload();
641
+ } catch (error) {
642
+ logger.warn("Failed to reload auth before startup model profile recovery", {
643
+ error: error instanceof Error ? error.message : String(error),
644
+ });
645
+ }
646
+ try {
647
+ await args.modelRegistry.refreshModelPresetProfilesFromRegistry();
648
+ } catch (error) {
649
+ if (!continueOnRefreshError && args.modelRegistry.getError()) throw error;
650
+ profileCatalogRefreshUnavailable = true;
651
+ logger.warn("Failed to refresh model profile catalog before startup recovery", {
652
+ error: error instanceof Error ? error.message : String(error),
653
+ });
654
+ }
655
+ try {
656
+ await args.modelRegistry.refresh("online-if-uncached", args.session.credentialSessionId);
657
+ } catch (error) {
658
+ if (!continueOnRefreshError) throw error;
659
+ logger.warn("Failed to refresh model catalog before startup profile recovery", {
660
+ error: error instanceof Error ? error.message : String(error),
661
+ });
662
+ }
663
+ };
664
+ const reportFinalProfileFailures = (markDefaultUnavailable = true): void => {
665
+ for (const failure of profilePassFailures) {
666
+ if (failure.error instanceof ModelProfileCredentialError) {
667
+ onCredentialError?.(failure.error);
668
+ } else {
669
+ onUnknownDefault?.(failure.error, profileCatalogRefreshUnavailable);
670
+ }
671
+ }
672
+ if (
673
+ markDefaultUnavailable &&
674
+ (onCredentialError || onUnknownDefault) &&
675
+ args.parsedArgs.mpreset === undefined &&
676
+ args.parsedArgs.model === undefined
677
+ ) {
678
+ const defaultFailure = profilePassFailures.find(failure => failure.profileName === defaultProfile);
679
+ if (defaultFailure) args.session.setUnavailableModelProfile(defaultFailure.profileName);
680
+ }
681
+ };
568
682
 
683
+ // Deferred --mpreset startup can overlap selector input; never reapply its
684
+ // captured profile after the user has selected a newer model.
569
685
  if (preferCachedProfiles) {
570
686
  let applied: boolean;
571
687
  let refreshedOnline = false;
688
+ const selectionRevision = args.session.getUserModelSelectionRevision();
572
689
  try {
573
- applied = await applyConfiguredProfiles(false);
690
+ applied = await applyConfiguredProfiles(false, undefined, selectionRevision);
574
691
  } catch (error) {
575
692
  if (error instanceof ModelProfileCredentialError) throw error;
576
693
  if (error instanceof UnknownModelProfileError) {
577
- try {
578
- await args.modelRegistry.refreshModelPresetProfilesFromRegistry();
579
- } catch (refreshError) {
580
- if (args.modelRegistry.getError()) throw refreshError;
581
- profileCatalogRefreshUnavailable = true;
582
- logger.warn("Failed to refresh model profile catalog before startup recovery", {
583
- error: refreshError instanceof Error ? refreshError.message : String(refreshError),
584
- });
585
- }
694
+ await refreshAuthAndCatalog(onCredentialError !== undefined || onUnknownDefault !== undefined);
695
+ } else {
696
+ await args.modelRegistry.refresh("online-if-uncached", args.session.credentialSessionId);
586
697
  }
587
- await args.modelRegistry.refresh("online-if-uncached", args.session.credentialSessionId);
698
+ if (args.session.getUserModelSelectionRevision() !== selectionRevision) return;
588
699
  refreshedOnline = true;
589
- applied = await applyConfiguredProfiles(true);
700
+ const failedProfiles = new Set(profilePassFailures.map(failure => failure.profileName));
701
+ if (unhandledProfileName) failedProfiles.add(unhandledProfileName);
702
+ const profilesToRetry = profilesFromFirstFailure(failedProfiles);
703
+ applied = await applyConfiguredProfiles(
704
+ true,
705
+ profilesToRetry.size > 0 ? profilesToRetry : undefined,
706
+ selectionRevision,
707
+ );
708
+ if (args.session.getUserModelSelectionRevision() !== selectionRevision) return;
709
+ }
710
+ if (args.session.getUserModelSelectionRevision() !== selectionRevision) {
711
+ modelSelectionChangedDuringRecovery = true;
712
+ applied = false;
713
+ }
714
+ if (
715
+ !modelSelectionChangedDuringRecovery &&
716
+ !applied &&
717
+ profilePassFailures.length > 0 &&
718
+ (onCredentialError !== undefined || onUnknownDefault !== undefined)
719
+ ) {
720
+ if (!refreshedOnline) {
721
+ const failedProfiles = new Set(profilePassFailures.map(failure => failure.profileName));
722
+ await refreshAuthAndCatalog(true);
723
+ refreshedOnline = true;
724
+ if (args.session.getUserModelSelectionRevision() !== selectionRevision) {
725
+ modelSelectionChangedDuringRecovery = true;
726
+ applied = false;
727
+ } else {
728
+ applied = await applyFailedProfiles(failedProfiles, selectionRevision);
729
+ if (args.session.getUserModelSelectionRevision() !== selectionRevision) {
730
+ modelSelectionChangedDuringRecovery = true;
731
+ applied = false;
732
+ }
733
+ }
734
+ }
735
+ if (applied) args.session.setUnavailableModelProfile(undefined);
736
+ else reportFinalProfileFailures(!modelSelectionChangedDuringRecovery);
590
737
  }
591
738
  if (applied && !refreshedOnline)
592
739
  args.modelRegistry.refreshInBackground("online-if-uncached", args.session.credentialSessionId);
@@ -598,7 +745,7 @@ async function applyStartupModelProfilesWithPolicy(
598
745
  }
599
746
 
600
747
  // Explicit CLI --model/--thinking must win over any activated or skipped profile.
601
- if (explicitModel) {
748
+ if (explicitModel && !modelSelectionChangedDuringRecovery) {
602
749
  await args.session.setModelTemporary(explicitModel, args.startupThinkingLevel ?? args.parsedArgs.thinking, {
603
750
  persistAsSessionDefault: true,
604
751
  cause: "startup-override",
@@ -606,7 +753,7 @@ async function applyStartupModelProfilesWithPolicy(
606
753
  const selector = `${explicitModel.provider}/${explicitModel.id}`;
607
754
  args.session.setConfiguredModelChain("default", [selector], "startup-override", undefined, true);
608
755
  args.session.seedDefaultFallbackResolution(0, []);
609
- } else if (args.parsedArgs.thinking && args.session.model) {
756
+ } else if (!modelSelectionChangedDuringRecovery && args.parsedArgs.thinking && args.session.model) {
610
757
  await args.session.setModelTemporary(args.session.model, args.parsedArgs.thinking, { cause: "startup-override" });
611
758
  }
612
759
  }
@@ -975,6 +1122,21 @@ export async function runInteractiveMode(
975
1122
  );
976
1123
  }
977
1124
 
1125
+ // Startup input bypasses the editor submit path, so seed the automatic title
1126
+ // here. Only text that `prompt` actually dispatches to a loaded skill is
1127
+ // skipped; unknown `/skill:` text is submitted as an ordinary prompt and is
1128
+ // titled like one. It must run before the prompt (the title gate skips
1129
+ // sessions that already hold a user message), and a title failure must never
1130
+ // drop the prompt.
1131
+ const maybeGenerateStartupTitle = (text: string, images?: readonly unknown[]): void => {
1132
+ try {
1133
+ if (session.resolvePromptSkillInvocation(text, images) || session.isLocallyHandledSlashCommand(text)) return;
1134
+ mode.maybeGenerateSessionTitle(text);
1135
+ } catch (error: unknown) {
1136
+ logger.warn("Startup session title generation failed", { error: String(error) });
1137
+ }
1138
+ };
1139
+
978
1140
  const runStartupInputAndPromptLoop = async (): Promise<never> => {
979
1141
  const hasStartupInput = initialMessage !== undefined || initialMessages.length > 0;
980
1142
  if (!hasStartupInput && resumeAction === "continue-tail") {
@@ -988,6 +1150,7 @@ export async function runInteractiveMode(
988
1150
 
989
1151
  if (initialMessage !== undefined) {
990
1152
  try {
1153
+ maybeGenerateStartupTitle(initialMessage, initialImages);
991
1154
  await session.prompt(initialMessage, { images: initialImages });
992
1155
  } catch (error: unknown) {
993
1156
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
@@ -1004,6 +1167,7 @@ export async function runInteractiveMode(
1004
1167
  });
1005
1168
  if (slashResult === true) continue;
1006
1169
  if (typeof slashResult === "string") text = slashResult;
1170
+ maybeGenerateStartupTitle(text);
1007
1171
  await session.prompt(text);
1008
1172
  } catch (error: unknown) {
1009
1173
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";