@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
@@ -1,3 +1,4 @@
1
+ import { escapePromptMetadata } from "../../session/messages";
1
2
  import { ToolError } from "../tool-errors";
2
3
 
3
4
  export interface RunnerTask {
@@ -197,23 +198,33 @@ function findAmbiguityExample(runners: DetectedRunner[]): { runner: string; task
197
198
  return firstRunner && firstTask ? { runner: firstRunner.id, task: firstTask.name } : undefined;
198
199
  }
199
200
 
201
+ function escapeRecipePromptText(value: string | undefined): string | undefined {
202
+ return value === undefined ? undefined : escapePromptMetadata(value);
203
+ }
204
+
200
205
  export function buildPromptModel(runners: DetectedRunner[]): RecipePromptModel {
201
206
  const ambiguityExample = findAmbiguityExample(runners);
202
207
  return {
203
208
  hasMultipleRunners: runners.length > 1,
204
- ambiguityExampleRunner: ambiguityExample?.runner,
205
- ambiguityExampleTask: ambiguityExample?.task,
209
+ ambiguityExampleRunner: escapeRecipePromptText(ambiguityExample?.runner),
210
+ ambiguityExampleTask: escapeRecipePromptText(ambiguityExample?.task),
206
211
  runners: runners.map(runner => ({
207
- id: runner.id,
208
- label: runner.label,
209
- commandPrefix: runner.commandPrefix,
210
- tasks: runner.tasks.slice(0, PROMPT_TASK_LIMIT).map(task => ({
211
- name: task.name,
212
- paramSig: task.parameters.length > 0 ? task.parameters.join(" ") : undefined,
213
- command: buildCommand(task.commandPrefix ?? runner.commandPrefix, task.commandName ?? task.name, ""),
214
- doc: task.doc,
215
- cwd: task.cwd,
216
- })),
212
+ id: escapePromptMetadata(runner.id),
213
+ label: escapePromptMetadata(runner.label),
214
+ commandPrefix: escapePromptMetadata(runner.commandPrefix),
215
+ // recipe.md is compiled with noEscape. Cap the list first, then encode the
216
+ // kept repository fields so a script key or path cannot close <instruction> or <runner>.
217
+ tasks: runner.tasks.slice(0, PROMPT_TASK_LIMIT).map(task => {
218
+ const paramSig = task.parameters.length > 0 ? task.parameters.join(" ") : undefined;
219
+ const command = buildCommand(task.commandPrefix ?? runner.commandPrefix, task.commandName ?? task.name, "");
220
+ return {
221
+ name: escapePromptMetadata(task.name),
222
+ paramSig: escapeRecipePromptText(paramSig),
223
+ command: escapePromptMetadata(command),
224
+ doc: escapeRecipePromptText(task.doc),
225
+ cwd: escapeRecipePromptText(task.cwd),
226
+ };
227
+ }),
217
228
  })),
218
229
  };
219
230
  }
@@ -10,7 +10,7 @@ import * as path from "node:path";
10
10
  import type { ToolCallContext } from "@gajae-code/agent-core";
11
11
  import type { Component, Ellipsis } from "@gajae-code/tui";
12
12
  import { replaceTabs, truncateToWidth } from "@gajae-code/tui";
13
- import { pluralize } from "@gajae-code/utils";
13
+ import { pluralize, sanitizeText } from "@gajae-code/utils";
14
14
  import { settings } from "../config/settings";
15
15
  import type { Theme } from "../modes/theme/theme";
16
16
  import { Hasher } from "../tui/utils";
@@ -175,7 +175,7 @@ export function formatMeta(meta: string[], theme: Theme): string {
175
175
  }
176
176
 
177
177
  export function formatErrorMessage(message: string | undefined, theme: Theme): string {
178
- const clean = (message ?? "").replace(/^Error:\s*/, "").trim();
178
+ const clean = sanitizeText((message ?? "").replace(/^Error:\s*/, "").trim());
179
179
  return `${theme.styledSymbol("status.error", "error")} ${theme.fg("error", `Error: ${clean || "Unknown error"}`)}`;
180
180
  }
181
181
 
@@ -19,6 +19,7 @@ import {
19
19
  openArchive,
20
20
  parseArchivePathCandidates,
21
21
  } from "./archive-reader";
22
+ import { wrapUntrustedContent } from "./fetch";
22
23
  import { createFileRecorder, formatResultPath } from "./file-recorder";
23
24
  import { formatGroupedFiles } from "./grouped-file-output";
24
25
  import { formatMatchLine } from "./match-line-format";
@@ -552,7 +553,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
552
553
  missingPaths: missingPaths.length > 0 ? missingPaths : undefined,
553
554
  };
