@oh-my-pi/pi-coding-agent 16.1.22 → 16.2.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 (276) hide show
  1. package/CHANGELOG.md +99 -1
  2. package/dist/cli.js +4387 -4164
  3. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +1 -0
  4. package/dist/types/advisor/emission-guard.d.ts +73 -0
  5. package/dist/types/advisor/index.d.ts +1 -0
  6. package/dist/types/advisor/runtime.d.ts +7 -0
  7. package/dist/types/advisor/watchdog.d.ts +2 -0
  8. package/dist/types/cli/flag-tables.d.ts +1 -0
  9. package/dist/types/cli/gallery-fixtures/search.d.ts +1 -1
  10. package/dist/types/cli/gc-cli.d.ts +58 -0
  11. package/dist/types/collab/display-name.d.ts +3 -0
  12. package/dist/types/collab/host.d.ts +0 -2
  13. package/dist/types/commands/gc.d.ts +37 -0
  14. package/dist/types/commands/worktree.d.ts +0 -3
  15. package/dist/types/config/inline-tool-descriptors-mode.d.ts +13 -0
  16. package/dist/types/config/model-discovery.d.ts +6 -1
  17. package/dist/types/config/model-registry.d.ts +6 -2
  18. package/dist/types/config/model-resolver.d.ts +12 -0
  19. package/dist/types/config/models-config-schema.d.ts +29 -2
  20. package/dist/types/config/models-config.d.ts +22 -1
  21. package/dist/types/config/settings-schema.d.ts +145 -21
  22. package/dist/types/config/settings.d.ts +13 -0
  23. package/dist/types/dap/config.d.ts +1 -1
  24. package/dist/types/edit/hashline/filesystem.d.ts +4 -2
  25. package/dist/types/edit/renderer.d.ts +4 -0
  26. package/dist/types/extensibility/extensions/types.d.ts +17 -17
  27. package/dist/types/extensibility/hooks/types.d.ts +10 -10
  28. package/dist/types/extensibility/plugins/marketplace/types.d.ts +1 -0
  29. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +1 -0
  30. package/dist/types/internal-urls/index.d.ts +1 -0
  31. package/dist/types/internal-urls/registry-helpers.d.ts +2 -0
  32. package/dist/types/internal-urls/router.d.ts +1 -1
  33. package/dist/types/internal-urls/ssh-protocol.d.ts +10 -0
  34. package/dist/types/internal-urls/types.d.ts +19 -2
  35. package/dist/types/irc/bus.d.ts +6 -0
  36. package/dist/types/mcp/transports/stdio.d.ts +25 -1
  37. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +1 -0
  38. package/dist/types/modes/components/custom-editor.d.ts +7 -0
  39. package/dist/types/modes/components/move-overlay.d.ts +23 -0
  40. package/dist/types/modes/components/plugin-selector.d.ts +2 -1
  41. package/dist/types/modes/components/queue-mode-selector.d.ts +2 -1
  42. package/dist/types/modes/components/select-list-mouse-routing.d.ts +2 -0
  43. package/dist/types/modes/components/settings-defs.d.ts +4 -1
  44. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  45. package/dist/types/modes/components/show-images-selector.d.ts +2 -1
  46. package/dist/types/modes/components/status-line/component.d.ts +2 -0
  47. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  48. package/dist/types/modes/components/theme-selector.d.ts +2 -1
  49. package/dist/types/modes/components/thinking-selector.d.ts +2 -1
  50. package/dist/types/modes/controllers/command-controller.d.ts +10 -1
  51. package/dist/types/modes/controllers/streaming-reveal.d.ts +6 -0
  52. package/dist/types/modes/interactive-mode.d.ts +10 -1
  53. package/dist/types/modes/internal-url-autocomplete.d.ts +1 -1
  54. package/dist/types/modes/running-subagent-badge.d.ts +6 -0
  55. package/dist/types/modes/theme/mermaid-rendering.test.d.ts +1 -0
  56. package/dist/types/modes/theme/theme.d.ts +2 -1
  57. package/dist/types/modes/types.d.ts +9 -1
  58. package/dist/types/sdk.d.ts +2 -2
  59. package/dist/types/session/agent-session.d.ts +2 -1
  60. package/dist/types/session/session-listing.d.ts +10 -1
  61. package/dist/types/session/session-manager.d.ts +13 -0
  62. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  63. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +1 -0
  64. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +1 -0
  65. package/dist/types/ssh/connection-manager.d.ts +2 -0
  66. package/dist/types/ssh/file-transfer.d.ts +79 -0
  67. package/dist/types/ssh/utils.d.ts +6 -0
  68. package/dist/types/system-prompt.d.ts +5 -0
  69. package/dist/types/task/executor.d.ts +16 -0
  70. package/dist/types/tiny/text.d.ts +1 -1
  71. package/dist/types/tools/builtin-names.d.ts +5 -1
  72. package/dist/types/tools/{find.d.ts → glob.d.ts} +15 -15
  73. package/dist/types/tools/{search.d.ts → grep.d.ts} +14 -14
  74. package/dist/types/tools/index.d.ts +3 -3
  75. package/dist/types/tools/path-utils.d.ts +29 -0
  76. package/dist/types/tools/plan-mode-guard.d.ts +7 -0
  77. package/dist/types/tools/read.d.ts +2 -2
  78. package/dist/types/tools/render-utils.d.ts +8 -0
  79. package/dist/types/tools/renderers.d.ts +11 -0
  80. package/dist/types/tools/ssh.d.ts +2 -0
  81. package/dist/types/tools/todo.d.ts +0 -16
  82. package/dist/types/tools/write.d.ts +2 -2
  83. package/dist/types/utils/active-repo-context.d.ts +8 -0
  84. package/dist/types/utils/image-resize.d.ts +1 -0
  85. package/dist/types/utils/markit-cache.d.ts +23 -0
  86. package/dist/types/utils/markit.d.ts +5 -1
  87. package/dist/types/utils/prompt-path.d.ts +1 -0
  88. package/dist/types/web/search/providers/duckduckgo.d.ts +14 -0
  89. package/dist/types/web/search/providers/firecrawl.d.ts +28 -0
  90. package/dist/types/web/search/providers/tinyfish.d.ts +29 -0
  91. package/dist/types/web/search/providers/xai.d.ts +13 -0
  92. package/dist/types/web/search/types.d.ts +18 -2
  93. package/package.json +30 -15
  94. package/scripts/bench-guard.ts +1 -1
  95. package/scripts/build-binary.ts +9 -9
  96. package/scripts/bundle-dist.ts +2 -2
  97. package/src/advisor/__tests__/advisor.test.ts +40 -4
  98. package/src/advisor/__tests__/emission-guard.test.ts +147 -0
  99. package/src/advisor/advise-tool.ts +1 -1
  100. package/src/advisor/emission-guard.ts +172 -0
  101. package/src/advisor/index.ts +1 -0
  102. package/src/advisor/runtime.ts +11 -0
  103. package/src/advisor/watchdog.ts +12 -1
  104. package/src/cli/args.ts +5 -2
  105. package/src/cli/auth-broker-cli.ts +17 -0
  106. package/src/cli/config-cli.ts +4 -0
  107. package/src/cli/flag-tables.ts +7 -4
  108. package/src/cli/gallery-cli.ts +14 -2
  109. package/src/cli/gallery-fixtures/edit.ts +60 -0
  110. package/src/cli/gallery-fixtures/fs.ts +17 -17
  111. package/src/cli/gallery-fixtures/search.ts +4 -4
  112. package/src/cli/gc-cli.ts +939 -0
  113. package/src/cli/usage-cli.ts +20 -1
  114. package/src/cli/web-search-cli.ts +1 -1
  115. package/src/cli-commands.ts +1 -0
  116. package/src/collab/display-name.ts +13 -0
  117. package/src/collab/guest.ts +5 -1
  118. package/src/collab/host.ts +2 -13
  119. package/src/commands/gc.ts +46 -0
  120. package/src/commands/worktree.ts +6 -0
  121. package/src/config/inline-tool-descriptors-mode.ts +27 -0
  122. package/src/config/model-discovery.ts +77 -8
  123. package/src/config/model-registry.ts +89 -11
  124. package/src/config/model-resolver.ts +39 -1
  125. package/src/config/models-config-schema.ts +41 -5
  126. package/src/config/models-config.ts +4 -1
  127. package/src/config/settings-schema.ts +130 -27
  128. package/src/config/settings.ts +216 -7
  129. package/src/cursor.ts +1 -1
  130. package/src/dap/config.ts +152 -8
  131. package/src/dap/session.ts +1 -1
  132. package/src/discovery/helpers.ts +3 -1
  133. package/src/edit/hashline/diff.ts +14 -3
  134. package/src/edit/hashline/execute.ts +42 -6
  135. package/src/edit/hashline/filesystem.ts +49 -8
  136. package/src/edit/index.ts +1 -0
  137. package/src/edit/modes/patch.ts +11 -1
  138. package/src/edit/renderer.ts +140 -13
  139. package/src/eval/__tests__/agent-bridge.test.ts +101 -0
  140. package/src/eval/__tests__/julia-prelude.test.ts +18 -0
  141. package/src/eval/agent-bridge.ts +26 -3
  142. package/src/eval/jl/runner.jl +7 -1
  143. package/src/eval/js/tool-bridge.ts +2 -2
  144. package/src/export/html/index.ts +1 -1
  145. package/src/export/html/template.js +3 -1
  146. package/src/export/html/tool-views.generated.js +20 -20
  147. package/src/extensibility/extensions/types.ts +22 -22
  148. package/src/extensibility/hooks/types.ts +11 -11
  149. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +10 -3
  150. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +31 -10
  151. package/src/extensibility/plugins/marketplace/manager.ts +22 -0
  152. package/src/extensibility/plugins/marketplace/types.ts +1 -0
  153. package/src/internal-urls/__tests__/ssh-protocol.test.ts +331 -0
  154. package/src/internal-urls/docs-index.generated.txt +2 -2
  155. package/src/internal-urls/docs-index.ts +2 -3
  156. package/src/internal-urls/index.ts +1 -0
  157. package/src/internal-urls/registry-helpers.ts +19 -4
  158. package/src/internal-urls/router.ts +5 -3
  159. package/src/internal-urls/ssh-protocol.ts +367 -0
  160. package/src/internal-urls/types.ts +19 -2
  161. package/src/irc/bus.ts +11 -3
  162. package/src/lsp/index.ts +8 -1
  163. package/src/mcp/oauth-discovery.ts +20 -20
  164. package/src/mcp/tool-bridge.ts +32 -2
  165. package/src/mcp/transports/stdio.test.ts +20 -3
  166. package/src/mcp/transports/stdio.ts +45 -14
  167. package/src/memories/index.ts +1 -1
  168. package/src/modes/acp/acp-event-mapper.ts +2 -2
  169. package/src/modes/components/__tests__/move-overlay.test.ts +166 -0
  170. package/src/modes/components/agent-hub.ts +3 -0
  171. package/src/modes/components/agent-transcript-viewer.ts +9 -7
  172. package/src/modes/components/custom-editor.ts +90 -5
  173. package/src/modes/components/move-overlay.ts +282 -0
  174. package/src/modes/components/plan-review-overlay.ts +35 -35
  175. package/src/modes/components/plugin-selector.ts +6 -1
  176. package/src/modes/components/queue-mode-selector.ts +6 -1
  177. package/src/modes/components/select-list-mouse-routing.ts +35 -0
  178. package/src/modes/components/session-selector.ts +11 -10
  179. package/src/modes/components/settings-defs.ts +14 -1
  180. package/src/modes/components/settings-selector.ts +196 -29
  181. package/src/modes/components/show-images-selector.ts +6 -1
  182. package/src/modes/components/status-line/component.ts +108 -28
  183. package/src/modes/components/status-line/segments.ts +5 -1
  184. package/src/modes/components/status-line/types.ts +2 -0
  185. package/src/modes/components/theme-selector.ts +6 -1
  186. package/src/modes/components/thinking-selector.ts +6 -1
  187. package/src/modes/components/tool-execution.ts +25 -9
  188. package/src/modes/components/tree-selector.ts +5 -5
  189. package/src/modes/controllers/command-controller.ts +140 -47
  190. package/src/modes/controllers/event-controller.ts +59 -3
  191. package/src/modes/controllers/input-controller.ts +70 -4
  192. package/src/modes/controllers/selector-controller.ts +15 -2
  193. package/src/modes/controllers/streaming-reveal.ts +17 -0
  194. package/src/modes/controllers/tool-args-reveal.ts +1 -1
  195. package/src/modes/interactive-mode.ts +142 -59
  196. package/src/modes/internal-url-autocomplete.ts +17 -2
  197. package/src/modes/prompt-action-autocomplete.ts +3 -1
  198. package/src/modes/running-subagent-badge.ts +13 -0
  199. package/src/modes/setup-wizard/scenes/glyph.ts +2 -13
  200. package/src/modes/setup-wizard/scenes/providers.ts +2 -1
  201. package/src/modes/setup-wizard/scenes/theme.ts +6 -12
  202. package/src/modes/setup-wizard/scenes/web-search.ts +8 -13
  203. package/src/modes/setup-wizard/wizard-overlay.ts +6 -5
  204. package/src/modes/theme/mermaid-rendering.test.ts +53 -0
  205. package/src/modes/theme/theme.ts +42 -15
  206. package/src/modes/types.ts +9 -1
  207. package/src/modes/utils/interactive-context-helpers.ts +1 -1
  208. package/src/priority.json +15 -0
  209. package/src/prompts/advisor/active-repo-watchdog.md +6 -0
  210. package/src/prompts/advisor/system.md +21 -7
  211. package/src/prompts/agents/designer.md +1 -1
  212. package/src/prompts/agents/explore.md +1 -1
  213. package/src/prompts/agents/librarian.md +2 -2
  214. package/src/prompts/agents/plan.md +2 -2
  215. package/src/prompts/agents/reviewer.md +1 -1
  216. package/src/prompts/agents/task.md +2 -2
  217. package/src/prompts/system/active-repo-context.md +4 -0
  218. package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
  219. package/src/prompts/system/plan-mode-active.md +12 -3
  220. package/src/prompts/system/project-prompt.md +2 -2
  221. package/src/prompts/system/system-prompt.md +8 -6
  222. package/src/prompts/tools/bash.md +2 -2
  223. package/src/prompts/tools/checkpoint.md +1 -1
  224. package/src/prompts/tools/{find.md → glob.md} +1 -1
  225. package/src/prompts/tools/{search.md → grep.md} +3 -3
  226. package/src/prompts/tools/read.md +4 -2
  227. package/src/sdk.ts +98 -29
  228. package/src/session/agent-session.ts +828 -191
  229. package/src/session/session-history-format.ts +2 -2
  230. package/src/session/session-listing.ts +35 -2
  231. package/src/session/session-manager.ts +46 -0
  232. package/src/slash-commands/builtin-registry.ts +158 -22
  233. package/src/slash-commands/helpers/usage-report.ts +23 -2
  234. package/src/ssh/__tests__/connection-manager-args.test.ts +69 -0
  235. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +68 -0
  236. package/src/ssh/connection-manager.ts +12 -15
  237. package/src/ssh/file-transfer.ts +209 -0
  238. package/src/ssh/utils.ts +24 -0
  239. package/src/system-prompt.ts +60 -28
  240. package/src/task/executor.ts +96 -36
  241. package/src/task/index.ts +3 -3
  242. package/src/task/render.ts +14 -13
  243. package/src/task/worktree.ts +38 -1
  244. package/src/tiny/text.ts +49 -4
  245. package/src/tiny/worker.ts +3 -3
  246. package/src/tools/acp-bridge.ts +6 -1
  247. package/src/tools/bash.ts +2 -2
  248. package/src/tools/builtin-names.ts +26 -2
  249. package/src/tools/{find.ts → glob.ts} +48 -42
  250. package/src/tools/{search.ts → grep.ts} +311 -129
  251. package/src/tools/index.ts +13 -14
  252. package/src/tools/irc.ts +6 -2
  253. package/src/tools/path-utils.ts +64 -1
  254. package/src/tools/plan-mode-guard.ts +26 -13
  255. package/src/tools/read.ts +35 -6
  256. package/src/tools/render-utils.ts +14 -0
  257. package/src/tools/renderers.ts +15 -4
  258. package/src/tools/ssh.ts +17 -2
  259. package/src/tools/todo.ts +4 -26
  260. package/src/tools/write.ts +25 -9
  261. package/src/utils/active-repo-context.ts +143 -0
  262. package/src/utils/edit-mode.ts +19 -2
  263. package/src/utils/image-resize.ts +88 -7
  264. package/src/utils/lang-from-path.ts +3 -3
  265. package/src/utils/markit-cache.ts +166 -0
  266. package/src/utils/markit.ts +86 -18
  267. package/src/utils/prompt-path.ts +3 -0
  268. package/src/utils/shell-snapshot.ts +1 -1
  269. package/src/utils/title-generator.ts +1 -1
  270. package/src/web/search/provider.ts +54 -34
  271. package/src/web/search/providers/duckduckgo.ts +140 -0
  272. package/src/web/search/providers/firecrawl.ts +144 -0
  273. package/src/web/search/providers/tinyfish.ts +159 -0
  274. package/src/web/search/providers/xai.ts +292 -0
  275. package/src/web/search/providers/zai.ts +142 -56
  276. package/src/web/search/types.ts +6 -2
