@gajae-code/coding-agent 0.11.1 → 0.11.2

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 (404) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/types/cli/config-cli.d.ts +11 -1
  3. package/dist/types/cli/notify-cli.d.ts +1 -0
  4. package/dist/types/cli/session-picker.d.ts +2 -1
  5. package/dist/types/cli/setup-cli.d.ts +1 -1
  6. package/dist/types/commands/mcp-serve.d.ts +33 -0
  7. package/dist/types/commands/mcp.d.ts +1 -1
  8. package/dist/types/commands/notify.d.ts +0 -2
  9. package/dist/types/commands/sdk.d.ts +1 -1
  10. package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
  11. package/dist/types/config/file-lock.d.ts +14 -4
  12. package/dist/types/config/keybindings.d.ts +42 -1
  13. package/dist/types/config/model-auth.d.ts +3 -0
  14. package/dist/types/config/model-bindings-applier.d.ts +13 -0
  15. package/dist/types/config/model-discovery-manager.d.ts +49 -0
  16. package/dist/types/config/model-equivalence.d.ts +20 -0
  17. package/dist/types/config/model-registry.d.ts +16 -15
  18. package/dist/types/config/model-resolver.d.ts +28 -18
  19. package/dist/types/config/models-config-schema.d.ts +11 -0
  20. package/dist/types/config/settings-schema.d.ts +56 -1
  21. package/dist/types/config/settings.d.ts +14 -11
  22. package/dist/types/coordinator-mcp/policy.d.ts +5 -0
  23. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
  24. package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
  25. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  26. package/dist/types/discovery/helpers.d.ts +0 -6
  27. package/dist/types/edit/index.d.ts +3 -0
  28. package/dist/types/eval/py/env.d.ts +9 -0
  29. package/dist/types/extensibility/extensions/loader.d.ts +2 -1
  30. package/dist/types/extensibility/extensions/runner.d.ts +2 -2
  31. package/dist/types/extensibility/extensions/types.d.ts +34 -3
  32. package/dist/types/extensibility/hooks/runner.d.ts +1 -1
  33. package/dist/types/extensibility/shared-events.d.ts +2 -0
  34. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  35. package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
  36. package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
  37. package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
  38. package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
  39. package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
  40. package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
  41. package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
  42. package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
  43. package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
  44. package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
  45. package/dist/types/gjc-runtime/team-store.d.ts +160 -0
  46. package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
  47. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
  48. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
  49. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
  50. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
  51. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
  52. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
  53. package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
  54. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
  55. package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
  56. package/dist/types/goals/runtime.d.ts +1 -0
  57. package/dist/types/goals/state.d.ts +8 -0
  58. package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
  59. package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
  60. package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
  61. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  62. package/dist/types/hindsight/state.d.ts +11 -4
  63. package/dist/types/lsp/lspmux.d.ts +5 -3
  64. package/dist/types/lsp/path-trust.d.ts +1 -0
  65. package/dist/types/main.d.ts +3 -2
  66. package/dist/types/memories/storage.d.ts +1 -0
  67. package/dist/types/modes/action-registry.d.ts +29 -0
  68. package/dist/types/modes/components/chrome.d.ts +14 -0
  69. package/dist/types/modes/components/command-palette.d.ts +27 -0
  70. package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
  71. package/dist/types/modes/components/index.d.ts +2 -1
  72. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  73. package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
  74. package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
  75. package/dist/types/modes/components/queue-pane.d.ts +12 -0
  76. package/dist/types/modes/components/read-tool-group.d.ts +6 -0
  77. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
  78. package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
  79. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  80. package/dist/types/modes/components/show-images-selector.d.ts +1 -1
  81. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  82. package/dist/types/modes/components/status-line.d.ts +1 -94
  83. package/dist/types/modes/components/tasks-pane.d.ts +15 -0
  84. package/dist/types/modes/components/theme-selector.d.ts +1 -1
  85. package/dist/types/modes/components/tool-execution.d.ts +15 -0
  86. package/dist/types/modes/components/tool-status-header.d.ts +111 -0
  87. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
  88. package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
  89. package/dist/types/modes/controllers/input-controller.d.ts +8 -1
  90. package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
  91. package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
  92. package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
  93. package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
  94. package/dist/types/modes/interactive-mode.d.ts +19 -17
  95. package/dist/types/modes/jobs-observer.d.ts +2 -0
  96. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
  97. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +20 -2
  98. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
  99. package/dist/types/modes/tasks-aggregator.d.ts +39 -0
  100. package/dist/types/modes/transcript-item-registry.d.ts +60 -0
  101. package/dist/types/modes/types.d.ts +20 -13
  102. package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
  103. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  104. package/dist/types/sdk/broker/broker.d.ts +78 -12
  105. package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
  106. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
  107. package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
  108. package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
  109. package/dist/types/sdk/broker/session-index.d.ts +0 -1
  110. package/dist/types/sdk/broker/state-version.d.ts +2 -0
  111. package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
  112. package/dist/types/sdk/bus/config-commands.d.ts +1 -1
  113. package/dist/types/sdk/bus/config.d.ts +11 -0
  114. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  115. package/dist/types/sdk/bus/html-format.d.ts +3 -2
  116. package/dist/types/sdk/bus/index.d.ts +27 -0
  117. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  118. package/dist/types/sdk/bus/notification-service.d.ts +20 -0
  119. package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
  120. package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
  121. package/dist/types/sdk/bus/rich-document.d.ts +7 -0
  122. package/dist/types/sdk/bus/rich-render.d.ts +18 -41
  123. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  124. package/dist/types/sdk/bus/telegram-daemon.d.ts +46 -3
  125. package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
  126. package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
  127. package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
  128. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  129. package/dist/types/sdk/host/host.d.ts +2 -0
  130. package/dist/types/sdk/index.d.ts +1 -0
  131. package/dist/types/sdk/session-directory.d.ts +56 -0
  132. package/dist/types/session/agent-session.d.ts +82 -47
  133. package/dist/types/session/agent-storage.d.ts +7 -0
  134. package/dist/types/session/fallback-chain-controller.d.ts +10 -2
  135. package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
  136. package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
  137. package/dist/types/session/messages.d.ts +16 -0
  138. package/dist/types/session/session-manager-internal.d.ts +14 -0
  139. package/dist/types/session/session-manager.d.ts +82 -12
  140. package/dist/types/session/session-storage.d.ts +43 -3
  141. package/dist/types/session/volatile-context-pruning.d.ts +15 -0
  142. package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
  143. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
  144. package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
  145. package/dist/types/system-prompt.d.ts +2 -11
  146. package/dist/types/task/executor.d.ts +3 -1
  147. package/dist/types/thinking.d.ts +7 -0
  148. package/dist/types/tool-discovery/tool-index.d.ts +1 -50
  149. package/dist/types/tools/ask.d.ts +64 -2
  150. package/dist/types/tools/bash.d.ts +3 -0
  151. package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
  152. package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
  153. package/dist/types/tools/cron.d.ts +2 -0
  154. package/dist/types/tools/fetch.d.ts +1 -0
  155. package/dist/types/tools/index.d.ts +30 -15
  156. package/dist/types/tools/output-meta.d.ts +1 -0
  157. package/dist/types/tools/read.d.ts +3 -0
  158. package/dist/types/tools/search-tool-bm25.d.ts +3 -4
  159. package/dist/types/utils/event-bus.d.ts +1 -1
  160. package/dist/types/web/search/index.d.ts +3 -1
  161. package/package.json +21 -9
  162. package/scripts/build-sdk-package-smoke.ts +34 -1
  163. package/scripts/generate-hotkeys-docs.ts +74 -0
  164. package/scripts/generate-sdk-operation-inventory.ts +64 -20
  165. package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
  166. package/src/async/job-manager.ts +49 -6
  167. package/src/cli/config-cli.ts +75 -2
  168. package/src/cli/notify-cli.ts +92 -26
  169. package/src/cli/session-picker.ts +20 -3
  170. package/src/cli/setup-cli.ts +1 -1
  171. package/src/cli.ts +1 -1
  172. package/src/commands/config.ts +1 -1
  173. package/src/commands/harness.ts +34 -106
  174. package/src/commands/launch.ts +6 -4
  175. package/src/commands/mcp-serve.ts +85 -8
  176. package/src/commands/mcp.ts +2 -2
  177. package/src/commands/notify.ts +8 -4
  178. package/src/commands/sdk.ts +61 -40
  179. package/src/commands/ultragoal.ts +4 -1
  180. package/src/config/atomic-yaml-patch.ts +80 -13
  181. package/src/config/file-lock.ts +105 -43
  182. package/src/config/keybindings.ts +64 -20
  183. package/src/config/model-auth.ts +9 -0
  184. package/src/config/model-bindings-applier.ts +139 -0
  185. package/src/config/model-discovery-manager.ts +224 -0
  186. package/src/config/model-equivalence.ts +66 -0
  187. package/src/config/model-registry.ts +169 -302
  188. package/src/config/model-resolver.ts +361 -369
  189. package/src/config/models-config-schema.ts +5 -4
  190. package/src/config/settings-schema.ts +159 -2
  191. package/src/config/settings.ts +458 -482
  192. package/src/coordinator-mcp/policy.ts +76 -11
  193. package/src/coordinator-mcp/question-gate-codec.ts +274 -0
  194. package/src/coordinator-mcp/question-state.ts +778 -0
  195. package/src/coordinator-mcp/server.ts +1702 -238
  196. package/src/debug/index.ts +1 -1
  197. package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
  198. package/src/defaults/gjc/skills/team/SKILL.md +16 -0
  199. package/src/discovery/helpers.ts +1 -50
  200. package/src/edit/index.ts +64 -0
  201. package/src/eval/py/env.ts +26 -0
  202. package/src/eval/py/kernel.ts +5 -3
  203. package/src/extensibility/extensions/loader.ts +9 -1
  204. package/src/extensibility/extensions/runner.ts +12 -3
  205. package/src/extensibility/extensions/types.ts +39 -1
  206. package/src/extensibility/gjc-plugins/installer.ts +16 -16
  207. package/src/extensibility/hooks/runner.ts +2 -2
  208. package/src/extensibility/runtime-skill-discovery.ts +33 -13
  209. package/src/extensibility/shared-events.ts +2 -0
  210. package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
  211. package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
  212. package/src/gjc-runtime/deep-interview-state.ts +317 -5
  213. package/src/gjc-runtime/goal-mode-request.ts +55 -8
  214. package/src/gjc-runtime/launch-tmux.ts +2 -5
  215. package/src/gjc-runtime/launch-worktree.ts +5 -5
  216. package/src/gjc-runtime/linux-proc.ts +71 -0
  217. package/src/gjc-runtime/ralplan-runtime.ts +74 -90
  218. package/src/gjc-runtime/session-resolution.ts +18 -3
  219. package/src/gjc-runtime/session-state-sidecar.ts +6 -107
  220. package/src/gjc-runtime/state-migrations.ts +1 -1
  221. package/src/gjc-runtime/state-runtime.ts +320 -249
  222. package/src/gjc-runtime/state-writer.ts +36 -40
  223. package/src/gjc-runtime/team-launch.ts +319 -0
  224. package/src/gjc-runtime/team-notify.ts +326 -0
  225. package/src/gjc-runtime/team-runtime.ts +1099 -1482
  226. package/src/gjc-runtime/team-store.ts +828 -0
  227. package/src/gjc-runtime/team-workers.ts +962 -0
  228. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
  229. package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
  230. package/src/gjc-runtime/tmux-sessions.ts +4 -21
  231. package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
  232. package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
  233. package/src/gjc-runtime/ultragoal-guard.ts +29 -5
  234. package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
  235. package/src/gjc-runtime/ultragoal-runtime.ts +84 -1311
  236. package/src/gjc-runtime/workflow-cli-common.ts +30 -0
  237. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  238. package/src/gjc-runtime/workflow-manifest.ts +18 -0
  239. package/src/goals/continuation-timeout-guard.ts +79 -0
  240. package/src/goals/runtime.ts +7 -5
  241. package/src/goals/state.ts +18 -0
  242. package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
  243. package/src/harness-control-plane/phase-rollup.ts +4 -7
  244. package/src/harness-control-plane/receipt-ingest.ts +5 -0
  245. package/src/harness-control-plane/receipt-spool.ts +51 -7
  246. package/src/harness-control-plane/session-lease.ts +12 -130
  247. package/src/hindsight/backend.ts +26 -27
  248. package/src/hindsight/state.ts +101 -32
  249. package/src/hooks/skill-state.ts +17 -11
  250. package/src/internal-urls/docs-index.generated.ts +21 -18
  251. package/src/lsp/client.ts +1 -1
  252. package/src/lsp/config.ts +145 -26
  253. package/src/lsp/index.ts +1 -1
  254. package/src/lsp/lspmux.ts +39 -10
  255. package/src/lsp/path-trust.ts +81 -0
  256. package/src/main.ts +28 -8
  257. package/src/memories/index.ts +1 -0
  258. package/src/memories/storage.ts +6 -2
  259. package/src/modes/action-registry.ts +165 -0
  260. package/src/modes/components/chrome.ts +40 -0
  261. package/src/modes/components/command-palette.ts +168 -0
  262. package/src/modes/components/custom-editor.ts +1 -1
  263. package/src/modes/components/custom-provider-wizard.ts +51 -14
  264. package/src/modes/components/index.ts +2 -1
  265. package/src/modes/components/pet-selector.ts +11 -17
  266. package/src/modes/components/plan-preview-overlay.ts +295 -0
  267. package/src/modes/components/queue-mode-selector.ts +10 -27
  268. package/src/modes/components/queue-pane.ts +79 -0
  269. package/src/modes/components/read-tool-group.ts +13 -0
  270. package/src/modes/components/session-observer-overlay.ts +207 -772
  271. package/src/modes/components/sessions-dashboard.ts +181 -0
  272. package/src/modes/components/settings-selector.ts +1 -1
  273. package/src/modes/components/show-images-selector.ts +10 -24
  274. package/src/modes/components/status-line/types.ts +1 -1
  275. package/src/modes/components/status-line.ts +1 -969
  276. package/src/modes/components/tasks-pane.ts +73 -0
  277. package/src/modes/components/theme-selector.ts +11 -31
  278. package/src/modes/components/tool-execution.ts +22 -0
  279. package/src/modes/components/tool-status-header.ts +1056 -0
  280. package/src/modes/components/transcript-viewer-overlay.ts +384 -0
  281. package/src/modes/controllers/btw-controller.ts +1 -0
  282. package/src/modes/controllers/event-controller.ts +3 -2
  283. package/src/modes/controllers/extension-ui-controller.ts +24 -1
  284. package/src/modes/controllers/goal-mode-controller.ts +474 -0
  285. package/src/modes/controllers/input-controller.ts +764 -229
  286. package/src/modes/controllers/mode-gate.ts +24 -0
  287. package/src/modes/controllers/plan-mode-controller.ts +517 -0
  288. package/src/modes/controllers/selector-controller.ts +225 -10
  289. package/src/modes/controllers/stt-controller.ts +125 -0
  290. package/src/modes/interactive-mode.ts +289 -1150
  291. package/src/modes/jobs-observer.ts +3 -0
  292. package/src/modes/runtime-init.ts +8 -0
  293. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
  294. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +71 -16
  295. package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
  296. package/src/modes/tasks-aggregator.ts +199 -0
  297. package/src/modes/transcript-item-registry.ts +229 -0
  298. package/src/modes/types.ts +45 -18
  299. package/src/modes/utils/ui-helpers.ts +17 -9
  300. package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
  301. package/src/prompts/agent-fragments/restricted-bash.md +1 -0
  302. package/src/prompts/agents/architect.md +2 -8
  303. package/src/prompts/agents/critic.md +2 -8
  304. package/src/prompts/agents/executor.md +3 -0
  305. package/src/prompts/agents/planner.md +4 -12
  306. package/src/prompts/system/custom-system-prompt.md +1 -23
  307. package/src/prompts/system/plan-mode-approved.md +8 -0
  308. package/src/prompts/system/system-prompt.md +17 -96
  309. package/src/prompts/tools/bash.md +4 -4
  310. package/src/prompts/tools/browser.md +15 -52
  311. package/src/prompts/tools/find.md +2 -2
  312. package/src/prompts/tools/hashline.md +13 -55
  313. package/src/prompts/tools/read.md +3 -6
  314. package/src/prompts/tools/search-tool-bm25.md +0 -2
  315. package/src/prompts/tools/search.md +2 -4
  316. package/src/prompts/tools/skill-discovery.md +1 -2
  317. package/src/prompts/tools/task.md +1 -1
  318. package/src/runtime-mcp/manager.ts +1 -1
  319. package/src/sdk/broker/broker.ts +206 -78
  320. package/src/sdk/broker/discovery.ts +1 -1
  321. package/src/sdk/broker/ensure.ts +3 -1
  322. package/src/sdk/broker/lifecycle-codec.ts +11 -0
  323. package/src/sdk/broker/lifecycle-ledger.ts +371 -74
  324. package/src/sdk/broker/lifecycle.ts +1399 -142
  325. package/src/sdk/broker/process-incarnation.ts +5 -13
  326. package/src/sdk/broker/session-index.ts +145 -56
  327. package/src/sdk/broker/state-version.ts +21 -0
  328. package/src/sdk/broker/transport.ts +1 -1
  329. package/src/sdk/bus/chat-daemon-cli.ts +2 -0
  330. package/src/sdk/bus/chat-daemon-control.ts +8 -28
  331. package/src/sdk/bus/chat-effect-journal.ts +10 -2
  332. package/src/sdk/bus/config-commands.ts +5 -4
  333. package/src/sdk/bus/config.ts +98 -0
  334. package/src/sdk/bus/conversation-store.ts +5 -29
  335. package/src/sdk/bus/daemon-paths.ts +2 -0
  336. package/src/sdk/bus/html-format.ts +20 -3
  337. package/src/sdk/bus/index.ts +778 -93
  338. package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
  339. package/src/sdk/bus/natives-augment.d.ts +26 -1
  340. package/src/sdk/bus/notification-service.ts +168 -20
  341. package/src/sdk/bus/rate-limit-pool.ts +58 -14
  342. package/src/sdk/bus/recent-activity.ts +188 -74
  343. package/src/sdk/bus/rich-document.ts +258 -0
  344. package/src/sdk/bus/rich-render.ts +62 -70
  345. package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
  346. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  347. package/src/sdk/bus/telegram-daemon.ts +1389 -237
  348. package/src/sdk/bus/threaded-inbound.ts +8 -3
  349. package/src/sdk/bus/threaded-render.ts +57 -1
  350. package/src/sdk/bus/topic-registry.ts +120 -13
  351. package/src/sdk/host/control/dispatch.ts +14 -2
  352. package/src/sdk/host/control/operations.ts +6 -1
  353. package/src/sdk/host/host.ts +31 -1
  354. package/src/sdk/host/query/handlers.ts +20 -22
  355. package/src/sdk/host/query/revision-store.ts +80 -57
  356. package/src/sdk/index.ts +1 -0
  357. package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
  358. package/src/sdk/session-directory.ts +141 -0
  359. package/src/sdk/session.ts +458 -479
  360. package/src/session/agent-session.ts +1700 -583
  361. package/src/session/agent-storage.ts +18 -0
  362. package/src/session/cache-economics.ts +25 -8
  363. package/src/session/fallback-chain-controller.ts +53 -3
  364. package/src/session/internal/managed-session-scope.ts +2226 -0
  365. package/src/session/internal/managed-session-storage.ts +453 -0
  366. package/src/session/messages.ts +91 -2
  367. package/src/session/session-manager-internal.ts +33 -0
  368. package/src/session/session-manager.ts +1112 -333
  369. package/src/session/session-storage.ts +444 -39
  370. package/src/session/volatile-context-pruning.ts +78 -0
  371. package/src/setup/credential-auto-import.ts +2 -1
  372. package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
  373. package/src/setup/provider-onboarding.ts +36 -61
  374. package/src/setup/provider-presets.json +5 -0
  375. package/src/skill-state/active-state.ts +3 -13
  376. package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
  377. package/src/skill-state/workflow-state-contract.ts +6 -41
  378. package/src/slash-commands/builtin-registry.ts +46 -11
  379. package/src/system-prompt.ts +37 -44
  380. package/src/task/agents.ts +19 -1
  381. package/src/task/executor.ts +90 -6
  382. package/src/task/index.ts +36 -21
  383. package/src/thinking.ts +21 -6
  384. package/src/tool-discovery/tool-index.ts +2 -134
  385. package/src/tools/ask.ts +157 -21
  386. package/src/tools/ast-edit.ts +1 -1
  387. package/src/tools/bash.ts +38 -0
  388. package/src/tools/browser/dead-tab-recovery.ts +78 -0
  389. package/src/tools/browser/tab-supervisor.ts +282 -36
  390. package/src/tools/computer.ts +10 -3
  391. package/src/tools/cron.ts +30 -16
  392. package/src/tools/eval.ts +1 -2
  393. package/src/tools/fetch.ts +11 -4
  394. package/src/tools/index.ts +76 -24
  395. package/src/tools/output-meta.ts +13 -5
  396. package/src/tools/read.ts +37 -1
  397. package/src/tools/resource-gc.ts +2 -2
  398. package/src/tools/search-tool-bm25.ts +11 -74
  399. package/src/utils/event-bus.ts +9 -4
  400. package/src/web/search/index.ts +3 -3
  401. package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
  402. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
  403. package/src/discovery/plugin-dir-roots.ts +0 -28
  404. package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