554
555
  const text = warningNote ? `No matches found\n${warningNote}` : "No matches found";
555
- const noMatchResult = toolResult(details).text(text).done();
556
+ const noMatchResult = toolResult(details).text(wrapUntrustedContent(text)).done();
556
557
  await cleanupArchiveScratch();
557
558
  throwIfSearchCancelled();
558
559
  return noMatchResult;
@@ -676,7 +677,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
676
677
  if (truncation.truncated) details.truncation = truncation;
677
678
  if (linesTruncated) details.linesTruncated = true;
678
679
  const resultBuilder = toolResult(details)
679
- .text(output)
680
+ .text(wrapUntrustedContent(output))
680
681
  .limits({ columnMax: linesTruncated ? DEFAULT_MAX_COLUMN : undefined });
681
682
  if (truncation.truncated) {
682
683
  resultBuilder.truncation(truncation, { direction: "head" });
package/src/tools/ssh.ts CHANGED
@@ -5,18 +5,35 @@ import { prompt } from "@gajae-code/utils";
5
5
  import * as z from "zod/v4";
6
6
  import type { SSHHost } from "../capability/ssh";
7
7
  import { sshCapability } from "../capability/ssh";
8
+ import { getDefault, type Settings } from "../config/settings";
8
9
  import { loadCapability } from "../discovery";
9
10
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
11
  import type { Theme } from "../modes/theme/theme";
11
12
  import sshDescriptionBase from "../prompts/tools/ssh.md" with { type: "text" };
12
- import { DEFAULT_MAX_BYTES, streamTailUpdates, TailBuffer } from "../session/streaming-output";
13
+ import type { SessionArtifactPublication } from "../session/session-manager";
14
+ import {
15
+ DEFAULT_MAX_BYTES,
16
+ streamTailUpdates,
17
+ TailBuffer,
18
+ type TerminalArtifactPublisher,
19
+ type TruncationResult,
20
+ truncateMiddle,
21
+ truncateMiddleWindows,
22
+ truncateTail,
23
+ } from "../session/streaming-output";
13
24
  import type { SSHHostInfo } from "../ssh/connection-manager";
14
25
  import { ensureHostInfo, getHostInfoForHost } from "../ssh/connection-manager";
15
- import { executeSSH } from "../ssh/ssh-executor";
26
+ import { executeSSH, type SSHResult } from "../ssh/ssh-executor";
16
27
  import { renderStatusLine } from "../tui";
17
28
  import { CachedOutputBlock } from "../tui/output-block";
18
29
  import type { ToolSession } from ".";
19
- import { formatStyledTruncationWarning, type OutputMeta, stripOutputNotice } from "./output-meta";
30
+ import { wrapUntrustedContent } from "./fetch";
31
+ import {
32
+ formatArtifactReference,
33
+ formatStyledTruncationWarning,
34
+ type OutputMeta,
35
+ stripOutputNotice,
36
+ } from "./output-meta";
20
37
  import { ToolError } from "./tool-errors";
21
38
  import { toolResult } from "./tool-result";
22
39
  import { clampTimeout } from "./tool-timeouts";
@@ -89,6 +106,181 @@ function quoteCmdPath(value: string): string {
89
106
  return `"${escaped}"`;
90
107
  }
91
108
 
109
+ const FENCE_OPEN = "<untrusted-content>\n";
110
+ const FENCE_CLOSE = "\n</untrusted-content>";
111
+ const UNTRUSTED_FENCE_OVERHEAD = Buffer.byteLength(`${FENCE_OPEN}${FENCE_CLOSE}`, "utf-8");
112
+ const INLINE_MARKER_RESERVE = 256;
113
+ /** `</untrusted-content>` grows when `wrapUntrustedContent` escapes it. */
114
+ const CLOSER_RAW_BYTES = "</untrusted-content>".length;
115
+ const CLOSER_ESCAPED_BYTES = "&lt;/untrusted-content&gt;".length;
116
+
117
+ type OutputBudgetPath = "tools.maxInlineResultBytes" | "tools.artifactHeadBytes" | "tools.artifactTailLines";
118
+
119
+ function settingNumber(settings: Settings | undefined, path: OutputBudgetPath): number {
120
+ return settings?.get(path) ?? getDefault(path);
121
+ }
122
+
123
+ type RemoteFrame = {
124
+ text: string;
125
+ truncation?: TruncationResult;
126
+ direction?: "head" | "tail" | "middle";
127
+ /** True only after the full remote text is already stored. */
128
+ shrunk: boolean;
129
+ visibleLines: number;
130
+ visibleBytes: number;
131
+ };
132
+
133
+ function inlineCapBytes(settings: Settings | undefined): number {
134
+ return settingNumber(settings, "tools.maxInlineResultBytes") * 1024;
135
+ }
136
+
137
+ function exceedsInlineCap(output: string, settings: Settings | undefined): boolean {
138
+ const cap = inlineCapBytes(settings);
139
+ if (cap <= 0) return false;
140
+ // Measure the escaped frame. A raw-size check misses closer expansion.
141
+ return Buffer.byteLength(wrapUntrustedContent(output), "utf-8") > cap;
142
+ }
143
+
144
+ function frameFullRemoteOutput(output: string): RemoteFrame {
145
+ return { text: wrapUntrustedContent(output), shrunk: false, visibleLines: 0, visibleBytes: 0 };
146
+ }
147
+
148
+ /**
149
+ * Cut to the inline window, then frame that excerpt. Call only when the full
150
+ * remote text is already stored, so a tail cut cannot drop the opening fence
151
+ * or the only copy of the output.
152
+ */
153
+ function frameShrunkRemoteOutput(output: string, settings: Settings | undefined): RemoteFrame {
154
+ const cap = inlineCapBytes(settings);
155
+ const escapedRoom = Math.max(1, cap - UNTRUSTED_FENCE_OVERHEAD - INLINE_MARKER_RESERVE);
156
+ // Budget in remote-output bytes so framing and closer escaping cannot distort
157
+ // the source coordinates reported by truncation metadata.
158
+ let budget = Math.max(1, Math.floor((escapedRoom * CLOSER_RAW_BYTES) / CLOSER_ESCAPED_BYTES));
159
+ const headBytes = settingNumber(settings, "tools.artifactHeadBytes") * 1024;
160
+ const tailLines = settingNumber(settings, "tools.artifactTailLines");
161
+ const headBudget = Math.max(
162
+ 1,
163
+ Math.floor((Math.min(headBytes, escapedRoom / 2) * CLOSER_RAW_BYTES) / CLOSER_ESCAPED_BYTES),
164
+ );
165
+ const cutRemoteOutput = (maxBytes: number) => {
166
+ if (headBytes > 0) {
167
+ const options = {
168
+ maxBytes,
169
+ maxLines: tailLines * 2,
170
+ maxHeadBytes: Math.min(headBudget, Math.floor(maxBytes / 2)),
171
+ maxHeadLines: tailLines,
172
+ };
173
+ return {
174
+ truncation: truncateMiddle(output, options),
175
+ windows: truncateMiddleWindows(output, options),
176
+ };
177
+ }
178
+ const options = { maxBytes, maxLines: tailLines };
179
+ return {
180
+ truncation: truncateTail(output, options),
181
+ windows: truncateMiddleWindows(output, { ...options, maxHeadBytes: 0, maxHeadLines: 0 }),
182
+ };
183
+ };
184
+ let { truncation, windows } = cutRemoteOutput(budget);
185
+ let excerpt = truncation.truncated ? truncation.content : output;
186
+ let text = wrapUntrustedContent(excerpt);
187
+ let textBytes = Buffer.byteLength(text, "utf-8");
188
+ while (textBytes > cap && budget > 1) {
189
+ const scaledBudget = Math.floor((budget * cap) / textBytes);
190
+ budget = Math.max(1, Math.min(budget - 1, scaledBudget));
191
+ ({ truncation, windows } = cutRemoteOutput(budget));
192
+ excerpt = truncation.truncated ? truncation.content : output;
193
+ text = wrapUntrustedContent(excerpt);
194
+ textBytes = Buffer.byteLength(text, "utf-8");
195
+ }
196
+ if (text === wrapUntrustedContent(output)) return frameFullRemoteOutput(output);
197
+ if (windows.kind === "full") return frameFullRemoteOutput(output);
198
+ const direction = windows.kind === "middle" ? "middle" : windows.kind === "head-only" ? "head" : "tail";
199
+ return {
200
+ text,
201
+ truncation,
202
+ direction,
203
+ shrunk: true,
204
+ visibleLines: (windows.head?.lines ?? 0) + (windows.tail?.lines ?? 0),
205
+ visibleBytes: (windows.head?.bytes ?? 0) + (windows.tail?.bytes ?? 0),
206
+ };
207
+ }
208
+
209
+ /** Store the full remote text. Never write an already-shortened excerpt.
210
+ * The command has already finished, so a rejected reservation must not discard it.
211
+ */
212
+ async function storeFullRemoteOutput(
213
+ session: ToolSession,
214
+ output: string,
215
+ artifactPublication: SessionArtifactPublication | undefined,
216
+ ): Promise<string | undefined> {
217
+ let allocated: { id?: string; path?: string } | undefined;
218
+ try {
219
+ allocated = await session.allocateOutputArtifact?.("ssh");
220
+ } catch {
221
+ allocated = undefined;
222
+ }
223
+ if (allocated?.path && allocated.id) {
224
+ try {
225
+ await Bun.write(allocated.path, output);
226
+ return allocated.id;
227
+ } catch {
228
+ // Fall through to the session-owned publisher when a writable path failed.
229
+ }
230
+ }
231
+ if (!artifactPublication) return undefined;
232
+ try {
233
+ return await artifactPublication(output, "ssh");
234
+ } catch {
235
+ return undefined;
236
+ }
237
+ }
238
+
239
+ function createSshArtifactPublisher(
240
+ artifactPublication: SessionArtifactPublication | undefined,
241
+ ): TerminalArtifactPublisher | undefined {
242
+ if (!artifactPublication) return undefined;
243
+ return async (content, info) => {
244
+ try {
245
+ const artifactId = await artifactPublication(content, "ssh");
246
+ if (!artifactId) return { status: "unavailable" };
247
+ return info.omittedBytes > 0
248
+ ? { status: "published", artifactId, omittedBytes: info.omittedBytes }
249
+ : { status: "published", artifactId };
250
+ } catch (error) {
251
+ return { status: "failed", diagnostic: error instanceof Error ? error.message : String(error) };
252
+ }
253
+ };
254
+ }
255
+
256
+ function hasCompleteRemoteOutput(result: SSHResult, output: string): boolean {
257
+ return (
258
+ !result.truncated &&
259
+ result.outputBytes === result.totalBytes &&
260
+ Buffer.byteLength(output, "utf-8") === result.outputBytes
261
+ );
262
+ }
263
+
264
+ function incompleteOutputNotice(result: SSHResult, artifactId: string | undefined): string | undefined {
265
+ const retainedWindowIncomplete = result.truncated || result.outputBytes < result.totalBytes;
266
+ if (!retainedWindowIncomplete) return undefined;
267
+
268
+ const artifactIncomplete =
269
+ (result.artifactTruncatedBytes ?? 0) > 0 || result.artifactFailureDiagnostic !== undefined;
270
+ const completeArtifactAvailable = artifactId !== undefined && !artifactIncomplete;
271
+ if (completeArtifactAvailable) return undefined;
272
+
273
+ const notes = [`SSH output is incomplete: retained ${result.outputBytes} of ${result.totalBytes} bytes`];
274
+ notes.push("no complete output artifact is available");
275
+ if ((result.artifactTruncatedBytes ?? 0) > 0) {
276
+ notes.push(`artifact storage omitted at least ${result.artifactTruncatedBytes} bytes`);
277
+ }
278
+ if (result.artifactFailureDiagnostic) {
279
+ notes.push(`Artifact storage failed: ${result.artifactFailureDiagnostic}`);
280
+ }
281
+ return notes.join("; ");
282
+ }
283
+
92
284
  function buildRemoteCommand(command: string, cwd: string | undefined, info: SSHHostInfo): string {
93
285
  if (!cwd) return command;
94
286
 
@@ -161,6 +353,8 @@ export class SshTool implements AgentTool<typeof sshSchema, SSHToolDetails> {
161
353
  // Clamp to reasonable range: 1s - 3600s (1 hour)
162
354
  const timeoutSec = clampTimeout("ssh", rawTimeout);
163
355
  const timeoutMs = timeoutSec * 1000;
356
+ const outputSettings = _ctx?.settings ?? this.session.settings;
357
+ const artifactPublication = this.session.captureArtifactPublication?.();
164
358
 
165
359
  const tailBuffer = new TailBuffer(DEFAULT_MAX_BYTES);
166
360
  const { path: artifactPath, id: artifactId } = (await this.session.allocateOutputArtifact?.("ssh")) ?? {};
@@ -172,22 +366,94 @@ export class SshTool implements AgentTool<typeof sshSchema, SSHToolDetails> {
172
366
  compatEnabled: hostInfo.compatEnabled,
173
367
  artifactPath,
174
368
  artifactId,
369
+ artifactPublisher: createSshArtifactPublisher(artifactPublication),
175
370
  onChunk: streamTailUpdates(tailBuffer, onUpdate),
176
371
  });
177
372
 
373
+ const remote = result.output ?? "";
374
+ let storedArtifactId = result.artifactId;
375
+ let framed: RemoteFrame | undefined;
376
+ if (!remote) {
377
+ framed = undefined;
378
+ } else if (!exceedsInlineCap(remote, outputSettings)) {
379
+ framed = frameFullRemoteOutput(remote);
380
+ } else {
381
+ if (!storedArtifactId && hasCompleteRemoteOutput(result, remote)) {
382
+ storedArtifactId = await storeFullRemoteOutput(this.session, remote, artifactPublication);
383
+ }
384
+ framed = storedArtifactId ? frameShrunkRemoteOutput(remote, outputSettings) : frameFullRemoteOutput(remote);
385
+ }
386
+ // The artifact id stays in truncation metadata. A line after the closing
387
+ // fence makes the inline backstop drop the opening fence.
388
+ const modelText = framed?.text;
389
+ const storedNote =
390
+ (framed?.shrunk || result.truncated) && storedArtifactId
391
+ ? `\n\n${formatArtifactReference(storedArtifactId, result.artifactTruncatedBytes)}`
392
+ : "";
393
+ const incompleteNote = incompleteOutputNotice(result, storedArtifactId);
394
+ const artifactFailureNote =
395
+ result.artifactFailureDiagnostic && !incompleteNote
396
+ ? `SSH streaming artifact storage failed: ${result.artifactFailureDiagnostic}`
397
+ : undefined;
178
398
  if (result.cancelled) {
179
- throw new ToolError(result.output || "Command aborted");
399
+ const message = [
400
+ modelText || "Command aborted",
401
+ storedNote.trim() || undefined,
402
+ incompleteNote,
403
+ artifactFailureNote,
404
+ ]
405
+ .filter((part): part is string => part !== undefined)
406
+ .join("\n\n");
407
+ throw new ToolError(message);
180
408
  }
181
409
 
182
- const outputText = result.output || "(no output)";
410
+ const outputText = modelText ?? "(no output)";
183
411
  const details: SSHToolDetails = {};
184
- const resultBuilder = toolResult(details).text(outputText).truncationFromSummary(result, { direction: "tail" });
412
+ const resultBuilder = toolResult(details).text(outputText);
413
+ if (framed?.shrunk && framed.truncation && storedArtifactId) {
414
+ resultBuilder.truncation(framed.truncation, {
415
+ direction: framed.direction ?? "tail",
416
+ artifactId: storedArtifactId,
417
+ });
418
+ } else {
419
+ resultBuilder.truncationFromSummary(result, { direction: "tail" });
420
+ }
185
421
 
186
422
  if (result.exitCode !== 0 && result.exitCode !== undefined) {
187
- throw new ToolError(`${outputText}\n\nCommand exited with code ${result.exitCode}`);
423
+ const message = [
424
+ outputText,
425
+ storedNote.trim() || undefined,
426
+ incompleteNote,
427
+ artifactFailureNote,
428
+ `Command exited with code ${result.exitCode}`,
429
+ ]
430
+ .filter((part): part is string => part !== undefined)
431
+ .join("\n\n");
432
+ throw new ToolError(message);
188
433
  }
189
434
 
190
- return resultBuilder.done();
435
+ const built = resultBuilder.done();
436
+ const truncation = built.details?.meta?.truncation;
437
+ if (framed?.shrunk && truncation) {
438
+ // The cut is a window of an earlier excerpt. Keep the stream totals
439
+ // and do not present that window's coordinates as the original range.
440
+ truncation.totalLines = result.totalLines;
441
+ truncation.totalBytes = result.totalBytes;
442
+ truncation.outputLines = framed.visibleLines;
443
+ truncation.outputBytes = framed.visibleBytes;
444
+ truncation.artifactId = storedArtifactId;
445
+ truncation.artifactTruncatedBytes = result.artifactTruncatedBytes;
446
+ truncation.artifactFailureDiagnostic = result.artifactFailureDiagnostic;
447
+ delete truncation.shownRange;
448
+ delete truncation.headRange;
449
+ delete truncation.tailRange;
450
+ delete truncation.nextOffset;
451
+ if (framed.direction === "middle") {
452
+ truncation.elidedLines = Math.max(0, result.totalLines - framed.visibleLines);
453
+ truncation.elidedBytes = Math.max(0, result.totalBytes - framed.visibleBytes);
454
+ }
455
+ }
456
+ return built;
191
457
  }
192
458
  }