@@ -49,8 +49,8 @@ async function main(): Promise<void> {
49
49
  // Generate inside the try so the finally always restores the empty checked-in
50
50
  // placeholders (stats client archive, docs index) even on failure.
51
51
  try {
52
- await runCommand(["bun", "--cwd=../stats", "scripts/generate-client-bundle.ts", "--generate"]);
53
- await runCommand(["bun", "scripts/generate-docs-index.ts", "--generate"]);
52
+ await runCommand(["bun", "--cwd=../stats", "run", "gen:stats"]);
53
+ await runCommand(["bun", "run", "gen:docs"]);
54
54
  // `legacy-pi-bundled-registry.ts` static-imports
55
55
  // `@oh-my-pi/pi-coding-agent/export/html` (one of pi-coding-agent's
56
56
  // named subpath exports, see scripts/generate-legacy-pi-bundled-registry.ts),
@@ -61,14 +61,14 @@ async function main(): Promise<void> {
61
61
  // resolving the missing generated bundle. Rebuilding the tool views
62
62
  // here makes the compile self-contained and matches what `prepack`
63
63
  // does for the npm bundle.
64
- await runCommand(["bun", "--cwd=../collab-web", "run", "build:tool-views"]);
64
+ await runCommand(["bun", "--cwd=../collab-web", "run", "gen:tool-views"]);
65
65
  await runCommand(
66
- ["bun", "--cwd=../natives", "run", "embed:native"],
66
+ ["bun", "--cwd=../natives", "run", "gen:native"],
67
67
  crossTarget
68
68
  ? { ...Bun.env, TARGET_PLATFORM: crossPlatform as string, TARGET_ARCH: crossArch as string }
69
69
  : Bun.env,
70
70
  );
71
- await runCommand(["bun", "scripts/embed-mupdf-wasm.ts", "--generate"]);
71
+ await runCommand(["bun", "run", "gen:mupdf"]);
72
72
  // Regenerate the bundled-pi registry + key set before the compile so any
73
73
  // new pi-* subpath export added under `packages/*/package.json` is served
74
74
  // from the host's in-process copy. Without this, `bun build --compile`
@@ -123,12 +123,12 @@ async function main(): Promise<void> {
123
123
  await runCommand(["codesign", "--force", "--sign", "-", outputPath]);
124
124
  }
125
125
  } finally {
126
- await runCommand(["bun", "scripts/embed-mupdf-wasm.ts", "--reset"]);
127
- await runCommand(["bun", "--cwd=../natives", "run", "embed:native", "--reset"]);
126
+ await runCommand(["bun", "run", "gen:mupdf:reset"]);
127
+ await runCommand(["bun", "--cwd=../natives", "run", "gen:native:reset"]);
128
128
  }
129
129
  } finally {
130
- await runCommand(["bun", "--cwd=../stats", "scripts/generate-client-bundle.ts", "--reset"]);
131
- await runCommand(["bun", "scripts/generate-docs-index.ts", "--reset"]);
130
+ await runCommand(["bun", "--cwd=../stats", "run", "gen:stats:reset"]);
131
+ await runCommand(["bun", "run", "gen:docs:reset"]);
132
132
  }
133
133
  }
