@gajae-code/coding-agent 0.18.12 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/CHANGELOG.md +467 -0
  2. package/dist/types/capability/rule.d.ts +7 -2
  3. package/dist/types/cli/append-system-prompt.d.ts +6 -0
  4. package/dist/types/cli/args.d.ts +7 -2
  5. package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
  6. package/dist/types/cli/customize-doctor.d.ts +4 -3
  7. package/dist/types/cli/public-command-errors.d.ts +1 -0
  8. package/dist/types/cli/root-flags.d.ts +0 -12
  9. package/dist/types/cli/update-cli.d.ts +7 -2
  10. package/dist/types/cli-main.d.ts +0 -6
  11. package/dist/types/commands/launch.d.ts +0 -6
  12. package/dist/types/commands/migrate.d.ts +1 -1
  13. package/dist/types/commands/sdk.d.ts +8 -0
  14. package/dist/types/commit/map-reduce/index.d.ts +4 -4
  15. package/dist/types/config/autorouting-tier-map.d.ts +23 -1
  16. package/dist/types/config/config-hot-reload.d.ts +5 -0
  17. package/dist/types/config/file-lock.d.ts +44 -2
  18. package/dist/types/config/model-profile-activation.d.ts +19 -5
  19. package/dist/types/config/model-registry.d.ts +16 -3
  20. package/dist/types/config/model-resolver.d.ts +5 -4
  21. package/dist/types/config/settings-schema.d.ts +11 -1
  22. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  23. package/dist/types/customization/import.d.ts +2 -0
  24. package/dist/types/customization/types.d.ts +1 -1
  25. package/dist/types/dap/session.d.ts +11 -2
  26. package/dist/types/discovery/helpers.d.ts +7 -2
  27. package/dist/types/edit/index.d.ts +5 -0
  28. package/dist/types/edit/modes/patch.d.ts +9 -0
  29. package/dist/types/eval/py/runtime.d.ts +9 -0
  30. package/dist/types/export/ttsr.d.ts +19 -0
  31. package/dist/types/extensibility/extensions/types.d.ts +5 -1
  32. package/dist/types/extensibility/slash-commands.d.ts +1 -0
  33. package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
  34. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
  36. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
  37. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  38. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  39. package/dist/types/hindsight/content.d.ts +5 -6
  40. package/dist/types/internal-urls/types.d.ts +2 -0
  41. package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
  42. package/dist/types/main.d.ts +23 -18
  43. package/dist/types/migrate/action-planner.d.ts +10 -1
  44. package/dist/types/migrate/adapters/codex.d.ts +1 -1
  45. package/dist/types/migrate/adapters/index.d.ts +35 -3
  46. package/dist/types/migrate/hook-module.d.ts +23 -0
  47. package/dist/types/migrate/report.d.ts +2 -1
  48. package/dist/types/migrate/types.d.ts +69 -2
  49. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  50. package/dist/types/modes/components/model-selector.d.ts +43 -0
  51. package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
  52. package/dist/types/modes/utils/host-status.d.ts +15 -11
  53. package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
  54. package/dist/types/sdk/broker/broker-exit.d.ts +1 -1
  55. package/dist/types/sdk/broker/broker.d.ts +22 -1
  56. package/dist/types/sdk/broker/ensure.d.ts +5 -2
  57. package/dist/types/sdk/broker/hop.d.ts +9 -0
  58. package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
  59. package/dist/types/sdk/broker/session-index.d.ts +2 -0
  60. package/dist/types/sdk/broker/transport.d.ts +2 -0
  61. package/dist/types/sdk/bus/index.d.ts +21 -0
  62. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -1
  63. package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
  64. package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
  65. package/dist/types/sdk/host/control/operations.d.ts +5 -1
  66. package/dist/types/sdk/host/events.d.ts +4 -0
  67. package/dist/types/sdk/host/host.d.ts +2 -1
  68. package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
  69. package/dist/types/sdk/host/session-runtime.d.ts +8 -8
  70. package/dist/types/sdk/host/surface-policy.d.ts +2 -1
  71. package/dist/types/sdk/lifecycle/service.d.ts +12 -0
  72. package/dist/types/sdk/prompt-status.d.ts +11 -4
  73. package/dist/types/session/agent-session.d.ts +50 -8
  74. package/dist/types/session/session-manager.d.ts +16 -0
  75. package/dist/types/skill-state/workflow-mutation-guard.d.ts +13 -1
  76. package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
  77. package/dist/types/ssh/ssh-executor.d.ts +8 -1
  78. package/dist/types/system-prompt.d.ts +3 -2
  79. package/dist/types/task/role-body.d.ts +11 -0
  80. package/dist/types/tools/awaiting-input.d.ts +28 -0
  81. package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -0
  82. package/dist/types/tools/bash-skill-urls.d.ts +4 -0
  83. package/dist/types/tools/bash.d.ts +7 -0
  84. package/dist/types/tools/descriptors.d.ts +13 -2
  85. package/dist/types/tools/fetch.d.ts +6 -0
  86. package/dist/types/tools/irc.d.ts +5 -3
  87. package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
  88. package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
  89. package/dist/types/tools/write.d.ts +2 -0
  90. package/dist/types/utils/file-mentions.d.ts +5 -0
  91. package/dist/types/utils/image-limits.d.ts +7 -0
  92. package/dist/types/utils/image-loading.d.ts +0 -1
  93. package/dist/types/utils/open.d.ts +0 -1
  94. package/dist/types/utils/pasted-image-loading.d.ts +1 -5
  95. package/dist/types/utils/prompt-suggestion.d.ts +2 -2
  96. package/dist/types/utils/shell-snapshot.d.ts +2 -0
  97. package/dist/types/utils/title-generator.d.ts +13 -3
  98. package/dist/types/web/search/providers/codex.d.ts +6 -7
  99. package/package.json +9 -8
  100. package/scripts/acp-conformance-agent.ts +12 -2
  101. package/scripts/generate-sdk-operation-inventory.ts +16 -0
  102. package/src/capability/fs.ts +31 -3
  103. package/src/capability/rule.ts +11 -2
  104. package/src/cli/append-system-prompt.ts +12 -0
  105. package/src/cli/args.ts +11 -7
  106. package/src/cli/customize-doctor-precedence.ts +31 -0
  107. package/src/cli/customize-doctor.ts +49 -39
  108. package/src/cli/fast-help.ts +0 -2
  109. package/src/cli/migrate-cli.ts +14 -3
  110. package/src/cli/public-command-errors.ts +2 -0
  111. package/src/cli/root-flags.ts +0 -2
  112. package/src/cli/update-cli.ts +247 -6
  113. package/src/commands/harness.ts +25 -15
  114. package/src/commands/migrate.ts +3 -3
  115. package/src/commands/sdk.ts +81 -11
  116. package/src/commit/agentic/agent.ts +6 -6
  117. package/src/commit/analysis/conventional.ts +14 -5
  118. package/src/commit/map-reduce/index.ts +7 -7
  119. package/src/commit/pipeline.ts +12 -12
  120. package/src/config/autorouting-tier-map.ts +20 -1
  121. package/src/config/config-file.ts +106 -3
  122. package/src/config/config-hot-reload.ts +8 -0
  123. package/src/config/file-lock.ts +1431 -304
  124. package/src/config/keybindings.ts +154 -7
  125. package/src/config/model-profile-activation.ts +177 -39
  126. package/src/config/model-registry.ts +380 -55
  127. package/src/config/model-resolver.ts +49 -21
  128. package/src/config/settings-schema.ts +13 -1
  129. package/src/config/settings.ts +2 -0
  130. package/src/coordinator-mcp/server.ts +37 -2
  131. package/src/crash/sanitize.ts +3 -1
  132. package/src/customization/import.ts +276 -7
  133. package/src/customization/inventory.ts +24 -8
  134. package/src/customization/types.ts +1 -0
  135. package/src/dap/client.ts +3 -2
  136. package/src/dap/session.ts +161 -36
  137. package/src/debug/crash-diagnostics.ts +87 -5
  138. package/src/debug/report-bundle.ts +71 -2
  139. package/src/discovery/builtin.ts +4 -3
  140. package/src/discovery/helpers.ts +24 -14
  141. package/src/edit/index.ts +191 -5
  142. package/src/edit/modes/patch.ts +27 -0
  143. package/src/eval/py/runtime.ts +44 -4
  144. package/src/exec/non-interactive-env.ts +0 -1
  145. package/src/export/html/index.ts +60 -20
  146. package/src/export/ttsr.ts +176 -1
  147. package/src/extensibility/extensions/loader.ts +2 -0
  148. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
  149. package/src/extensibility/extensions/types.ts +5 -1
  150. package/src/extensibility/hooks/loader.ts +10 -2
  151. package/src/extensibility/plugins/marketplace/manager.ts +36 -10
  152. package/src/extensibility/slash-commands.ts +5 -0
  153. package/src/gjc-runtime/gc-runtime.ts +4 -1
  154. package/src/gjc-runtime/launch-tmux.ts +23 -13
  155. package/src/gjc-runtime/ralplan-current-final.ts +294 -0
  156. package/src/gjc-runtime/ralplan-runtime.ts +95 -1
  157. package/src/gjc-runtime/state-writer.ts +109 -5
  158. package/src/gjc-runtime/tmux-sessions.ts +93 -13
  159. package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
  160. package/src/gjc-runtime/ultragoal-guard.ts +9 -1
  161. package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
  162. package/src/harness-control-plane/finalize.ts +11 -0
  163. package/src/harness-control-plane/receipt-spool.ts +105 -28
  164. package/src/harness-control-plane/storage.ts +124 -6
  165. package/src/hindsight/content.ts +116 -16
  166. package/src/hindsight/mental-models.ts +22 -10
  167. package/src/hindsight/state.ts +7 -4
  168. package/src/hooks/native-skill-hook.ts +3 -1
  169. package/src/internal-urls/artifact-protocol.ts +3 -1
  170. package/src/internal-urls/docs-index.generated.ts +14 -14
  171. package/src/internal-urls/local-protocol.ts +28 -3
  172. package/src/internal-urls/types.ts +2 -0
  173. package/src/lsp/client.ts +2 -1
  174. package/src/lsp/clients/biome-client.ts +2 -0
  175. package/src/lsp/clients/swiftlint-client.ts +2 -0
  176. package/src/main.ts +330 -92
  177. package/src/memories/index.ts +25 -2
  178. package/src/migrate/action-planner.ts +517 -2
  179. package/src/migrate/adapters/claude-code.ts +141 -3
  180. package/src/migrate/adapters/codex.ts +57 -5
  181. package/src/migrate/adapters/index.ts +240 -5
  182. package/src/migrate/adapters/opencode.ts +4 -0
  183. package/src/migrate/executor.ts +141 -3
  184. package/src/migrate/hook-module.ts +193 -0
  185. package/src/migrate/report.ts +45 -1
  186. package/src/migrate/types.ts +73 -3
  187. package/src/modes/acp/acp-agent.ts +10 -1
  188. package/src/modes/components/agent-dashboard.ts +20 -4
  189. package/src/modes/components/customization/customization-dashboard.ts +7 -0
  190. package/src/modes/components/customization/import-wizard.ts +29 -3
  191. package/src/modes/components/model-selector.ts +157 -103
  192. package/src/modes/components/welcome.ts +3 -1
  193. package/src/modes/controllers/command-controller.ts +271 -16
  194. package/src/modes/controllers/event-controller.ts +1 -1
  195. package/src/modes/controllers/extension-ui-controller.ts +31 -1
  196. package/src/modes/controllers/goal-mode-controller.ts +79 -10
  197. package/src/modes/controllers/input-controller.ts +2 -0
  198. package/src/modes/controllers/plan-mode-controller.ts +49 -4
  199. package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
  200. package/src/modes/controllers/selector-controller.ts +14 -1
  201. package/src/modes/interactive-mode.ts +55 -4
  202. package/src/modes/prompt-suggestion-controller.ts +1 -1
  203. package/src/modes/runtime-init.ts +5 -1
  204. package/src/modes/utils/host-status.ts +21 -10
  205. package/src/prompts/agent-fragments/browser-aside.md +2 -2
  206. package/src/prompts/system/irc-incoming.md +3 -1
  207. package/src/prompts/tools/irc.md +1 -1
  208. package/src/runtime-mcp/manager.ts +8 -3
  209. package/src/runtime-mcp/project-host-values.ts +12 -8
  210. package/src/runtime-mcp/tool-bridge.ts +68 -4
  211. package/src/runtime-mcp/transports/stdio.ts +2 -1
  212. package/src/sdk/broker/broker-exit.ts +2 -0
  213. package/src/sdk/broker/broker.ts +143 -4
  214. package/src/sdk/broker/ensure.ts +74 -6
  215. package/src/sdk/broker/hop.ts +13 -1
  216. package/src/sdk/broker/lifecycle-ledger.ts +3 -0
  217. package/src/sdk/broker/lifecycle.ts +102 -6
  218. package/src/sdk/broker/session-index.ts +29 -4
  219. package/src/sdk/broker/transport.ts +17 -1
  220. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  221. package/src/sdk/bus/index.ts +408 -14
  222. package/src/sdk/bus/session-control.ts +2 -0
  223. package/src/sdk/bus/slack-daemon.ts +105 -9
  224. package/src/sdk/bus/telegram-daemon-contract.ts +7 -1
  225. package/src/sdk/bus/telegram-daemon.ts +10 -2
  226. package/src/sdk/bus/threaded-render.ts +20 -1
  227. package/src/sdk/cli/session-cli.ts +2 -0
  228. package/src/sdk/host/control/dispatch.ts +13 -1
  229. package/src/sdk/host/control/operations.ts +10 -1
  230. package/src/sdk/host/events.ts +67 -4
  231. package/src/sdk/host/host.ts +8 -2
  232. package/src/sdk/host/prompt-image-upload.ts +438 -0
  233. package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
  234. package/src/sdk/host/session-runtime.test.ts +1537 -24
  235. package/src/sdk/host/session-runtime.ts +332 -36
  236. package/src/sdk/host/surface-policy.ts +4 -0
  237. package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
  238. package/src/sdk/host/websocket-transport.ts +19 -7
  239. package/src/sdk/lifecycle/service.ts +14 -1
  240. package/src/sdk/prompt-status.ts +6 -4
  241. package/src/sdk/protocol/adapter-validation.ts +28 -1
  242. package/src/sdk/protocol/operation-inventory.generated.json +160 -0
  243. package/src/sdk/protocol/operation-registry.ts +33 -3
  244. package/src/sdk/session.ts +123 -36
  245. package/src/session/agent-session.ts +1614 -329
  246. package/src/session/history-storage.ts +182 -12
  247. package/src/session/internal/managed-session-scope.ts +47 -18
  248. package/src/session/internal/managed-session-storage.ts +71 -1
  249. package/src/session/session-manager.ts +90 -5
  250. package/src/session-import/redact.ts +53 -2
  251. package/src/skill-state/workflow-mutation-guard.ts +1498 -18
  252. package/src/slash-commands/builtin-registry.ts +6 -3
  253. package/src/slash-commands/helpers/mcp.ts +11 -3
  254. package/src/ssh/ssh-executor.ts +9 -2
  255. package/src/stt/recorder.ts +26 -9
  256. package/src/system-prompt.ts +61 -16
  257. package/src/task/executor.ts +12 -8
  258. package/src/task/role-body.ts +84 -0
  259. package/src/task/token-log-mode.test.ts +162 -0
  260. package/src/task/token-log.ts +91 -2
  261. package/src/tools/archive-reader.ts +650 -37
  262. package/src/tools/ask.ts +1 -0
  263. package/src/tools/ast-grep.ts +7 -2
  264. package/src/tools/auto-generated-guard.ts +12 -3
  265. package/src/tools/awaiting-input.ts +114 -0
  266. package/src/tools/bash-allowed-prefixes.ts +61 -1
  267. package/src/tools/bash-skill-urls.ts +58 -5
  268. package/src/tools/bash.ts +99 -26
  269. package/src/tools/browser/tab-worker.ts +39 -13
  270. package/src/tools/browser.ts +17 -7
  271. package/src/tools/cron.ts +10 -1
  272. package/src/tools/debug.ts +9 -2
  273. package/src/tools/descriptors.test.ts +4 -4
  274. package/src/tools/descriptors.ts +16 -4
  275. package/src/tools/eval.ts +15 -0
  276. package/src/tools/fetch.ts +90 -2
  277. package/src/tools/gh.ts +27 -12
  278. package/src/tools/image-gen.ts +20 -6
  279. package/src/tools/irc.ts +5 -3
  280. package/src/tools/monitor.ts +9 -1
  281. package/src/tools/output-meta.ts +51 -3
  282. package/src/tools/read.ts +9 -2
  283. package/src/tools/recipe/runner.ts +23 -12
  284. package/src/tools/render-utils.ts +2 -2
  285. package/src/tools/search.ts +3 -2
  286. package/src/tools/ssh.ts +274 -8
  287. package/src/tools/subagent.ts +10 -4
  288. package/src/tools/tmux-self-injection-guard.ts +3301 -122
  289. package/src/tools/tool-catalog.generated.ts +1 -1
  290. package/src/tools/ultragoal-ask-guard.ts +13 -1
  291. package/src/tools/write.ts +7 -0
  292. package/src/tui/tree-list.ts +15 -1
  293. package/src/utils/commit-message-generator.ts +3 -2
  294. package/src/utils/external-editor.ts +21 -7
  295. package/src/utils/file-mentions.ts +46 -6
  296. package/src/utils/git.ts +38 -7
  297. package/src/utils/image-limits.ts +8 -0
  298. package/src/utils/image-loading.ts +1 -1
  299. package/src/utils/open.ts +73 -4
  300. package/src/utils/pasted-image-loading.ts +9 -8
  301. package/src/utils/prompt-suggestion.ts +5 -4
  302. package/src/utils/shell-snapshot.ts +9 -1
  303. package/src/utils/title-generator.ts +65 -20
  304. package/src/web/search/index.ts +2 -1
  305. package/src/web/search/providers/codex.ts +29 -32