193
459
 
@@ -6,6 +6,7 @@ import { type AsyncJob, AsyncJobManager, jobElapsedMs, type SubagentRecord } fro
6
6
  import subagentDescription from "../prompts/tools/subagent.md" with { type: "text" };
7
7
  import type { AgentProgress, AgentSource, LocalErrorSummary } from "../task/types";
8
8
  import { Ellipsis, truncateToWidth } from "../tui";
9
+ import { wrapUntrustedContent } from "./fetch";
9
10
  import type { ToolSession } from "./index";
10
11
  import { replaceTabs } from "./render-utils";
11
12
  import { ToolError } from "./tool-errors";
@@ -799,7 +800,11 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
799
800
  }
800
801
  if (snapshot.description) lines.push(`Description: ${snapshot.description}`);
801
802
  if (snapshot.outputRef) lines.push(`Output: ${snapshot.outputRef}`);
802
- if (snapshot.setupFailureSummary) lines.push(`Setup failure: ${snapshot.setupFailureSummary}`);
803
+ // A markdown fence is not a boundary: subagent text can contain ``` and
804
+ // close it, leaving the rest as raw tool-result text. Frame the data.
805
+ if (snapshot.setupFailureSummary) {
806
+ lines.push(wrapUntrustedContent(`Setup failure: ${snapshot.setupFailureSummary}`));
807
+ }
803
808
  if (snapshot.localErrorSummary) {
804
809
  const local = snapshot.localErrorSummary;
805
810
  lines.push(`Local failure (${local.kind}): ${local.summary}`);
@@ -816,13 +821,14 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
816
821
  );
817
822
  }
818
823
  }
819
- if (snapshot.assignment) lines.push("Assignment:", "```", snapshot.assignment, "```");
824
+ if (snapshot.assignment) lines.push(wrapUntrustedContent(`Assignment:\n${snapshot.assignment}`));
820
825
  if (snapshot.steerMessage) {
821
- lines.push(`Steer (${snapshot.steerState ?? "queued"}):`, "```", snapshot.steerMessage, "```");
826
+ lines.push(wrapUntrustedContent(`Steer (${snapshot.steerState ?? "queued"}):\n${snapshot.steerMessage}`));
822
827
  lines.push(STEER_QUEUED_GUIDANCE);
823
828
  }
824
829
  if (snapshot.resultPreview) {
825
- lines.push(snapshot.errorText ? "Error preview:" : "Result preview:", "```", snapshot.resultPreview, "```");
830
+ const previewLabel = snapshot.errorText ? "Error preview:" : "Result preview:";
831
+ lines.push(wrapUntrustedContent(`${previewLabel}\n${snapshot.resultPreview}`));
826
832
  if (snapshot.truncated)
827
833
  lines.push("Preview truncated; use the output ref or explicit ids with `verbosity=full` for more.");
828
834
  }