134
134
 
@@ -79,7 +79,7 @@ async function main(): Promise<void> {
79
79
  // so embed the dashboard archive the same way compiled binaries do
80
80
  // (scripts/build-binary.ts). Reset afterwards to keep the checked-in
81
81
  // placeholder empty.
82
- await runCommand(["bun", "--cwd=../stats", "scripts/generate-client-bundle.ts", "--generate"]);
82
+ await runCommand(["bun", "--cwd=../stats", "run", "gen:stats"]);
83
83
  try {
84
84
  await runCommand([
85
85
  "bun",
@@ -97,7 +97,7 @@ async function main(): Promise<void> {
97
97
  "./src/cli.ts",
98
98
  ]);
99
99
  } finally {
100
- await runCommand(["bun", "--cwd=../stats", "scripts/generate-client-bundle.ts", "--reset"]);
100
+ await runCommand(["bun", "--cwd=../stats", "run", "gen:stats:reset"]);
101
101
  }
102
102
  await ensureShebang();
103
103
  const stat = await fs.stat(cliPath);
@@ -3,6 +3,7 @@ import type { AgentMessage, AgentTelemetryConfig } from "@oh-my-pi/pi-agent-core
3
3
  import { type } from "arktype";
4
4
  import { createAdvisorMessageCard } from "../../modes/components/advisor-message";
5
5
  import { getThemeByName } from "../../modes/theme/theme";
6
+ import advisorSystemPrompt from "../../prompts/advisor/system.md" with { type: "text" };
6
7
  import { SecretObfuscator } from "../../secrets/obfuscator";
7
8
  import { formatSessionHistoryMarkdown } from "../../session/session-history-format";
8
9
  import { YieldQueue } from "../../session/yield-queue";
@@ -21,6 +22,41 @@ import {
21
22
  } from "..";
22
23
 
23
24
  describe("advisor", () => {
25
+ describe("advisor system prompt", () => {
26
+ it("forbids concrete claims about tool arguments hidden from the advisor transcript", () => {
27
+ const messages = [
28
+ {
29
+ role: "assistant",
30
+ content: [
31
+ {
32
+ type: "toolCall",
33
+ id: "search-timeout",
34
+ name: "grep",
35
+ arguments: { pattern: "needle", paths: ["packages/coding-agent/src"] },
36
+ },
37
+ ],
38
+ timestamp: 1,
39
+ },
40
+ {
41
+ role: "toolResult",
42
+ toolCallId: "search-timeout",
43
+ toolName: "grep",
44
+ content: [{ type: "text", text: "timed out after 30s" }],
45
+ isError: true,
46
+ timestamp: 2,
47
+ },
48
+ ] as unknown as AgentMessage[];
49
+
50
+ const rendered = formatSessionHistoryMarkdown(messages);
51
+
52
+ expect(rendered).toContain("→ grep(needle @ packages/coding-agent/src) ⇒ error");
53
+ expect(rendered).not.toContain("paths[0]");
54
+ expect(advisorSystemPrompt).toContain("Arguments absent from the rendered transcript are UNKNOWN");
55
+ expect(advisorSystemPrompt).toContain("NEVER assert concrete values, array indexes");
56
+ expect(advisorSystemPrompt).toContain("NEVER claim `paths[0]`, array flattening, or malformed `paths`");
57
+ });
58
+ });
59
+
24
60
  describe("formatSessionHistoryMarkdown includeThinking", () => {
25
61
  it("includes thinking text when includeThinking is true", () => {
26
62
  const thinking = "I should check the edge case first.";
@@ -675,13 +711,13 @@ describe("advisor", () => {
675
711
  } as AgentMessage,
676
712
  {
677
713
  role: "assistant",
678
- content: [{ type: "toolCall", id: "b", name: "search", arguments: { pattern: "y" } }],
714
+ content: [{ type: "toolCall", id: "b", name: "grep", arguments: { pattern: "y" } }],
679
715
  timestamp: 4,
680
716
  } as unknown as AgentMessage,
681
717
  {
682
718
  role: "toolResult",
683
719
  toolCallId: "b",
684
- toolName: "search",
720
+ toolName: "grep",
685
721
  content: [{ type: "text", text: "ok" }],
686
722
  isError: false,
687
723
  timestamp: 5,
@@ -1008,9 +1044,9 @@ describe("advisor", () => {
1008
1044
 
1009
1045
  describe("read-only tool allowlist", () => {
1010
1046
  it("selects only the investigation tools from a mixed toolset", () => {
1011
- const toolset = ["read", "edit", "search", "bash", "find", "write", "advise"];
1047
+ const toolset = ["read", "edit", "grep", "bash", "glob", "write", "advise"];
1012
1048
  const selected = toolset.filter(name => ADVISOR_READONLY_TOOL_NAMES.has(name));
1013
- expect(selected).toEqual(["read", "search", "find"]);
1049
+ expect(selected).toEqual(["read", "grep", "glob"]);
1014
1050
  expect(ADVISOR_READONLY_TOOL_NAMES.has("edit")).toBe(false);
1015
1051
  expect(ADVISOR_READONLY_TOOL_NAMES.has("bash")).toBe(false);
1016
1052
  expect(ADVISOR_READONLY_TOOL_NAMES.has("write")).toBe(false);
@@ -0,0 +1,147 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { AdvisorEmissionGuard, normalizeAdvisorNote } from "../emission-guard";
3
+
4
+ describe("normalizeAdvisorNote", () => {
5
+ it("collapses punctuation, casing, and surrounding whitespace into one canonical key", () => {
6
+ // The reporter's three top duplicates all key to the same canonical form
7
+ // regardless of trailing punctuation or casing — that's what makes the
8
+ // dedupe + suppression checks single-membership.
9
+ expect(normalizeAdvisorNote("Stop.")).toBe("stop");
10
+ expect(normalizeAdvisorNote(" STOP! ")).toBe("stop");
11
+ expect(normalizeAdvisorNote("*Stop*")).toBe("stop");
12
+ expect(normalizeAdvisorNote("Done.")).toBe("done");
13
+ expect(normalizeAdvisorNote("No issue; continue.")).toBe("no issue continue");
14
+ });
15
+
16
+ it("returns empty string for whitespace-only input so callers can short-circuit", () => {
17
+ expect(normalizeAdvisorNote("")).toBe("");
18
+ expect(normalizeAdvisorNote(" ")).toBe("");
19
+ expect(normalizeAdvisorNote("...")).toBe("");
20
+ });
21
+
22
+ it("preserves internal letters/digits but folds non-alphanumeric runs to one space", () => {
23
+ expect(normalizeAdvisorNote("Refactor `auth-flow.ts`: drop legacy branch.")).toBe(
24
+ "refactor auth flow ts drop legacy branch",
25
+ );
26
+ });
27
+ });
28
+
29
+ describe("AdvisorEmissionGuard", () => {
30
+ it("drops the exact content-free filler the reporter observed flooding the chat", () => {
31
+ // Issue #3520: 114× "Stop.", 52× "No issue; continue.", 41× "Done." —
32
+ // none of these carry a concrete reason and they cannot be acted on, so
33
+ // the guard suppresses them regardless of severity.
34
+ const guard = new AdvisorEmissionGuard();
35
+ expect(guard.accept("Stop.")).toBe(false);
36
+ expect(guard.accept("Done.")).toBe(false);
37
+ expect(guard.accept("No issue; continue.")).toBe(false);
38
+ expect(guard.accept("LGTM")).toBe(false);
39
+ expect(guard.accept("No further watcher input needed.")).toBe(false);
40
+ });
41
+
42
+ it("dedupes by normalized text across the session, ignoring casing and trailing punctuation", () => {
43
+ const guard = new AdvisorEmissionGuard();
44
+ expect(guard.accept("Move retries into the queue, not the request path.")).toBe(true);
45
+ // Same advice with different casing and trailing punctuation must NOT
46
+ // land twice in the primary transcript.
47
+ expect(guard.accept("move retries into the queue, not the request path")).toBe(false);
48
+ expect(guard.accept("Move retries into the queue, not the request path!")).toBe(false);
49
+ });
50
+
51
+ it("rate-limits to one accepted advise per advisor update cycle", () => {
52
+ // The advisor system prompt says "at most one `advise` per update". Real
53
+ // models violate this; the guard enforces it at the boundary so the
54
+ // primary transcript never receives two advisories from one model cycle.
55
+ const guard = new AdvisorEmissionGuard();
56
+ expect(guard.accept("First concern: missing await in #handleRetry.")).toBe(true);
57
+ expect(guard.accept("Second concern: wrong env var name.")).toBe(false);
58
+ guard.beginUpdate();
59
+ // New cycle: budget reset.
60
+ expect(guard.accept("Second concern: wrong env var name.")).toBe(true);
61
+ });
62
+
63
+ it("does not let a suppressed call consume the per-update budget", () => {
64
+ // A noise call like "Stop." must never displace a real concern that
65
+ // follows in the same advisor model cycle.
66
+ const guard = new AdvisorEmissionGuard();
67
+ expect(guard.accept("Stop.")).toBe(false);
68
+ expect(guard.accept("Concrete: read race in #handleRetry.")).toBe(true);
69
+ });
70
+
71
+ it("does not let a deduped call consume the per-update budget", () => {
72
+ // A repeat of a prior session note is dropped, but the model can still
73
+ // follow it with a fresh concrete concern in the same cycle.
74
+ const guard = new AdvisorEmissionGuard();
75
+ expect(guard.accept("Concrete: read race in #handleRetry.")).toBe(true);
76
+ guard.beginUpdate();
77
+ expect(guard.accept("Concrete: read race in #handleRetry.")).toBe(false);
78
+ expect(guard.accept("New concern: cache eviction never fires.")).toBe(true);
79
+ });
80
+
81
+ it("reset clears dedupe and the per-update gate so a re-primed advisor can re-raise old issues", () => {
82
+ // Compaction / session-switch rewrites the primary transcript. The
83
+ // advisor is re-primed from scratch and may legitimately re-raise the
84
+ // same concerns — they're new context for a freshly-primed reviewer.
85
+ const guard = new AdvisorEmissionGuard();
86
+ expect(guard.accept("Race in #handleRetry.")).toBe(true);
87
+ expect(guard.accept("Race in #handleRetry.")).toBe(false);
88
+ guard.reset();
89
+ expect(guard.accept("Race in #handleRetry.")).toBe(true);
90
+ });
91
+
92
+ it("evicts oldest entries when dedupe history exceeds capacity", () => {
93
+ // Bounded so very long sessions cannot grow the dedupe state without
94
+ // bound. Pre-eviction unique notes are remembered; post-eviction the
95
+ // oldest one is forgotten and can resurface.
96
+ const guard = new AdvisorEmissionGuard({ capacity: 3 });
97
+ expect(guard.accept("first")).toBe(true);
98
+ guard.beginUpdate();
99
+ expect(guard.accept("second")).toBe(true);
100
+ guard.beginUpdate();
101
+ expect(guard.accept("third")).toBe(true);
102
+ guard.beginUpdate();
103
+ // "first" still in history.
104
+ expect(guard.accept("first")).toBe(false);
105
+ guard.beginUpdate();
106
+ // Fourth unique entry evicts "first".
107
+ expect(guard.accept("fourth")).toBe(true);
108
+ guard.beginUpdate();
109
+ expect(guard.accept("first")).toBe(true);
110
+ });
111
+
112
+ it("rejects empty / whitespace-only notes without consuming the budget", () => {
113
+ const guard = new AdvisorEmissionGuard();
114
+ expect(guard.accept("")).toBe(false);
115
+ expect(guard.accept(" ")).toBe(false);
116
+ expect(guard.accept("Concrete advice.")).toBe(true);
117
+ });
118
+
119
+ it("end-to-end: the reporter's 309-call spam log produces ≤1 accepted note across many updates", () => {
120
+ // Mimic the issue's distribution: 114× "Stop.", 52× "No issue; continue.",
121
+ // 41× "Done.", plus 102 copies of one concrete-but-repeated nit. Spread
122
+ // the calls across 50 advisor update cycles. Each cycle is allowed at
123
+ // most one accepted note, and identical-text repeats never escape the
124
+ // guard. After all calls, exactly the concrete nit has been accepted
125
+ // — and only once.
126
+ const guard = new AdvisorEmissionGuard();
127
+ const accepted: string[] = [];
128
+ const stream: string[] = [
129
+ ...Array(114).fill("Stop."),
130
+ ...Array(52).fill("No issue; continue."),
131
+ ...Array(41).fill("Done."),
132
+ ...Array(102).fill("Concrete-but-repeated nit: x"),
133
+ ];
134
+ // Interleave across 50 update cycles.
135
+ const cycles = 50;
136
+ const perCycle = Math.ceil(stream.length / cycles);
137
+ for (let c = 0; c < cycles; c++) {
138
+ guard.beginUpdate();
139
+ for (let i = 0; i < perCycle; i++) {
140
+ const note = stream[c * perCycle + i];
141
+ if (note === undefined) break;
142
+ if (guard.accept(note)) accepted.push(note);
143
+ }
144
+ }
145
+ expect(accepted).toEqual(["Concrete-but-repeated nit: x"]);
146
+ });
147
+ });
@@ -137,7 +137,7 @@ export function deriveAdvisorTelemetry(
137
137
  * inspect the workspace before weighing in. Names match the primary session's
138
138
  * tool instances, which the advisor reuses.
139
139
  */
140
- export const ADVISOR_READONLY_TOOL_NAMES: ReadonlySet<string> = new Set(["read", "search", "find"]);
140
+ export const ADVISOR_READONLY_TOOL_NAMES: ReadonlySet<string> = new Set(["read", "grep", "glob"]);
141
141
 
142
142
  function advisorNoteDedupeKey(note: string): string {
143
143
  return note.trim().replace(/\s+/g, " ");
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Per-session policy gate for advisor `advise()` calls.
3
+ *
4
+ * The advisor system prompt tells the watcher model:
5
+ *
6
+ * > at most one `advise` per update
7
+ * > NEVER repeat advice you already gave, and NEVER send the same advice twice
8
+ *
9
+ * Real advisor models violate this. Issue #3520 captured a session where
10
+ * `__advisor.jsonl` recorded 309 `advise` calls covering 92 unique notes —
11
+ * 114× `Stop.`, 52× `No issue; continue.`, 41× `Done.` — flooding the primary
12
+ * transcript with `<advisory severity="blocker">Stop.</advisory>` after the
13
+ * task was already complete. The fix is to make the rules load-bearing in code
14
+ * instead of prose: silently drop duplicates, content-free self-talk, and
15
+ * over-budget calls at the `enqueueAdvice` boundary so the primary stays
16
+ * clean even when the advisor misbehaves.
17
+ *
18
+ * The gate is intentionally invisible to the advisor model — `AdviseTool`
19
+ * still returns `Recorded.` for a suppressed call. Surfacing "suppressed"
20
+ * back into advisor context risks the model rephrasing the same useless note
21
+ * to bypass the dedupe ("Stop.", then "Halt." then "Stop now.").
22
+ */
23
+
24
+ /**
25
+ * Case-insensitive, punctuation-folded normalization. Collapses every run of
26
+ * non-letter / non-digit characters into a single space and trims, so
27
+ * `"Stop."`, `"*Stop*"`, and `" stop "` all key to `stop`, while
28
+ * `"No issue; continue."` keys to `no issue continue`.
29
+ *
30
+ * Exported for tests.
31
+ */
32
+ export function normalizeAdvisorNote(note: string): string {
33
+ return note
34
+ .toLowerCase()
35
+ .normalize("NFKC")
36
+ .replace(/[^\p{L}\p{N}]+/gu, " ")
37
+ .trim();
38
+ }
39
+
40
+ /**
41
+ * Normalized phrases the advisor occasionally emits that carry no concrete
42
+ * actionable content. Each must be the output of {@link normalizeAdvisorNote}
43
+ * so a single membership check covers every punctuation/casing variant
44
+ * (`"Stop."`, `"stop"`, `"STOP!"`).
45
+ *
46
+ * The list is conservative — only short, content-free filler the reporter
47
+ * observed driving primary-transcript pollution. A genuine `blocker` like
48
+ * `"Stop: 'await' missing on writeStream.end() will lose buffered writes."`
49
+ * does not match.
50
+ */
51
+ const SUPPRESSED_NORMALIZED_PHRASES: Record<string, true> = {
52
+ // Self-stop noise — telling the agent to "stop" without a reason is useless.
53
+ stop: true,
54
+ "stop here": true,
55
+ "stop now": true,
56
+ halt: true,
57
+ abort: true,
58
+ // Completion self-talk — the agent already finished the task.
59
+ done: true,
60
+ "task done": true,
61
+ "task complete": true,
62
+ complete: true,
63
+ finished: true,
64
+ ok: true,
65
+ okay: true,
66
+ "ok done": true,
67
+ // "Nothing to flag" — silence is the correct expression of "no concerns".
68
+ "no issue": true,
69
+ "no issues": true,
70
+ "no issue continue": true,
71
+ "no concerns": true,
72
+ "no concern": true,
73
+ "nothing to add": true,
74
+ "nothing to flag": true,
75
+ "nothing to report": true,
76
+ "no notes": true,
77
+ "no further input": true,
78
+ "no further input needed": true,
79
+ "no further input required": true,
80
+ "no further watcher input": true,
81
+ "no further watcher input needed": true,
82
+ "no further advice": true,
83
+ "no further advice needed": true,
84
+ // Endorsements — equivalent to silence.
85
+ lgtm: true,
86
+ "looks good": true,
87
+ "all good": true,
88
+ "agent is on track": true,
89
+ "agent on track": true,
90
+ "on track": true,
91
+ continue: true,
92
+ "carry on": true,
93
+ };
94
+
95
+ /**
96
+ * Bounds the dedupe history. Sessions with very long advisor activity could
97
+ * otherwise grow the set without bound. The reporter's pathological session
98
+ * had 92 unique notes; 4096 leaves headroom while staying tiny (≤ ~256 KB of
99
+ * normalized strings even at long max).
100
+ */
101
+ const DEFAULT_HISTORY_CAPACITY = 4096;
102
+
103
+ /**
104
+ * Decides whether an advisor `advise()` call should reach the primary agent.
105
+ *
106
+ * Enforces — in this order — the noise filter, session-scoped exact-text
107
+ * dedupe (FIFO-evicted at {@link DEFAULT_HISTORY_CAPACITY}), and a per-update
108
+ * rate limit of one accepted note per advisor model prompt. Suppressed calls
109
+ * never consume the per-update budget — a noise call doesn't burn the slot
110
+ * for a real concern that follows in the same update.
111
+ *
112
+ * Reset on advisor reset (compaction, session switch, `/new`) via
113
+ * {@link reset}. Per-update gate is cleared at the start of every advisor
114
+ * `agent.prompt()` cycle via {@link beginUpdate}.
115
+ */
116
+ export class AdvisorEmissionGuard {
117
+ #seen = new Set<string>();
118
+ /** Insertion-order log to drive FIFO eviction without an extra Map. */
119
+ #seenOrder: string[] = [];
120
+ #consumedThisUpdate = false;
121
+ readonly #capacity: number;
122
+
123
+ constructor(opts: { capacity?: number } = {}) {
124
+ this.#capacity = opts.capacity ?? DEFAULT_HISTORY_CAPACITY;
125
+ }
126
+
127
+ /**
128
+ * Drop all dedupe and per-update state. Called from
129
+ * `AgentSession#resetAdvisorSessionState()` whenever the advisor runtime is
130
+ * reset — same boundary as `yieldQueue.clear("advisor")`, so a re-primed
131
+ * advisor can re-raise old issues (the primary transcript was rewritten).
132
+ */
133
+ reset(): void {
134
+ this.#seen.clear();
135
+ this.#seenOrder.length = 0;
136
+ this.#consumedThisUpdate = false;
137
+ }
138
+
139
+ /**
140
+ * Clear the per-update rate-limit gate. Called by `AdvisorRuntime` right
141
+ * before each `agent.prompt(batch)` invocation so the next advisor model
142
+ * cycle starts with a fresh budget of one advise.
143
+ */
144
+ beginUpdate(): void {
145
+ this.#consumedThisUpdate = false;
146
+ }
147
+
148
+ /**
149
+ * Whether the proposed note should reach the primary. On `true` the gate
150
+ * has already recorded the note (consumed the per-update budget and added
151
+ * it to the dedupe history) — caller delivers the note. On `false` the
152
+ * caller drops it.
153
+ *
154
+ * Empty / whitespace-only notes are suppressed; the model's
155
+ * tool-args contract still requires a non-empty string but defense-in-depth.
156
+ */
157
+ accept(note: string): boolean {
158
+ const key = normalizeAdvisorNote(note);
159
+ if (!key) return false;
160
+ if (SUPPRESSED_NORMALIZED_PHRASES[key]) return false;
161
+ if (this.#seen.has(key)) return false;
162
+ if (this.#consumedThisUpdate) return false;
163
+ this.#consumedThisUpdate = true;
164
+ this.#seen.add(key);
165
+ this.#seenOrder.push(key);
166
+ if (this.#seenOrder.length > this.#capacity) {
167
+ const stale = this.#seenOrder.shift();
168
+ if (stale !== undefined) this.#seen.delete(stale);
169
+ }
170
+ return true;
171
+ }
172
+ }
@@ -1,4 +1,5 @@
1
1
  export * from "./advise-tool";
2
+ export * from "./emission-guard";
2
3
  export * from "./runtime";
3
4
  export * from "./transcript-recorder";
4
5
  export * from "./watchdog";
@@ -30,6 +30,13 @@ export interface AdvisorRuntimeHost {
30
30
  * the primary's next compaction triggers {@link AdvisorRuntime.reset}).
31
31
  */
32
32
  maintainContext?(incomingTokens: number): Promise<boolean>;
33
+ /**
34
+ * Called immediately before each `agent.prompt(batch)` cycle. Lets the host
35
+ * clear per-update advisor state — currently the one-advise-per-update gate
36
+ * in {@link AdvisorEmissionGuard}, which the host owns because it is the
37
+ * one that routes `advise()` results back to the primary.
38
+ */
39
+ beginAdvisorUpdate?(): void;
33
40
  }
34
41
 
35
42
  interface PendingDelta {
@@ -275,6 +282,10 @@ export class AdvisorRuntime {
275
282
 
276
283
  let success = false;
277
284
  try {
285
+ // Reset the host's per-update advisor state (one-advise-per-update
286
+ // gate) before each model cycle, so the new batch starts with a
287
+ // fresh budget. Dedupe history persists across cycles.
288
+ this.host.beginAdvisorUpdate?.();
278
289
  await this.agent.prompt(batch);
279
290
  success = true;
280
291
  this.#consecutiveFailures = 0;
@@ -1,8 +1,19 @@
1
1
  import * as os from "node:os";
2
2
  import * as path from "node:path";
3
- import { getAgentDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
3
+ import { getAgentDir, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
4
4
  import { expandAtImports } from "../discovery/at-imports";
5
+ import activeRepoWatchdogTemplate from "../prompts/advisor/active-repo-watchdog.md" with { type: "text" };
6
+ import type { ActiveRepoContext } from "../utils/active-repo-context";
5
7
  import { repo } from "../utils/git";
8
+ import { normalizePromptPath } from "../utils/prompt-path";
9
+
10
+ export function formatActiveRepoWatchdogPrompt(activeRepoContext: ActiveRepoContext): string {
11
+ return prompt
12
+ .render(activeRepoWatchdogTemplate, {
13
+ relativeRepoRoot: normalizePromptPath(activeRepoContext.relativeRepoRoot),
14
+ })
15
+ .trim();
16
+ }
6
17
 
7
18
  /**
8
19
  * Discover and load WATCHDOG.md files walking up from cwd, project .omp folder, and user agent dir.
package/src/cli/args.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  import { APP_NAME, CONFIG_DIR_NAME, logger } from "@oh-my-pi/pi-utils";
5
5
  import chalk from "chalk";
6
6
  import { CLI_THINKING_LEVELS, type ConfiguredThinkingLevel, parseCliThinkingLevel } from "../thinking";
7
- import { BUILTIN_TOOL_NAMES } from "../tools/builtin-names";
7
+ import { BUILTIN_TOOL_NAMES, normalizeToolNames } from "../tools/builtin-names";
8
8
  import {
9
9
  OPTIONAL_FLAGS,
10
10
  OPTIONAL_VALUE_FLAGS,
@@ -90,6 +90,7 @@ const PARSE_DEPS: ParseDeps = {
90
90
  logger,
91
91
  parseThinking: parseCliThinkingLevel,
92
92
  builtinToolNames: BUILTIN_TOOL_NAMES,
93
+ normalizeToolNames,
93
94
  thinkingEfforts: CLI_THINKING_LEVELS,
94
95
  };
95
96
 
@@ -310,6 +311,8 @@ export function getExtraHelpText(): string {
310
311
  PERPLEXITY_API_KEY - Perplexity web search API key (optional; anonymous fallback)
311
312
  PERPLEXITY_COOKIES - Perplexity web search (session cookie)
312
313
  TAVILY_API_KEY - Tavily web search
314
+ TINYFISH_API_KEY - TinyFish web search
315
+ FIRECRAWL_API_KEY - Firecrawl web search
313
316
  ANTHROPIC_SEARCH_API_KEY - Anthropic web search (override; isolates search from main ANTHROPIC_API_KEY)
314
317
  ANTHROPIC_SEARCH_BASE_URL - Anthropic web search base URL (override; pairs with ANTHROPIC_SEARCH_API_KEY)
315
318
 
@@ -330,7 +333,7 @@ ${chalk.bold("Available Tools (default-enabled unless noted):")}
330
333
  edit - Edit files with find/replace
331
334
  write - Write files (creates/overwrites)
332
335
  grep - Search file contents
333
- find - Find files by glob pattern
336
+ glob - Find files by glob pattern
334
337
  lsp - Language server protocol (code intelligence)
335
338
  python - Execute Python code (requires: ${APP_NAME} setup python)
336
339
  notebook - Edit Jupyter notebooks
@@ -28,6 +28,7 @@ import {
28
28
  type OAuthCredential,
29
29
  type OAuthProvider,
30
30
  type OAuthProviderInfo,
31
+ PASTE_CODE_LOGIN_PROVIDERS,
31
32
  PROVIDER_REGISTRY,
32
33
  SqliteAuthCredentialStore,
33
34
  } from "@oh-my-pi/pi-ai";
@@ -211,6 +212,15 @@ async function runLocalLogin(provider: OAuthProvider): Promise<void> {
211
212
  const storage = new AuthStorage(store);
212
213
  await storage.reload();
213
214
  try {
215
+ // Only paste-code providers (fixed non-loopback redirect, e.g. GitLab Duo
216
+ // Agent's vscode:// URI) get the manual paste fallback. An explicit
217
+ // `onManualCodeInput` is honored for ANY provider (the storage escape hatch),
218
+ // so for loopback providers we must not pass it: it would make
219
+ // `OAuthCallbackFlow` race a readline prompt against the HTTP callback and, if
220
+ // the callback wins, leave that prompt outstanding (dirty/blocked terminal).
221
+ // `AuthStorage.login` independently refuses to synthesize the default prompt
222
+ // for non-paste-code providers, so this is defense-in-depth on the same gate.
223
+ const usesManualInput = PASTE_CODE_LOGIN_PROVIDERS.has(provider);
214
224
  await storage.login(provider, {
215
225
  onAuth({ url, instructions }) {
216
226
  process.stdout.write(`\nOpen this URL in your browser:\n${url}\n`);
@@ -223,6 +233,13 @@ async function runLocalLogin(provider: OAuthProvider): Promise<void> {
223
233
  onPrompt(p) {
224
234
  return ask(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
225
235
  },
236
+ ...(usesManualInput
237
+ ? {
238
+ onManualCodeInput() {
239
+ return ask("Paste the authorization code (or full redirect URL):");
240
+ },
241
+ }
242
+ : undefined),
226
243
  });
227
244
  process.stdout.write(`\nCredentials saved to ${getAgentDbPath()}\n`);
228
245
  } finally {
@@ -16,6 +16,7 @@ import {
16
16
  Settings,
17
17
  type SettingValue,
18
18
  settings,
19
+ validateProviderMaxInFlightRequests,
19
20
  } from "../config/settings";
20
21
  import { SETTINGS_SCHEMA } from "../config/settings-schema";
21
22
  import { theme } from "../modes/theme/theme";
@@ -218,6 +219,9 @@ function parseAndSetValue(path: SettingPath, rawValue: string): void {
218
219
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
219
220
  throw new Error(`Invalid record JSON: ${rawValue}`);
220
221
  }
222
+ if (path === "providers.maxInFlightRequests") {
223
+ parsed = validateProviderMaxInFlightRequests(parsed);
224
+ }
221
225
  parsedValue = parsed;
222
226
  break;
223
227
  }
@@ -46,6 +46,7 @@ export interface ParseDeps {
46
46
  logger: { warn: (message: string, meta?: Record<string, unknown>) => void };
47
47
  parseThinking: (value: string | null | undefined) => ConfiguredThinkingLevel | undefined;
48
48
  builtinToolNames: readonly string[];
49
+ normalizeToolNames: (values: Iterable<string>) => string[];
49
50
  thinkingEfforts: readonly string[];
50
51
  }
51
52
 
@@ -147,10 +148,12 @@ export const STRING_SETTERS: Record<string, StringSetter> = {
147
148
  result.models = value.split(",").map(s => s.trim());
148
149
  },
149
150
  "--tools": (result, value, deps) => {
150
- const names = value
151
- .split(",")
152
- .map(s => s.trim().toLowerCase())
153
- .filter(Boolean);
151
+ const names = deps.normalizeToolNames(
152
+ value
153
+ .split(",")
154
+ .map(s => s.trim())
155
+ .filter(Boolean),
156
+ );
154
157
  const valid: string[] = [];
155
158
  for (const name of names) {
156
159
  if (deps.builtinToolNames.includes(name)) {