@@ -2509,7 +2509,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
2509
2509
  "irc": {
2510
2510
  "name": "irc",
2511
2511
  "label": "IRC",
2512
- "description": "Sends short text messages to other live agents in this process and receives their prose replies.\n\n<instruction>\n- The main agent is addressable as `0-Main`. Subagents reuse their task id (e.g. `0-AuthLoader`).\n- `op: \"list\"` returns the current set of visible peers. Use it before sending if you are not sure who is live.\n- `op: \"send\"` delivers `message` to `to`. `to` may be a specific id or `\"all\"` to broadcast.\n- `awaitReply` (optional): wait for a prose reply. Defaults to `true` for DMs and `false` for `to: \"all\"` broadcasts.\n- The recipient generates the reply via an ephemeral side-channel turn that uses their current model, system prompt, and history — it does **not** wait for the recipient's main loop to be free, so it is safe to IRC an agent that is currently inside a long-running tool call.\n- The exchange (incoming question + auto-reply) is queued for injection into the recipient's persisted history; the recipient sees it on its next turn and can follow up if needed.\n</instruction>\n\n<conditions>\nYou SHOULD reach for `irc` proactively when continuing alone is wasteful or wrong. When in doubt, prefer messaging.\n- **Unexpected state.** You hit something the original task did not describe — a missing file, a config that contradicts the assignment, an API behaving differently than you were told, a tool failing in a way that suggests the spec is wrong. DM `0-Main` (or the spawning agent) for guidance instead of guessing.\n- **Blocked by another agent.** A peer holds the file/branch/resource you need, has already started the change you are about to make, or owns a decision you depend on. DM that peer (or broadcast to discover who) before duplicating or stepping on work.\n- **Decision points outside your scope.** A genuine fork in the road that the assignment did not pre-decide (e.g. which of two viable APIs to use, whether to refactor adjacent code). Ask the requester rather than picking unilaterally.\n- **Coordination opportunities.** Before editing a shared file or relying on another agent's in-flight API, message the relevant peer; proactively share state that affects their work.\n\nDo **not** use `irc` for: routine progress updates, things you can verify with a tool call, or questions whose answer is already in your assignment / repo / docs.\n</conditions>\n\n<etiquette>\nThese rules apply to both sending and replying.\n- **Plain prose only.** Do not send structured JSON status payloads (e.g. `{\"type\":\"task_completed\",…}`). Write a normal sentence: \"Done with the auth refactor — left a TODO in `src/server/auth.ts` for the rate limiter.\"\n- **Do not quote the message you are replying to.** The sender already saw it; the TUI already renders it. Lead with the answer.\n- **Use IRC, not terminal tools, to learn about peers.** Do not `search` artifacts, read other sessions' JSONL files, or shell-poke around to figure out what another agent is doing. DM them — they have the live answer and you do not.\n- **One round-trip is enough.** Replies arrive synchronously when the recipient is reachable. Do not follow up with \"did you get my message?\" — they did. If `delivered` is empty or the result was `failed`, the peer is unavailable; move on or report the blocker, do not retry in a loop.\n- **Stay terse.** A DM is a chat message, not a memo. One question per send when you can. Share file paths and artifacts via `local://` / `artifact://` URLs instead of pasting blobs.\n- **Address peers by id.** Use the exact id from `op: \"list\"` (e.g. `0-AuthLoader`, `0-Main`). Do not invent friendly names.\n- **Do not IRC for things a tool would answer.** If a `read`, `search`, or build command would resolve the question, do that first.\n- **When you receive an IRC message, answer it before continuing.** The recipient injects the question + your auto-reply into your history; address it directly, do not repeat it back to the user.\n</etiquette>\n\n<output>\n- `send`: returns each recipient that received the message and any prose replies that arrived.\n- `list`: returns peers and channels visible to the caller.\n</output>\n\n<examples>\n# List peers\n`{\"op\": \"list\"}`\n# Direct message to the main agent (waits for prose reply)\n`{\"op\": \"send\", \"to\": \"0-Main\", \"message\": \"Should I prefer JWT or session cookies for the auth flow?\"}`\n# Unexpected state — ask the originator\n`{\"op\": \"send\", \"to\": \"0-Main\", \"message\": \"Assignment says edit src/auth/jwt.ts but the file does not exist. Is the new path src/server/auth/jwt.ts?\"}`\n# Blocked by a peer — ask them directly\n`{\"op\": \"send\", \"to\": \"0-AuthLoader\", \"message\": \"Are you still touching src/server/auth.ts? I need to add a 401 path; OK to proceed or should I wait?\"}`\n# Broadcast to discover who owns something (no replies, just informs them)\n`{\"op\": \"send\", \"to\": \"all\", \"message\": \"About to refactor src/server/middleware/*. Anyone already in there?\", \"awaitReply\": false}`\n</examples>",
2512
+ "description": "Sends short text messages to other live agents in this process and receives their prose replies.\n\n<instruction>\n- The main agent is addressable as `0-Main`. Subagents reuse their task id (e.g. `0-AuthLoader`).\n- `op: \"list\"` returns the current set of visible peers. Use it before sending if you are not sure who is live.\n- `op: \"send\"` delivers `message` to `to`. `to` may be a specific id or `\"all\"` to broadcast.\n- `awaitReply` (optional): wait for a prose reply. Defaults to `true` for DMs and `false` for `to: \"all\"` broadcasts.\n- The recipient generates the reply via an ephemeral side-channel turn that uses their current model, system prompt, and history — it does **not** wait for the recipient's main loop to be free, so it is safe to IRC an agent that is currently inside a long-running tool call.\n- The exchange (incoming question + auto-reply) is injected into the recipient's persisted history as soon as it can be delivered: an idle recipient gets it immediately, and a recipient that is already streaming receives it at its next step boundary instead of at the end of its turn. The recipient can then follow up if needed.\n</instruction>\n\n<conditions>\nYou SHOULD reach for `irc` proactively when continuing alone is wasteful or wrong. When in doubt, prefer messaging.\n- **Unexpected state.** You hit something the original task did not describe — a missing file, a config that contradicts the assignment, an API behaving differently than you were told, a tool failing in a way that suggests the spec is wrong. DM `0-Main` (or the spawning agent) for guidance instead of guessing.\n- **Blocked by another agent.** A peer holds the file/branch/resource you need, has already started the change you are about to make, or owns a decision you depend on. DM that peer (or broadcast to discover who) before duplicating or stepping on work.\n- **Decision points outside your scope.** A genuine fork in the road that the assignment did not pre-decide (e.g. which of two viable APIs to use, whether to refactor adjacent code). Ask the requester rather than picking unilaterally.\n- **Coordination opportunities.** Before editing a shared file or relying on another agent's in-flight API, message the relevant peer; proactively share state that affects their work.\n\nDo **not** use `irc` for: routine progress updates, things you can verify with a tool call, or questions whose answer is already in your assignment / repo / docs.\n</conditions>\n\n<etiquette>\nThese rules apply to both sending and replying.\n- **Plain prose only.** Do not send structured JSON status payloads (e.g. `{\"type\":\"task_completed\",…}`). Write a normal sentence: \"Done with the auth refactor — left a TODO in `src/server/auth.ts` for the rate limiter.\"\n- **Do not quote the message you are replying to.** The sender already saw it; the TUI already renders it. Lead with the answer.\n- **Use IRC, not terminal tools, to learn about peers.** Do not `search` artifacts, read other sessions' JSONL files, or shell-poke around to figure out what another agent is doing. DM them — they have the live answer and you do not.\n- **One round-trip is enough.** Replies arrive synchronously when the recipient is reachable. Do not follow up with \"did you get my message?\" — they did. If `delivered` is empty or the result was `failed`, the peer is unavailable; move on or report the blocker, do not retry in a loop.\n- **Stay terse.** A DM is a chat message, not a memo. One question per send when you can. Share file paths and artifacts via `local://` / `artifact://` URLs instead of pasting blobs.\n- **Address peers by id.** Use the exact id from `op: \"list\"` (e.g. `0-AuthLoader`, `0-Main`). Do not invent friendly names.\n- **Do not IRC for things a tool would answer.** If a `read`, `search`, or build command would resolve the question, do that first.\n- **When you receive an IRC message, answer it before continuing.** The recipient injects the question + your auto-reply into your history; address it directly, do not repeat it back to the user.\n</etiquette>\n\n<output>\n- `send`: returns each recipient that received the message and any prose replies that arrived.\n- `list`: returns peers and channels visible to the caller.\n</output>\n\n<examples>\n# List peers\n`{\"op\": \"list\"}`\n# Direct message to the main agent (waits for prose reply)\n`{\"op\": \"send\", \"to\": \"0-Main\", \"message\": \"Should I prefer JWT or session cookies for the auth flow?\"}`\n# Unexpected state — ask the originator\n`{\"op\": \"send\", \"to\": \"0-Main\", \"message\": \"Assignment says edit src/auth/jwt.ts but the file does not exist. Is the new path src/server/auth/jwt.ts?\"}`\n# Blocked by a peer — ask them directly\n`{\"op\": \"send\", \"to\": \"0-AuthLoader\", \"message\": \"Are you still touching src/server/auth.ts? I need to add a 401 path; OK to proceed or should I wait?\"}`\n# Broadcast to discover who owns something (no replies, just informs them)\n`{\"op\": \"send\", \"to\": \"all\", \"message\": \"About to refactor src/server/middleware/*. Anyone already in there?\", \"awaitReply\": false}`\n</examples>",
2513
2513
  "parameters": {
2514
2514
  "type": "object",
2515
2515
  "properties": {
@@ -1,4 +1,5 @@
1
1
  import type { AgentTool } from "@gajae-code/agent-core";
2
+ import { resolveRalplanCurrentFinal, selectsRalplanApprovalGate } from "../gjc-runtime/ralplan-current-final";
2
3
  import {
3
4
  consumeUltragoalAskNudge,
4
5
  isUltragoalAskBlocked,
@@ -33,6 +34,7 @@ export async function assertUltragoalAskAllowed(
33
34
  cwd: string,
34
35
  context: UltragoalAskGuardContext = {},
35
36
  agentDir?: string,
37
+ pendingQuestions?: unknown,
36
38
  ): Promise<void> {
37
39
  // Durable Ultragoal state is session-scoped, so only the caller's own run may
38
40
  // block `ask`. An anonymous caller has no run attributable to it, so the guard
@@ -43,6 +45,16 @@ export async function assertUltragoalAskAllowed(
43
45
  if (!sessionId) return;
44
46
  const diagnostic = await isUltragoalAskBlocked(cwd, { sessionId });
45
47
  if (!diagnostic.active) return;
48
+ // The one ask that may proceed while a goal is blocked is the execution
49
+ // approval for an authenticated current ralplan final (issue #6578). The
50
+ // request must select that gate and the final must validate on disk; a
51
+ // caller-declared tag alone never lifts the guard. AskTool still captures and
52
+ // authenticates the publication it records consent against.
53
+ if (selectsRalplanApprovalGate(pendingQuestions)) {
54
+ const resolution = await resolveRalplanCurrentFinal({ cwd, sessionId, agentDir });
55
+ if (resolution.ok) return;
56
+ throw new ToolError(`Ultragoal ask guard refused the ralplan execution approval: ${resolution.refusal.reason}`);
57
+ }
46
58
  const nudge = await consumeUltragoalAskNudge(cwd, sessionId, agentDir);
47
59
  if (nudge.nudged) throw new ToolError(nudge.message);
48
60
  throw new ToolError(formatUltragoalAskBlockMessage(diagnostic));
@@ -66,7 +78,7 @@ export function guardToolForUltragoalAsk<T extends AgentTool>(
66
78
  // budget against the SESSION profile, never the process-global one.
67
79
  // The caller session id is the only binding, so a foreign session's
68
80
  // durable run can never gate this ask.
69
- await assertUltragoalAskAllowed(getCwd(), getContext(), getSessionAgentDir());
81
+ await assertUltragoalAskAllowed(getCwd(), getContext(), getSessionAgentDir(), args[1]);
70
82
  return Reflect.apply(target.execute, target, args);
71
83
  };
72
84
  },
@@ -218,6 +218,13 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
218
218
  readonly loadMode = "discoverable";
219
219
  readonly summary = "Write content to a file (creates or overwrites)";
220
220
 
221
+ /** Stream matchers should see the real file content, not its JSON-escaped argument encoding. */
222
+ matcherDigest(args: unknown): string | undefined {
223
+ if (!args || typeof args !== "object" || Array.isArray(args)) return undefined;
224
+ const content = (args as Partial<WriteParams>).content;
225
+ return typeof content === "string" ? content : undefined;
226
+ }
227
+
221
228
  readonly #writethrough: WritethroughCallback;
222
229
 
223
230
  constructor(private readonly session: ToolSession) {
@@ -3,11 +3,25 @@
3
3
  */
4
4
 
5
5
  import { replaceTabs } from "@gajae-code/tui";
6
+ import { sanitizeText } from "@gajae-code/utils";
6
7
  import type { Theme } from "../modes/theme/theme";
7
8
  import { formatMoreItems } from "../tools/render-utils";
8
9
  import type { TreeContext } from "./types";
9
10
  import { getTreeBranch, getTreeContinuePrefix } from "./utils";
10
11
 
12
+ /**
13
+ * Tool renderers pass raw match lines or line groups into `renderItem`, which
14
+ * then applies theme colors. Strip controls first so OSC bytes are not wrapped
15
+ * into the styled string. Objects (file entries, todos) are left unchanged.
16
+ */
17
+ function sanitizeTreeItem<T>(item: T): T {
18
+ if (typeof item === "string") return sanitizeText(item) as T;
19
+ if (Array.isArray(item)) {
20
+ return item.map(part => (typeof part === "string" ? sanitizeText(part) : part)) as T;
21
+ }
22
+ return item;
23
+ }
24
+
11
25
  export interface TreeListOptions<T> {
12
26
  items: T[];
13
27
  expanded?: boolean;
@@ -32,7 +46,7 @@ export function renderTreeList<T>(options: TreeListOptions<T>, theme: Theme): st
32
46
  // renderItem implementations MUST NOT vary line count based on isLast.
33
47
  const preRendered: string[][] = [];
34
48
  for (let i = 0; i < maxItems; i++) {
35
- const rendered = renderItem(items[i], {
49
+ const rendered = renderItem(sanitizeTreeItem(items[i]!), {
36
50
  index: i,
37
51
  isLast: false,
38
52
  depth: 0,
@@ -44,8 +44,9 @@ function redactCommitMessageText(text: string): string {
44
44
  function getModelCandidates(
45
45
  registry: ModelRegistry,
46
46
  settings: Settings,
47
+ credentialSessionId?: string,
47
48
  ): Array<{ model: Model<Api>; thinkingLevel?: ThinkingLevel }> {
48
- const availableModels = registry.getAvailable();
49
+ const availableModels = registry.getAvailable(credentialSessionId);
49
50
  if (availableModels.length === 0) return [];
50
51
 
51
52
  const candidates: Array<{ model: Model<Api>; thinkingLevel?: ThinkingLevel }> = [];
@@ -80,7 +81,7 @@ export async function generateCommitMessage(
80
81
  settings: Settings,
81
82
  sessionId?: string,
82
83
  ): Promise<string | null> {
83
- const candidates = getModelCandidates(registry, settings);
84
+ const candidates = getModelCandidates(registry, settings, sessionId);
84
85
  if (candidates.length === 0) {
85
86
  logger.debug("commit-msg-generator: no model found");
86
87
  return null;
@@ -6,7 +6,7 @@ import { spawn } from "node:child_process";
6
6
  import * as fs from "node:fs/promises";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
- import { $pickCredentialEnv, Snowflake } from "@gajae-code/utils";
9
+ import { $pickCredentialEnv } from "@gajae-code/utils";
10
10
 
11
11
  /** Returns the user's preferred editor command, or undefined if not configured. */
12
12
  export function getEditorCommand(): string | undefined {
@@ -119,10 +119,22 @@ export async function openInEditor(
119
119
  options?: OpenInEditorOptions,
120
120
  ): Promise<string | null> {
121
121
  const ext = options?.extension ?? ".md";
122
- const tmpFile = path.join(os.tmpdir(), `gjc-editor-${Snowflake.next()}${ext}`);
122
+ // Random directory plus an exclusive create: a preexisting symlink under the
123
+ // temp directory is not opened, and the draft is not left world-readable.
124
+ let tempDir: string | undefined;
125
+ let tmpFile = "";
123
126
 
124
127
  try {
125
- await Bun.write(tmpFile, content);
128
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "gjc-editor-"));
129
+ if (process.platform !== "win32") await fs.chmod(tempDir, 0o700);
130
+ tmpFile = path.join(tempDir, `draft${ext}`);
131
+ const handle = await fs.open(tmpFile, "wx", 0o600);
132
+ try {
133
+ await handle.writeFile(content);
134
+ } finally {
135
+ await handle.close();
136
+ }
137
+ if (process.platform !== "win32") await fs.chmod(tmpFile, 0o600);
126
138
 
127
139
  const [editor, ...editorArgs] = editorCmd.split(" ");
128
140
  const stdio = options?.stdio ?? ["inherit", "inherit", "inherit"];
@@ -147,10 +159,12 @@ export async function openInEditor(
147
159
  }
148
160
  return null;
149
161
  } finally {
150
- try {
151
- await fs.rm(tmpFile, { force: true });
152
- } catch {
153
- // Ignore cleanup errors
162
+ if (tempDir) {
163
+ try {
164
+ await fs.rm(tempDir, { recursive: true, force: true });
165
+ } catch {
166
+ // Ignore cleanup errors
167
+ }
154
168
  }
155
169
  }
156
170
  }
@@ -11,7 +11,7 @@ import type { AgentMessage } from "@gajae-code/agent-core";
11
11
  import type { ImageContent } from "@gajae-code/ai/core";
12
12
  import type { glob as globFn } from "@gajae-code/natives";
13
13
  import { fuzzyMatch } from "@gajae-code/tui";
14
- import { formatAge, formatBytes, readImageMetadata } from "@gajae-code/utils";
14
+ import { formatAge, formatBytes, pathIsWithin, readImageMetadata, resolveEquivalentPath } from "@gajae-code/utils";
15
15
  import { formatHashLines } from "../hashline/hash";
16
16
  import type { FileMentionMessage } from "../session/messages";
17
17
  import {
@@ -313,6 +313,26 @@ export function extractFileMentions(text: string): string[] {
313
313
  return [...new Set(mentions)];
314
314
  }
315
315
 
316
+ /**
317
+ * Compare canonical Windows paths without folding directory names: individual
318
+ * directories can opt into case-sensitive names.
319
+ */
320
+ export function isCanonicalWindowsPathWithin(root: string, candidate: string): boolean {
321
+ if (!path.win32.isAbsolute(root) || !path.win32.isAbsolute(candidate)) return false;
322
+
323
+ const canonicalRoot = path.win32.resolve(root);
324
+ const canonicalCandidate = path.win32.resolve(candidate);
325
+ const rootParts = path.win32.parse(canonicalRoot);
326
+ const candidateParts = path.win32.parse(canonicalCandidate);
327
+ if (rootParts.root.toLowerCase() !== candidateParts.root.toLowerCase()) return false;
328
+
329
+ const rootSegments = canonicalRoot.slice(rootParts.root.length).split(path.win32.sep).filter(Boolean);
330
+ const candidateSegments = canonicalCandidate.slice(candidateParts.root.length).split(path.win32.sep).filter(Boolean);
331
+ if (candidateSegments.length < rootSegments.length) return false;
332
+
333
+ return rootSegments.every((segment, index) => segment === candidateSegments[index]);
334
+ }
335
+
316
336
  /**
317
337
  * Generate a FileMentionMessage containing the contents of mentioned files.
318
338
  * Returns empty array if no files could be read.
@@ -336,6 +356,8 @@ export async function generateFileMentionMessages(
336
356
  if (filePaths.length === 0) return [];
337
357
 
338
358
  const autoResizeImages = options?.autoResizeImages ?? true;
359
+ const canonicalWindowsCwd = process.platform === "win32" ? await fs.realpath(cwd).catch(() => undefined) : undefined;
360
+ if (process.platform === "win32" && canonicalWindowsCwd === undefined) return [];
339
361
 
340
362
  const files: FileMentionMessage["files"] = [];
341
363
  let mentionCandidatesPromise: Promise<MentionCandidate[]> | null = null;
@@ -351,6 +373,24 @@ export async function generateFileMentionMessages(
351
373
  continue;
352
374
  }
353
375
  const absolutePath = resolveReadPath(resolvedPath, cwd);
376
+ // Skip `..`, `~`, and outside absolute paths. On Windows, realpath both
377
+ // paths and compare exact components because directories can be case-sensitive.
378
+ // Fail closed if either real path is unavailable. Read the canonical path so
379
+ // replacing the original mention symlink does not retarget this read.
380
+ const canonicalPath =
381
+ process.platform === "win32"
382
+ ? await fs.realpath(absolutePath).catch(() => undefined)
383
+ : resolveEquivalentPath(absolutePath);
384
+ if (!canonicalPath) {
385
+ continue;
386
+ }
387
+ const staysWithinSession =
388
+ process.platform === "win32"
389
+ ? canonicalWindowsCwd !== undefined && isCanonicalWindowsPathWithin(canonicalWindowsCwd, canonicalPath)
390
+ : pathIsWithin(cwd, canonicalPath);
391
+ if (!staysWithinSession) {
392
+ continue;
393
+ }
354
394
  // Duplicate suppression, keyed by absolute path: same path shown earlier this
355
395
  // batch or recently in the conversation → compact note, no re-read (the path
356
396
  // stays referenced so the model can re-fetch via the read tool).
@@ -364,14 +404,14 @@ export async function generateFileMentionMessages(
364
404
  }
365
405
  shownInBatch.add(absolutePath);
366
406
  try {
367
- const stat = await Bun.file(absolutePath).stat();
407
+ const stat = await Bun.file(canonicalPath).stat();
368
408
  if (stat.isDirectory()) {
369
- const { output, lineCount } = await buildDirectoryListing(absolutePath);
409
+ const { output, lineCount } = await buildDirectoryListing(canonicalPath);
370
410
  files.push({ path: resolvedPath, content: output, lineCount });
371
411
  continue;
372
412
  }
373
413
 
374
- const imageMetadata = await readImageMetadata(absolutePath);
414
+ const imageMetadata = await readImageMetadata(canonicalPath);
375
415
  const mimeType = imageMetadata?.mimeType;
376
416
  if (mimeType) {
377
417
  if (stat.size > MAX_AUTO_READ_IMAGE_BYTES) {
@@ -383,7 +423,7 @@ export async function generateFileMentionMessages(
383
423
  });
384
424
  continue;
385
425
  }
386
- const buffer = await fs.readFile(absolutePath);
426
+ const buffer = await fs.readFile(canonicalPath);
387
427
  if (buffer.length === 0) {
388
428
  continue;
389
429
  }
@@ -420,7 +460,7 @@ export async function generateFileMentionMessages(
420
460
  continue;
421
461
  }
422
462
 
423
- const content = await Bun.file(absolutePath).text();
463
+ const content = await Bun.file(canonicalPath).text();
424
464
  let { output, lineCount } = buildTextOutput(content, options?.maxInlineBytes);
425
465
  if (options?.useHashLines) {
426
466
  output = formatHashLines(output);
package/src/utils/git.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { $which, hasFsCode, isEnoent, Snowflake } from "@gajae-code/utils";
4
+ import { $which, hasFsCode, isEnoent } from "@gajae-code/utils";
5
+ import { projectSafeSpawnEnv } from "@gajae-code/utils/shell-config";
5
6
  import {
6
7
  parseDiffHunks as parseCommitDiffHunks,
7
8
  parseFileDiffs,
@@ -198,7 +199,10 @@ async function runCommand(
198
199
  const commandArgs = withShortLivedGitConfig(options.readOnly ? withNoOptionalLocks(args) : [...args]);
199
200
  const child = Bun.spawn(["git", ...commandArgs], {
200
201
  cwd,
201
- env: options.env ? { ...process.env, GIT_OPTIONAL_LOCKS: "0", ...options.env } : undefined,
202
+ // The inherited environment holds the autoloaded project `.env`; without
203
+ // filtering, a repository could set e.g. `GIT_CONFIG_*` (`core.hooksPath`)
204
+ // or `GIT_SSH_COMMAND` and have GJC's own commit/fetch/push run its code.
205
+ env: options.env ? { ...projectSafeSpawnEnv(), GIT_OPTIONAL_LOCKS: "0", ...options.env } : projectSafeSpawnEnv(),
202
206
  signal: options.signal,
203
207
  stdin: normalizeStdin(options.stdin),
204
208
  stdout: "pipe",
@@ -365,9 +369,24 @@ function buildApplyArgs(patchPath: string, options: PatchOptions): string[] {
365
369
  }
366
370
 
367
371
  async function writeTempPatch(content: string): Promise<string> {
368
- const tempPath = path.join(os.tmpdir(), `gjc-git-patch-${Snowflake.next()}.patch`);
369
- await Bun.write(tempPath, content);
370
- return tempPath;
372
+ // Random directory plus an exclusive create: a preexisting symlink under the
373
+ // shared temp directory is not opened, and the patch is not left world-readable.
374
+ const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "gjc-git-patch-"));
375
+ try {
376
+ if (process.platform !== "win32") await fs.promises.chmod(tempDir, 0o700);
377
+ const tempPath = path.join(tempDir, "patch");
378
+ const handle = await fs.promises.open(tempPath, "wx", 0o600);
379
+ try {
380
+ await handle.writeFile(content);
381
+ } finally {
382
+ await handle.close();
383
+ }
384
+ if (process.platform !== "win32") await fs.promises.chmod(tempPath, 0o600);
385
+ return tempPath;
386
+ } catch (error) {
387
+ await fs.promises.rm(tempDir, { recursive: true, force: true });
388
+ throw error;
389
+ }
371
390
  }
372
391
 
373
392
  // ════════════════════════════════════════════════════════════════════════════
@@ -1232,7 +1251,7 @@ export const patch = {
1232
1251
  try {
1233
1252
  await patch.apply(cwd, tempPath, options);
1234
1253
  } finally {
1235
- await fs.promises.rm(tempPath, { force: true });
1254
+ await fs.promises.rm(path.dirname(tempPath), { recursive: true, force: true });
1236
1255
  }
1237
1256
  },
1238
1257
 
@@ -1253,7 +1272,7 @@ export const patch = {
1253
1272
  try {
1254
1273
  return await patch.canApply(cwd, tempPath, options);
1255
1274
  } finally {
1256
- await fs.promises.rm(tempPath, { force: true });
1275
+ await fs.promises.rm(path.dirname(tempPath), { recursive: true, force: true });
1257
1276
  }
1258
1277
  },
1259
1278
 
@@ -1594,6 +1613,14 @@ function formatGhFailure(args: readonly string[], stdout: string, stderr: string
1594
1613
  return `GitHub CLI command failed: gh ${args.join(" ")}`;
1595
1614
  }
1596
1615
 
1616
+ /** Captured `gh` output never wants a pager or forced-terminal behavior. */
1617
+ function ghSpawnEnv(): Record<string, string> {
1618
+ const env = projectSafeSpawnEnv();
1619
+ delete env.GH_FORCE_TTY;
1620
+ env.GH_PAGER = "cat";
1621
+ return env;
1622
+ }
1623
+
1597
1624
  export const github = {
1598
1625
  /** Check if `gh` CLI is installed. */
1599
1626
  available(): boolean {
@@ -1609,6 +1636,10 @@ export const github = {
1609
1636
  try {
1610
1637
  const child = Bun.spawn(["gh", ...args], {
1611
1638
  cwd,
1639
+ // Same project-dotenv filtering as git: `gh` runs git for checkout
1640
+ // and push. Output is captured, so gh must never page or act as if
1641
+ // on a terminal; pin both so a project `.env` cannot select a pager.
1642
+ env: ghSpawnEnv(),
1612
1643
  stdin: "ignore",
1613
1644
  stdout: "pipe",
1614
1645
  stderr: "pipe",
@@ -0,0 +1,8 @@
1
+ // Shared input policy must remain independent of file loading, tools and sessions.
2
+ export const MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024;
3
+ export const MAX_STAGED_PROMPT_IMAGES = 16;
4
+ export const MAX_PASTED_IMAGE_SOURCE_BYTES = 64 * 1024 * 1024;
5
+ export const MAX_PASTED_IMAGE_OUTPUT_BYTES = 64 * 1024 * 1024;
6
+ export const MAX_PASTED_IMAGE_DIMENSION = 20_000;
7
+ export const MAX_PASTED_IMAGE_PIXELS = 40_000_000;
8
+ export const MAX_PASTED_IMAGE_DECODED_BYTES = 160 * 1024 * 1024;
@@ -2,9 +2,9 @@ import * as fs from "node:fs/promises";
2
2
  import type { ImageContent } from "@gajae-code/ai/core";
3
3
  import { formatBytes, readImageMetadata, SUPPORTED_IMAGE_MIME_TYPES } from "@gajae-code/utils";
4
4
  import { resolveReadPath } from "../tools/path-utils";
5
+ import { MAX_IMAGE_INPUT_BYTES } from "./image-limits";
5
6
  import { formatDimensionNote, resizeImageBuffer } from "./image-resize";
6
7
 
7
- export const MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024;
8
8
  export const SUPPORTED_INPUT_IMAGE_MIME_TYPES = SUPPORTED_IMAGE_MIME_TYPES;
9
9
 
10
10
  const SVG_MIME_TYPE = "image/svg+xml";
package/src/utils/open.ts CHANGED
@@ -1,15 +1,84 @@
1
- /** Open a URL or file path in the default browser/application. Best-effort, never throws. */
1
+ /** Open an http(s) URL or local path. Best-effort, never throws. */
2
+ const ALLOWED_OPEN_PROTOCOLS = new Set(["http:", "https:"]);
3
+
4
+ /**
5
+ * Zero-width and non-ASCII spaces. ASCII controls are rejected, not stripped:
6
+ * the URL parser drops some of them and would turn a hostile string into a clean href.
7
+ */
8
+ const OUTER_FORMAT = /[\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF\u200B\u2060]/;
9
+
10
+ function hasAsciiControl(value: string): boolean {
11
+ for (let i = 0; i < value.length; i++) {
12
+ const code = value.charCodeAt(i);
13
+ if (code <= 0x1f || code === 0x7f) return true;
14
+ }
15
+ return false;
16
+ }
17
+
18
+ function isOuterPadding(ch: string): boolean {
19
+ return ch.trim() === "" || OUTER_FORMAT.test(ch);
20
+ }
21
+
22
+ /** Drop every outer space or format character. Classification only; local paths stay raw. */
23
+ function peelPadding(value: string): string {
24
+ let start = 0;
25
+ let end = value.length;
26
+ while (start < end && isOuterPadding(value.charAt(start))) start++;
27
+ while (end > start && isOuterPadding(value.charAt(end - 1))) end--;
28
+ return start === 0 && end === value.length ? value : value.slice(start, end);
29
+ }
30
+
31
+ /** Drive paths parse as one-letter schemes (`c:`). Recognize them before that check. */
32
+ function isWindowsDrivePath(value: string): boolean {
33
+ return /^[A-Za-z]:[\\/]/.test(value);
34
+ }
35
+
36
+ const SCHEME_PREFIX = /^[A-Za-z][A-Za-z0-9+.-]*:/;
37
+
38
+ /** http(s) href, "reject" for any other URL, or undefined when `value` is not a URL. */
39
+ function classifyUrl(value: string): string | "reject" | undefined {
40
+ if (isWindowsDrivePath(value) || isWindowsDrivePath(value.trim())) return undefined;
41
+ try {
42
+ const parsed = new URL(value);
43
+ if (!ALLOWED_OPEN_PROTOCOLS.has(parsed.protocol) || hasAsciiControl(parsed.href)) return "reject";
44
+ return parsed.href;
45
+ } catch {
46
+ return undefined;
47
+ }
48
+ }
49
+
50
+ function resolveOpenTarget(raw: string): string | undefined {
51
+ if (hasAsciiControl(raw) || raw.length === 0) return undefined;
52
+ // Local paths, including surrounding spaces, are spawned unchanged.
53
+ if (isWindowsDrivePath(raw)) return raw;
54
+ const direct = classifyUrl(raw);
55
+ if (direct === "reject") return undefined;
56
+ if (direct !== undefined) return direct;
57
+
58
+ const peeled = peelPadding(raw);
59
+ if (peeled.length === 0) return undefined;
60
+ // Padding can hide a scheme or a flag. Refuse that instead of opening a different string.
61
+ if (peeled !== raw && !isWindowsDrivePath(peeled)) {
62
+ const hidden = classifyUrl(peeled);
63
+ if (hidden !== undefined || peeled.startsWith("-") || SCHEME_PREFIX.test(peeled)) return undefined;
64
+ }
65
+ if (raw.startsWith("-") || SCHEME_PREFIX.test(raw)) return undefined;
66
+ return raw;
67
+ }
68
+
2
69
  export function openPath(urlOrPath: string): void {
70
+ const target = resolveOpenTarget(urlOrPath);
71
+ if (target === undefined) return;
3
72
  let cmd: string[];
4
73
  switch (process.platform) {
5
74
  case "darwin":
6
- cmd = ["open", urlOrPath];
75
+ cmd = ["open", target];
7
76
  break;
8
77
  case "win32":
9
- cmd = ["rundll32", "url.dll,FileProtocolHandler", urlOrPath];
78
+ cmd = ["rundll32", "url.dll,FileProtocolHandler", target];
10
79
  break;
11
80
  default:
12
- cmd = ["xdg-open", urlOrPath];
81
+ cmd = ["xdg-open", target];
13
82
  break;
14
83
  }
15
84
  try {
@@ -4,10 +4,17 @@ import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import type { ImageContent } from "@gajae-code/ai/core";
6
6
  import { formatBytes, parseImageMetadata } from "@gajae-code/utils";
7
+ import {
8
+ MAX_IMAGE_INPUT_BYTES,
9
+ MAX_PASTED_IMAGE_DECODED_BYTES,
10
+ MAX_PASTED_IMAGE_DIMENSION,
11
+ MAX_PASTED_IMAGE_OUTPUT_BYTES,
12
+ MAX_PASTED_IMAGE_PIXELS,
13
+ MAX_PASTED_IMAGE_SOURCE_BYTES,
14
+ } from "./image-limits";
7
15
  import {
8
16
  ImageInputTooLargeError,
9
17
  type LoadedImageInput,
10
- MAX_IMAGE_INPUT_BYTES,
11
18
  materializeImageInput,
12
19
  type TransformedImageInput,
13
20
  transformImageInputBytes,
@@ -15,12 +22,6 @@ import {
15
22
  import { DEFAULT_IMAGE_RESIZE_MAX_BYTES } from "./image-resize";
16
23
  import { MAX_PASTED_IMAGE_COUNT } from "./pasted-image-path";
17
24
 
18
- export const MAX_PASTED_IMAGE_SOURCE_BYTES = 64 * 1024 * 1024;
19
- export const MAX_PASTED_IMAGE_OUTPUT_BYTES = 64 * 1024 * 1024;
20
- export const MAX_PASTED_IMAGE_DIMENSION = 20_000;
21
- export const MAX_PASTED_IMAGE_PIXELS = 40_000_000;
22
- export const MAX_PASTED_IMAGE_DECODED_BYTES = 160 * 1024 * 1024;
23
-
24
25
  export type PastedImageBatchErrorCode =
25
26
  | "too-many"
26
27
  | "unsafe-path"
@@ -319,7 +320,7 @@ function hasWebpAnimation(bytes: Buffer): boolean {
319
320
  return false;
320
321
  }
321
322
 
322
- function isAnimatedImage(bytes: Buffer, mimeType: string): boolean | undefined {
323
+ export function isAnimatedImage(bytes: Buffer, mimeType: string): boolean | undefined {
323
324
  if (mimeType === "image/gif") {
324
325
  const frames = gifFrameCount(bytes);
325
326
  return frames === undefined ? undefined : frames > 1;
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Predict the user's likely next prompt after an agent turn using a smol,
3
- * fast model. Native GJC port of the ghost-text prompt-suggestion behavior
2
+ * Predict the user's likely next prompt after an agent turn with the `default`
3
+ * model role. Native GJC port of the ghost-text prompt-suggestion behavior
4
4
  * from Claude Code: the prediction renders as dim ghost text in the empty
5
5
  * composer and Tab accepts it.
6
6
  */
@@ -169,8 +169,9 @@ function getSuggestionModel(
169
169
  registry: ModelRegistry,
170
170
  settings: Settings,
171
171
  currentModel?: Model<Api>,
172
+ credentialSessionId?: string,
172
173
  ): Model<Api> | undefined {
173
- const availableModels = registry.getAvailable();
174
+ const availableModels = registry.getAvailable(credentialSessionId);
174
175
  if (availableModels.length === 0) return undefined;
175
176
  const model = resolveRoleSelection(["default"], settings, availableModels, registry)?.model;
176
177
  if (model) return model;
@@ -203,7 +204,7 @@ export async function generatePromptSuggestion(
203
204
  const context = buildPromptSuggestionContext(messages);
204
205
  if (!context) return null;
205
206
 
206
- const model = getSuggestionModel(registry, settings, currentModel);
207
+ const model = getSuggestionModel(registry, settings, currentModel, sessionId);
207
208
  if (!model) {
208
209
  logger.debug("prompt-suggestion: no model available");
209
210
  return null;
@@ -31,6 +31,11 @@ function getShellConfigFile(shell: string, home: string): string {
31
31
  return path.join(home, ".profile");
32
32
  }
33
33
 
34
+ /** One POSIX shell word. Double quotes would still run `$()` and backticks. */
35
+ export function quotePosixShell(value: string): string {
36
+ return `'${value.replace(/'/g, `'\\''`)}'`;
37
+ }
38
+
34
39
  /**
35
40
  * Generate the snapshot creation script.
36
41
  * This script sources the user's rc file and extracts functions, aliases, and options.
@@ -43,6 +48,9 @@ function generateSnapshotScript(shell: string, snapshotPath: string, rcFile: str
43
48
 
44
49
  // Escape the snapshot path for shell
45
50
  const escapedPath = snapshotPath.replace(/'/g, "'\\''");
51
+ const rcSource = hasRcFile
52
+ ? `source ${quotePosixShell(rcFile)} < /dev/null 2>/dev/null`
53
+ : "# No user config file to source";
46
54
 
47
55
  // Function extraction differs between bash and zsh
48
56
  const functionScript = isZsh
@@ -83,7 +91,7 @@ umask 077
83
91
  SNAPSHOT_FILE='${escapedPath}'
84
92
 
85
93
  # Source user's rc file if it exists
86
- ${hasRcFile ? `source "${rcFile}" < /dev/null 2>/dev/null` : "# No user config file to source"}
94
+ ${rcSource}
87
95
 
88
96
  # Create/clear the snapshot file
89
97
  echo "# Shell snapshot - generated by gjc agent" >| "$SNAPSHOT_FILE"