package/src/lsp/client.ts CHANGED
@@ -509,7 +509,7 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
509
509
 
510
510
  // Wrap with lspmux if available and supported
511
511
  const { command, args, env } = isLspmuxSupported(baseCommand)
512
- ? await getLspmuxCommand(baseCommand, baseArgs)
512
+ ? await getLspmuxCommand(baseCommand, baseArgs, cwd)
513
513
  : { command: baseCommand, args: baseArgs };
514
514
 
515
515
  const owner = spawnOwnedProcess([command, ...args], {
package/src/lsp/config.ts CHANGED
@@ -8,6 +8,7 @@ import { type ClaudePluginRoot, getPreloadedPluginRoots } from "../discovery/hel
8
8
  import { BiomeClient } from "./clients/biome-client";
9
9
  import { SwiftLintClient } from "./clients/swiftlint-client";
10
10
  import DEFAULTS from "./defaults.json" with { type: "json" };
11
+ import { isProjectControlledPath } from "./path-trust";
11
12
  import type { ServerConfig } from "./types";
12
13
 
13
14
  export interface LspConfig {
@@ -69,6 +70,40 @@ function normalizeExtensionToFileTypes(value: unknown): string[] | null {
69
70
  return extensions.length > 0 ? extensions : null;
70
71
  }
71
72
 
73
+ function sanitizeServerConfig(config: unknown, allowProcessOverrides: boolean): RawServerConfig | null {
74
+ if (!isRecord(config)) return null;
75
+
76
+ const sanitized: RawServerConfig = {};
77
+ if (allowProcessOverrides && typeof config.command === "string" && config.command.length > 0) {
78
+ sanitized.command = config.command;
79
+ }
80
+ if (allowProcessOverrides && Array.isArray(config.args)) {
81
+ sanitized.args = config.args.filter((entry): entry is string => typeof entry === "string");
82
+ }
83
+
84
+ const fileTypes = normalizeStringArray(config.fileTypes);
85
+ if (fileTypes) sanitized.fileTypes = fileTypes;
86
+ if (isRecord(config.extensionToLanguage)) sanitized.extensionToLanguage = config.extensionToLanguage;
87
+
88
+ const rootMarkers = normalizeStringArray(config.rootMarkers);
89
+ if (rootMarkers) sanitized.rootMarkers = rootMarkers;
90
+ if (allowProcessOverrides && isRecord(config.initOptions)) sanitized.initOptions = config.initOptions;
91
+ if (allowProcessOverrides && isRecord(config.initializationOptions)) {
92
+ sanitized.initializationOptions = config.initializationOptions;
93
+ }
94
+ if (allowProcessOverrides && isRecord(config.settings)) sanitized.settings = config.settings;
95
+ if (typeof config.disabled === "boolean") sanitized.disabled = config.disabled;
96
+ if (typeof config.warmupTimeoutMs === "number" && Number.isFinite(config.warmupTimeoutMs)) {
97
+ sanitized.warmupTimeoutMs = config.warmupTimeoutMs;
98
+ }
99
+ if (isRecord(config.capabilities)) sanitized.capabilities = config.capabilities;
100
+ const supersedes = normalizeStringArray(config.supersedes);
101
+ if (supersedes) sanitized.supersedes = supersedes;
102
+ if (typeof config.isLinter === "boolean") sanitized.isLinter = config.isLinter;
103
+
104
+ return sanitized;
105
+ }
106
+
72
107
  function normalizeServerConfig(name: string, config: RawServerConfig): ServerConfig | null {
73
108
  const command = typeof config.command === "string" && config.command.length > 0 ? config.command : null;
74
109
  const fileTypes =
@@ -91,13 +126,17 @@ function normalizeServerConfig(name: string, config: RawServerConfig): ServerCon
91
126
  const supersedes = normalizeStringArray(config.supersedes);
92
127
 
93
128
  return {
94
- ...config,
95
129
  command,
96
- args,
130
+ ...(args ? { args } : {}),
97
131
  fileTypes,
98
132
  rootMarkers,
99
133
  ...(initOptions ? { initOptions } : {}),
134
+ ...(isRecord(config.settings) ? { settings: config.settings } : {}),
135
+ ...(typeof config.disabled === "boolean" ? { disabled: config.disabled } : {}),
136
+ ...(typeof config.warmupTimeoutMs === "number" ? { warmupTimeoutMs: config.warmupTimeoutMs } : {}),
137
+ ...(isRecord(config.capabilities) ? { capabilities: config.capabilities } : {}),
100
138
  ...(supersedes ? { supersedes } : {}),
139
+ ...(typeof config.isLinter === "boolean" ? { isLinter: config.isLinter } : {}),
101
140
  };
102
141
  }
103
142
 
@@ -114,7 +153,8 @@ function readConfigFile(filePath: string): NormalizedConfig | null {
114
153
  function coerceServerConfigs(servers: Record<string, RawServerConfig>): Record<string, ServerConfig> {
115
154
  const result: Record<string, ServerConfig> = {};
116
155
  for (const [name, config] of Object.entries(servers)) {
117
- const normalized = normalizeServerConfig(name, config);
156
+ const sanitized = sanitizeServerConfig(config, true);
157
+ const normalized = sanitized ? normalizeServerConfig(name, sanitized) : null;
118
158
  if (normalized) {
119
159
  result[name] = normalized;
120
160
  }
@@ -125,11 +165,30 @@ function coerceServerConfigs(servers: Record<string, RawServerConfig>): Record<s
125
165
  function mergeServers(
126
166
  base: Record<string, ServerConfig>,
127
167
  overrides: Record<string, RawServerConfig>,
168
+ allowProcessOverrides: boolean,
128
169
  ): Record<string, ServerConfig> {
129
170
  const merged: Record<string, ServerConfig> = { ...base };
130
171
  for (const [name, config] of Object.entries(overrides)) {
172
+ if (
173
+ !allowProcessOverrides &&
174
+ isRecord(config) &&
175
+ ("command" in config ||
176
+ "args" in config ||
177
+ "resolvedCommand" in config ||
178
+ "createClient" in config ||
179
+ "initOptions" in config ||
180
+ "initializationOptions" in config ||
181
+ "settings" in config)
182
+ ) {
183
+ logger.warn("Ignoring project-controlled LSP process-affecting overrides.", { name });
184
+ }
185
+ const sanitized = sanitizeServerConfig(config, allowProcessOverrides);
186
+ if (!sanitized) {
187
+ logger.warn("Ignoring invalid LSP server config.", { name });
188
+ continue;
189
+ }
131
190
  if (merged[name]) {
132
- const candidate = { ...merged[name], ...config };
191
+ const candidate = { ...merged[name], ...sanitized };
133
192
  const normalized = normalizeServerConfig(name, candidate);
134
193
  if (normalized) {
135
194
  merged[name] = normalized;
@@ -137,7 +196,7 @@ function mergeServers(
137
196
  logger.warn("Ignoring invalid LSP overrides (keeping previous config).", { name });
138
197
  }
139
198
  } else {
140
- const normalized = normalizeServerConfig(name, config);
199
+ const normalized = normalizeServerConfig(name, sanitized);
141
200
  if (normalized) {
142
201
  merged[name] = normalized;
143
202
  }
@@ -276,17 +335,49 @@ export function resolveCommand(command: string, cwd: string): string | null {
276
335
  return $which(command);
277
336
  }
278
337
 
338
+ /** Resolve an LSP executable without consulting project-controlled bin directories. */
339
+ function resolveTrustedLspCommand(command: string, cwd: string): string | null {
340
+ if (!path.isAbsolute(command) && (command.includes("/") || command.includes("\\"))) return null;
341
+ const discovered = path.isAbsolute(command) ? command : $which(command);
342
+ if (!discovered) return null;
343
+ if (isProjectControlledPath(discovered, cwd)) return null;
344
+ const canonical = canonicalExistingPath(discovered);
345
+ return canonical;
346
+ }
347
+
279
348
  interface ConfigSource {
280
- read(): NormalizedConfig | null;
349
+ allowProcessOverrides: boolean;
350
+ read(): LoadedConfigSource | null;
351
+ }
352
+
353
+ interface LoadedConfigSource {
354
+ config: NormalizedConfig;
355
+ projectControlled: boolean;
356
+ }
357
+
358
+ function canonicalExistingPath(filePath: string): string | null {
359
+ try {
360
+ return fs.realpathSync(filePath);
361
+ } catch {
362
+ return null;
363
+ }
281
364
  }
282
365
 
283
- function fileConfigSource(filePath: string): ConfigSource {
366
+ function readCanonicalConfigFile(filePath: string, cwd: string): LoadedConfigSource | null {
367
+ const canonicalPath = canonicalExistingPath(filePath);
368
+ if (!canonicalPath) return null;
369
+ const config = readConfigFile(canonicalPath);
370
+ return config ? { config, projectControlled: isProjectControlledPath(canonicalPath, cwd) } : null;
371
+ }
372
+
373
+ function fileConfigSource(filePath: string, cwd: string, allowProcessOverrides: boolean): ConfigSource {
284
374
  return {
285
- read: () => readConfigFile(filePath),
375
+ allowProcessOverrides,
376
+ read: () => readCanonicalConfigFile(filePath, cwd),
286
377
  };
287
378
  }
288
379
 
289
- function readMarketplaceLspConfig(root: ClaudePluginRoot): NormalizedConfig | null {
380
+ function readMarketplaceLspConfig(root: ClaudePluginRoot, cwd: string): LoadedConfigSource | null {
290
381
  const catalogPaths = [
291
382
  path.resolve(root.path, "..", "..", "marketplace.json"),
292
383
  path.resolve(root.path, "..", "..", ".claude-plugin", "marketplace.json"),
@@ -294,8 +385,11 @@ function readMarketplaceLspConfig(root: ClaudePluginRoot): NormalizedConfig | nu
294
385
 
295
386
  for (const catalogPath of catalogPaths) {
296
387
  try {
297
- const catalog = JSON.parse(fs.readFileSync(catalogPath, "utf-8")) as unknown;
388
+ const canonicalCatalogPath = canonicalExistingPath(catalogPath);
389
+ if (!canonicalCatalogPath) continue;
390
+ const catalog = JSON.parse(fs.readFileSync(canonicalCatalogPath, "utf-8")) as unknown;
298
391
  if (!isRecord(catalog) || !Array.isArray(catalog.plugins)) continue;
392
+ const catalogIsProjectControlled = isProjectControlledPath(canonicalCatalogPath, cwd);
299
393
 
300
394
  for (const plugin of catalog.plugins) {
301
395
  if (!isRecord(plugin) || plugin.name !== root.plugin) continue;
@@ -304,10 +398,27 @@ function readMarketplaceLspConfig(root: ClaudePluginRoot): NormalizedConfig | nu
304
398
  if (typeof lspServers === "string") {
305
399
  const configPath = path.resolve(root.path, lspServers);
306
400
  if (!pathIsWithin(root.path, configPath)) return null;
307
- return readConfigFile(configPath);
401
+ const canonicalRootPath = canonicalExistingPath(root.path);
402
+ const canonicalConfigPath = canonicalExistingPath(configPath);
403
+ if (
404
+ !canonicalRootPath ||
405
+ !canonicalConfigPath ||
406
+ !pathIsWithin(canonicalRootPath, canonicalConfigPath)
407
+ ) {
408
+ return null;
409
+ }
410
+ const config = readConfigFile(canonicalConfigPath);
411
+ return config
412
+ ? {
413
+ config,
414
+ projectControlled:
415
+ catalogIsProjectControlled || isProjectControlledPath(canonicalConfigPath, cwd),
416
+ }
417
+ : null;
308
418
  }
309
419
  if (isRecord(lspServers)) {
310
- return normalizeConfig({ servers: lspServers });
420
+ const config = normalizeConfig({ servers: lspServers });
421
+ return config ? { config, projectControlled: catalogIsProjectControlled } : null;
311
422
  }
312
423
  return null;
313
424
  }
@@ -317,12 +428,17 @@ function readMarketplaceLspConfig(root: ClaudePluginRoot): NormalizedConfig | nu
317
428
  return null;
318
429
  }
319
430
 
320
- function marketplaceConfigSource(root: ClaudePluginRoot): ConfigSource {
431
+ function marketplaceConfigSource(root: ClaudePluginRoot, cwd: string, allowProcessOverrides: boolean): ConfigSource {
321
432
  return {
322
- read: () => readMarketplaceLspConfig(root),
433
+ allowProcessOverrides,
434
+ read: () => readMarketplaceLspConfig(root, cwd),
323
435
  };
324
436
  }
325
437
 
438
+ function pluginCanOverrideProcess(root: ClaudePluginRoot, cwd: string): boolean {
439
+ return root.scope !== "project" && !isProjectControlledPath(root.path, cwd);
440
+ }
441
+
326
442
  /**
327
443
  * Configuration sources in priority order.
328
444
  * Supports both visible and hidden variants at each config location.
@@ -333,14 +449,14 @@ function getConfigSources(cwd: string): ConfigSource[] {
333
449
 
334
450
  // Project root files (highest priority)
335
451
  for (const filename of filenames) {
336
- sources.push(fileConfigSource(path.join(cwd, filename)));
452
+ sources.push(fileConfigSource(path.join(cwd, filename), cwd, false));
337
453
  }
338
454
 
339
455
  // Project config directories (.gjc/, .gemini/)
340
456
  const projectDirs = getConfigDirPaths("", { user: false, project: true, cwd });
341
457
  for (const dir of projectDirs) {
342
458
  for (const filename of filenames) {
343
- sources.push(fileConfigSource(path.join(dir, filename)));
459
+ sources.push(fileConfigSource(path.join(dir, filename), cwd, false));
344
460
  }
345
461
  }
346
462
 
@@ -348,22 +464,23 @@ function getConfigSources(cwd: string): ConfigSource[] {
348
464
  const userDirs = getConfigDirPaths("", { user: true, project: false });
349
465
  for (const dir of userDirs) {
350
466
  for (const filename of filenames) {
351
- sources.push(fileConfigSource(path.join(dir, filename)));
467
+ sources.push(fileConfigSource(path.join(dir, filename), cwd, true));
352
468
  }
353
469
  }
354
470
 
355
- // Plugin LSP configs (from marketplace/--plugin-dir roots)
471
+ // Plugin LSP configs
356
472
  const pluginRoots = getPreloadedPluginRoots();
357
473
  for (const root of pluginRoots) {
474
+ const allowProcessOverrides = pluginCanOverrideProcess(root, cwd);
358
475
  for (const filename of filenames) {
359
- sources.push(fileConfigSource(path.join(root.path, filename)));
476
+ sources.push(fileConfigSource(path.join(root.path, filename), cwd, allowProcessOverrides));
360
477
  }
361
- sources.push(marketplaceConfigSource(root));
478
+ sources.push(marketplaceConfigSource(root, cwd, allowProcessOverrides));
362
479
  }
363
480
 
364
481
  // User home root files (lowest priority fallback)
365
482
  for (const filename of filenames) {
366
- sources.push(fileConfigSource(path.join(os.homedir(), filename)));
483
+ sources.push(fileConfigSource(path.join(os.homedir(), filename), cwd, true));
367
484
  }
368
485
 
369
486
  return sources;
@@ -408,12 +525,14 @@ export function loadConfig(cwd: string): LspConfig {
408
525
 
409
526
  let idleTimeoutMs: number | undefined;
410
527
  for (const source of configSources) {
411
- const parsed = source.read();
412
- if (!parsed) continue;
528
+ const loaded = source.read();
529
+ if (!loaded) continue;
530
+ const parsed = loaded.config;
531
+ const allowProcessOverrides = source.allowProcessOverrides && !loaded.projectControlled;
413
532
  const hasServerOverrides = Object.keys(parsed.servers).length > 0;
414
533
  if (hasServerOverrides) {
415
534
  hasOverrides = true;
416
- mergedServers = mergeServers(mergedServers, parsed.servers);
535
+ mergedServers = mergeServers(mergedServers, parsed.servers, allowProcessOverrides);
417
536
  }
418
537
  if (parsed.idleTimeoutMs !== undefined) {
419
538
  idleTimeoutMs = parsed.idleTimeoutMs;
@@ -430,7 +549,7 @@ export function loadConfig(cwd: string): LspConfig {
430
549
  if (!hasRootMarkers(cwd, config.rootMarkers)) continue;
431
550
 
432
551
  // Check if the language server binary is available (local or $PATH)
433
- const resolved = resolveCommand(config.command, cwd);
552
+ const resolved = resolveTrustedLspCommand(config.command, cwd);
434
553
  if (!resolved) continue;
435
554
 
436
555
  detected[name] = { ...config, resolvedCommand: resolved };
@@ -445,7 +564,7 @@ export function loadConfig(cwd: string): LspConfig {
445
564
  for (const [name, config] of Object.entries(mergedWithRuntime)) {
446
565
  if (config.disabled) continue;
447
566
  if (!hasRootMarkers(cwd, config.rootMarkers)) continue;
448
- const resolved = resolveCommand(config.command, cwd);
567
+ const resolved = resolveTrustedLspCommand(config.command, cwd);
449
568
  if (!resolved) continue;
450
569
  available[name] = { ...config, resolvedCommand: resolved };
451
570
  }
package/src/lsp/index.ts CHANGED
@@ -1213,7 +1213,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1213
1213
  // Status action doesn't need a file
1214
1214
  if (action === "status") {
1215
1215
  const servers = Object.keys(config.servers);
1216
- const lspmuxState = await detectLspmux();
1216
+ const lspmuxState = await detectLspmux(this.session.cwd);
1217
1217
  const lspmuxStatus = lspmuxState.available
1218
1218
  ? lspmuxState.running
1219
1219
  ? "lspmux: active (multiplexing enabled)"
package/src/lsp/lspmux.ts CHANGED
@@ -1,8 +1,10 @@
1
+ import * as fs from "node:fs";
1
2
  import * as os from "node:os";
2
3
  import * as path from "node:path";
3
4
  import { $flag, $which, logger } from "@gajae-code/utils";
4
5
  import { TOML } from "bun";
5
6
  import { spawnOwnedProcess } from "../runtime/process-lifecycle";
7
+ import { isProjectControlledPath } from "./path-trust";
6
8
 
7
9
  /**
8
10
  * lspmux integration for LSP server multiplexing.
@@ -81,6 +83,7 @@ function getConfigPath(): string {
81
83
 
82
84
  let cachedState: LspmuxState | null = null;
83
85
  let cacheTimestamp = 0;
86
+ let cacheCwd: string | null = null;
84
87
 
85
88
  /**
86
89
  * Parse lspmux config.toml file.
@@ -110,7 +113,7 @@ async function checkServerRunning(binaryPath: string): Promise<boolean> {
110
113
  drainStream(proc.stdout);
111
114
  drainStream(proc.stderr);
112
115
 
113
- let timer: ReturnType<typeof setTimeout> | undefined;
116
+ let timer: NodeJS.Timeout | undefined;
114
117
  const timeout = new Promise<null>(resolve => {
115
118
  timer = setTimeout(() => resolve(null), LIVENESS_TIMEOUT_MS);
116
119
  });
@@ -147,28 +150,42 @@ function drainStream(stream: ReadableStream<Uint8Array> | null | undefined): voi
147
150
  })();
148
151
  }
149
152
 
153
+ function resolveTrustedLspmuxBinary(cwd: string): string | null {
154
+ const discoveredPath = $which("lspmux");
155
+ if (!discoveredPath) return null;
156
+ if (isProjectControlledPath(discoveredPath, cwd)) return null;
157
+
158
+ try {
159
+ return fs.realpathSync(discoveredPath);
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+
150
165
  /**
151
166
  * Detect lspmux availability and state.
152
167
  * Results are cached for STATE_CACHE_TTL_MS.
153
168
  *
154
- * Set PI_DISABLE_LSPMUX=1 to disable.
169
+ * Set GJC_DISABLE_LSPMUX=1 or PI_DISABLE_LSPMUX=1 to disable.
155
170
  */
156
- export async function detectLspmux(): Promise<LspmuxState> {
171
+ export async function detectLspmux(cwd = process.cwd()): Promise<LspmuxState> {
157
172
  const now = Date.now();
158
- if (cachedState && now - cacheTimestamp < STATE_CACHE_TTL_MS) {
173
+ if ($flag("GJC_DISABLE_LSPMUX") || $flag("PI_DISABLE_LSPMUX")) {
174
+ cachedState = { available: false, running: false, binaryPath: null, config: null };
175
+ cacheTimestamp = now;
176
+ cacheCwd = cwd;
159
177
  return cachedState;
160
178
  }
161
179
 
162
- if ($flag("PI_DISABLE_LSPMUX")) {
163
- cachedState = { available: false, running: false, binaryPath: null, config: null };
164
- cacheTimestamp = now;
180
+ if (cachedState && cacheCwd === cwd && now - cacheTimestamp < STATE_CACHE_TTL_MS) {
165
181
  return cachedState;
166
182
  }
167
183
 
168
- const binaryPath = $which("lspmux");
184
+ const binaryPath = resolveTrustedLspmuxBinary(cwd);
169
185
  if (!binaryPath) {
170
186
  cachedState = { available: false, running: false, binaryPath: null, config: null };
171
187
  cacheTimestamp = now;
188
+ cacheCwd = cwd;
172
189
  return cachedState;
173
190
  }
174
191
 
@@ -176,6 +193,7 @@ export async function detectLspmux(): Promise<LspmuxState> {
176
193
 
177
194
  cachedState = { available: true, running, binaryPath, config };
178
195
  cacheTimestamp = now;
196
+ cacheCwd = cwd;
179
197
 
180
198
  if (running) {
181
199
  logger.debug("lspmux detected and running", { binaryPath });
@@ -184,6 +202,13 @@ export async function detectLspmux(): Promise<LspmuxState> {
184
202
  return cachedState;
185
203
  }
186
204
 
205
+ /** Reset cached lspmux detection state for focused tests. */
206
+ export function resetLspmuxStateForTesting(): void {
207
+ cachedState = null;
208
+ cacheTimestamp = 0;
209
+ cacheCwd = null;
210
+ }
211
+
187
212
  // =============================================================================
188
213
  // Command Wrapping
189
214
  // =============================================================================
@@ -251,7 +276,11 @@ export function wrapWithLspmux(
251
276
  * @param args - Original command arguments
252
277
  * @returns Command and args to use (possibly wrapped with lspmux)
253
278
  */
254
- export async function getLspmuxCommand(command: string, args?: string[]): Promise<LspmuxWrappedCommand> {
255
- const state = await detectLspmux();
279
+ export async function getLspmuxCommand(
280
+ command: string,
281
+ args?: string[],
282
+ cwd = process.cwd(),
283
+ ): Promise<LspmuxWrappedCommand> {
284
+ const state = await detectLspmux(cwd);
256
285
  return wrapWithLspmux(command, args, state);
257
286
  }
@@ -0,0 +1,81 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { CONFIG_DIR_NAME, pathIsWithin, relativePathEscapesRoot } from "@gajae-code/utils";
5
+
6
+ function normalizePathForComparison(candidate: string): string {
7
+ const resolved = path.resolve(candidate);
8
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
9
+ }
10
+
11
+ function pathIsLexicallyWithin(root: string, candidate: string): boolean {
12
+ const relative = path.relative(normalizePathForComparison(root), normalizePathForComparison(candidate));
13
+ return !relativePathEscapesRoot(relative);
14
+ }
15
+
16
+ function canonicalPath(candidate: string): string {
17
+ try {
18
+ return fs.realpathSync(candidate);
19
+ } catch {
20
+ return path.resolve(candidate);
21
+ }
22
+ }
23
+
24
+ function canonicalParentPath(candidate: string): string {
25
+ const resolved = path.resolve(candidate);
26
+ return path.join(canonicalPath(path.dirname(resolved)), path.basename(resolved));
27
+ }
28
+
29
+ function isDirectory(candidate: string): boolean {
30
+ try {
31
+ return fs.statSync(candidate).isDirectory();
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ function pathMatchesStop(candidate: string, stopPaths: ReadonlySet<string>): boolean {
38
+ return (
39
+ stopPaths.has(normalizePathForComparison(candidate)) ||
40
+ stopPaths.has(normalizePathForComparison(canonicalPath(candidate)))
41
+ );
42
+ }
43
+
44
+ function findProjectTrustRoot(start: string, stopPaths: ReadonlySet<string>): string | undefined {
45
+ const fallback = path.resolve(start);
46
+ let nearestConfigRoot: string | undefined;
47
+ let current = fallback;
48
+ for (;;) {
49
+ if (pathMatchesStop(current, stopPaths)) {
50
+ return current === fallback ? undefined : (nearestConfigRoot ?? fallback);
51
+ }
52
+ if (fs.existsSync(path.join(current, ".git"))) return current;
53
+ if (nearestConfigRoot === undefined && isDirectory(path.join(current, CONFIG_DIR_NAME))) {
54
+ nearestConfigRoot = current;
55
+ }
56
+ const parent = path.dirname(current);
57
+ if (parent === current) return nearestConfigRoot ?? fallback;
58
+ current = parent;
59
+ }
60
+ }
61
+
62
+ export function isProjectControlledPath(candidate: string, cwd: string): boolean {
63
+ const home = os.homedir();
64
+ const stopPaths = new Set([path.resolve(home), canonicalPath(home)].map(normalizePathForComparison));
65
+ const lexicalTrustRoot = findProjectTrustRoot(cwd, stopPaths);
66
+ if (lexicalTrustRoot !== undefined && pathIsLexicallyWithin(lexicalTrustRoot, candidate)) return true;
67
+
68
+ const canonicalTrustRoots = new Set<string>();
69
+ if (lexicalTrustRoot !== undefined) canonicalTrustRoots.add(canonicalPath(lexicalTrustRoot));
70
+ const canonicalTrustRoot = findProjectTrustRoot(canonicalPath(cwd), stopPaths);
71
+ if (canonicalTrustRoot !== undefined) canonicalTrustRoots.add(canonicalTrustRoot);
72
+ for (const trustRoot of canonicalTrustRoots) {
73
+ if (
74
+ pathIsLexicallyWithin(trustRoot, canonicalParentPath(candidate)) ||
75
+ pathIsWithin(trustRoot, canonicalPath(candidate))
76
+ ) {
77
+ return true;
78
+ }
79
+ }
80
+ return false;
81
+ }
package/src/main.ts CHANGED
@@ -57,6 +57,7 @@ import type { AgentSession } from "./session/agent-session";
57
57
  import {
58
58
  type ResumeSessionIdentity,
59
59
  resolveResumableSession,
60
+ type SessionDirectoryMigrationPolicy,
60
61
  type SessionInfo,
61
62
  SessionManager,
62
63
  type StrictSessionOpenResult,
@@ -510,6 +511,7 @@ type SelectResumeSession = (sessions: SessionInfo[]) => Promise<SessionSelection
510
511
  type OpenExistingSessionStrict = (
511
512
  identity: ResumeSessionIdentity,
512
513
  sessionDir?: string,
514
+ migrationPolicy?: SessionDirectoryMigrationPolicy,
513
515
  ) => Promise<StrictSessionOpenResult>;
514
516
 
515
517
  export const BARE_RESUME_CONFLICT_ERROR =
@@ -694,6 +696,7 @@ export async function createSessionManager(
694
696
  cwd: string,
695
697
  activeSettings: Settings = settings,
696
698
  ): Promise<SessionManager | undefined> {
699
+ const migrationPolicy = activeSettings.get("session.directoryMigration") === "disabled" ? "disabled" : "copy-retain";
697
700
  if (parsed.resume === true) {
698
701
  return undefined;
699
702
  }
@@ -703,13 +706,13 @@ export async function createSessionManager(
703
706
  }
704
707
  const forkSource = parsed.fork;
705
708
  if (forkSource.includes("/") || forkSource.includes("\\") || forkSource.endsWith(".jsonl")) {
706
- return await SessionManager.forkFrom(forkSource, cwd, parsed.sessionDir);
709
+ return await SessionManager.forkFrom(forkSource, cwd, parsed.sessionDir, undefined, migrationPolicy);
707
710
  }
708
711
  const match = await resolveResumableSession(forkSource, cwd, parsed.sessionDir);
709
712
  if (!match) {
710
713
  throw new Error(`Session "${forkSource}" not found.`);
711
714
  }
712
- return await SessionManager.forkFrom(match.session.path, cwd, parsed.sessionDir);
715
+ return await SessionManager.forkFrom(match.session.path, cwd, parsed.sessionDir, undefined, migrationPolicy);
713
716
  }
714
717
 
715
718
  if (parsed.noSession) {
@@ -718,7 +721,7 @@ export async function createSessionManager(
718
721
  if (typeof parsed.resume === "string") {
719
722
  const sessionArg = parsed.resume;
720
723
  if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) {
721
- return await SessionManager.open(sessionArg, parsed.sessionDir);
724
+ return await SessionManager.open(sessionArg, parsed.sessionDir, undefined, migrationPolicy);
722
725
  }
723
726
  const match = await resolveResumableSession(sessionArg, cwd, parsed.sessionDir);
724
727
  if (!match) {
@@ -732,13 +735,19 @@ export async function createSessionManager(
732
735
  if (!shouldFork) {
733
736
  throw new Error(`Session "${sessionArg}" is in another project (${match.session.cwd}).`);
734
737
  }
735
- return await SessionManager.forkFrom(match.session.path, cwd, parsed.sessionDir);
738
+ return await SessionManager.forkFrom(
739
+ match.session.path,
740
+ cwd,
741
+ parsed.sessionDir,
742
+ undefined,
743
+ migrationPolicy,
744
+ );
736
745
  }
737
746
  }
738
- return await SessionManager.open(match.session.path, parsed.sessionDir);
747
+ return await SessionManager.open(match.session.path, parsed.sessionDir, undefined, migrationPolicy);
739
748
  }
740
749
  if (parsed.continue) {
741
- return await SessionManager.continueRecent(cwd, parsed.sessionDir);
750
+ return await SessionManager.continueRecent(cwd, parsed.sessionDir, undefined, migrationPolicy);
742
751
  }
743
752
  // --resume without value is handled separately (needs picker UI)
744
753
  // If --session-dir provided without --continue/--resume, create new session there
@@ -761,7 +770,7 @@ export async function createSessionManager(
761
770
  // buildSessionOptions restores the session's model/thinking instead of
762
771
  // overriding them with CLI defaults.
763
772
  if (activeSettings.get("autoResume")) {
764
- const manager = await SessionManager.continueRecent(cwd, parsed.sessionDir);
773
+ const manager = await SessionManager.continueRecent(cwd, parsed.sessionDir, undefined, migrationPolicy);
765
774
  if (manager.getEntries().length > 0) {
766
775
  parsed.continue = true;
767
776
  }
@@ -1014,6 +1023,7 @@ export interface RunRootCommandDependencies {
1014
1023
  selectResumeSession?: SelectResumeSession;
1015
1024
  openExistingSessionStrict?: OpenExistingSessionStrict;
1016
1025
  initializeSettings?: typeof Settings.init;
1026
+ loadSettingsForScope?: typeof Settings.loadForScope;
1017
1027
  }
1018
1028
 
1019
1029
  export async function runRootCommand(
@@ -1054,15 +1064,25 @@ export async function runRootCommand(
1054
1064
  process.stdout.write(`${chalk.dim("No sessions found")}\n`);
1055
1065
  return;
1056
1066
  }
1057
- const selection = await (deps.selectResumeSession ?? selectSession)(sessions);
1067
+ const selection = deps.selectResumeSession
1068
+ ? await deps.selectResumeSession(sessions)
1069
+ : await selectSession(sessions, parsedArgs.sessionDir);
1058
1070
  if (selection.kind === "cancelled") {
1059
1071
  return;
1060
1072
  }
1073
+ const resumeMigrationPolicy =
1074
+ (await (deps.loadSettingsForScope ?? Settings.loadForScope)({ cwd: resumeCwd })).get(
1075
+ "session.directoryMigration",
1076
+ ) === "disabled"
1077
+ ? "disabled"
1078
+ : "copy-retain";
1061
1079
  let opened: StrictSessionOpenResult;
1062
1080
  try {
1063
1081
  opened = await (deps.openExistingSessionStrict ?? SessionManager.openExistingStrict)(
1064
1082
  selection.identity,
1065
1083
  parsedArgs.sessionDir,
1084
+ undefined,
1085
+ resumeMigrationPolicy,
1066
1086
  );
1067
1087
  } catch {
1068
1088
  process.stderr.write(`${BARE_RESUME_OPEN_ERROR}\n`);
@@ -249,6 +249,7 @@ async function runPhase1(options: {
249
249
 
250
250
  const claims = claimStage1Jobs(db, {
251
251
  nowSec,
252
+ cwd: session.sessionManager.getCwd(),
252
253
  threadScanLimit: config.threadScanLimit,
253
254
  maxRolloutsPerStartup: config.maxRolloutsPerStartup,
254
255
  maxRolloutAgeDays: config.maxRolloutAgeDays,
@@ -137,6 +137,7 @@ export function claimStage1Jobs(
137
137
  db: Database,
138
138
  params: {
139
139
  nowSec: number;
140
+ cwd: string;
140
141
  threadScanLimit: number;
141
142
  maxRolloutsPerStartup: number;
142
143
  maxRolloutAgeDays: number;
@@ -149,6 +150,7 @@ export function claimStage1Jobs(
149
150
  ): Stage1Claim[] {
150
151
  const {
151
152
  nowSec,
153
+ cwd,
152
154
  threadScanLimit,
153
155
  maxRolloutsPerStartup,
154
156
  maxRolloutAgeDays,
@@ -168,8 +170,10 @@ export function claimStage1Jobs(
168
170
  let runningCount = runningCountRow?.count ?? 0;
169
171
  if (runningCount >= runningConcurrencyCap) return [];
170
172
  const candidateRows = db
171
- .prepare("SELECT id, updated_at, rollout_path, cwd, source_kind FROM threads ORDER BY updated_at DESC LIMIT ?")
172
- .all(threadScanLimit) as Array<{
173
+ .prepare(
174
+ "SELECT id, updated_at, rollout_path, cwd, source_kind FROM threads WHERE cwd = ? ORDER BY updated_at DESC LIMIT ?",
175
+ )
176
+ .all(cwd, threadScanLimit) as Array<{
173
177
  id: string;
174
178
  updated_at: number;
175
179
  rollout_path: string;