@phuetz/code-buddy 1.1.0 → 1.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 (291) hide show
  1. package/README.md +160 -165
  2. package/dist/agent/autonomous/agentic-coding-contract.d.ts +6 -6
  3. package/dist/agent/codebuddy-agent.d.ts +1 -1
  4. package/dist/agent/codebuddy-agent.js +69 -14
  5. package/dist/agent/execution/agent-executor.js +122 -12
  6. package/dist/agent/execution/tool-dependency-graph.js +7 -0
  7. package/dist/agent/execution/tool-hooks.d.ts +6 -0
  8. package/dist/agent/execution/tool-hooks.js +13 -5
  9. package/dist/agent/execution/tool-selection-strategy.d.ts +11 -2
  10. package/dist/agent/execution/tool-selection-strategy.js +67 -10
  11. package/dist/agent/extended-thinking.d.ts +8 -0
  12. package/dist/agent/extended-thinking.js +27 -0
  13. package/dist/agent/hermes-browser-backends.js +151 -11
  14. package/dist/agent/hermes-claw-migrate.d.ts +59 -0
  15. package/dist/agent/hermes-claw-migrate.js +427 -26
  16. package/dist/agent/hermes-parity-manifest.js +78 -71
  17. package/dist/agent/hermes-runtime-lifecycle.d.ts +67 -0
  18. package/dist/agent/hermes-runtime-lifecycle.js +468 -0
  19. package/dist/agent/middleware/index.d.ts +1 -0
  20. package/dist/agent/middleware/index.js +1 -0
  21. package/dist/agent/middleware/visual-validation-middleware.d.ts +17 -0
  22. package/dist/agent/middleware/visual-validation-middleware.js +67 -0
  23. package/dist/agent/model-benchmark.d.ts +77 -0
  24. package/dist/agent/model-benchmark.js +309 -0
  25. package/dist/agent/model-tier.d.ts +14 -0
  26. package/dist/agent/model-tier.js +71 -0
  27. package/dist/agent/multi-agent/agents/orchestrator-agent.js +17 -6
  28. package/dist/agent/multi-agent/multi-agent-system.js +3 -1
  29. package/dist/agent/multi-agent/session-fleet-bridge.d.ts +2 -0
  30. package/dist/agent/multi-agent/session-fleet-bridge.js +9 -1
  31. package/dist/agent/multi-agent/session-tools.js +22 -5
  32. package/dist/agent/reasoning/mcts.js +20 -6
  33. package/dist/agent/session-end-flush.d.ts +6 -1
  34. package/dist/agent/session-end-flush.js +20 -4
  35. package/dist/agent/streaming/streaming-handler.d.ts +7 -0
  36. package/dist/agent/streaming/streaming-handler.js +8 -1
  37. package/dist/agent/tool-executor.js +1 -0
  38. package/dist/agent/tool-handler.d.ts +5 -0
  39. package/dist/agent/tool-handler.js +130 -4
  40. package/dist/browser-automation/browser-operator-executor.js +15 -0
  41. package/dist/browser-automation/browser-use-runner.d.ts +96 -0
  42. package/dist/browser-automation/browser-use-runner.js +492 -0
  43. package/dist/browser-automation/camofox-runner.d.ts +107 -0
  44. package/dist/browser-automation/camofox-runner.js +287 -0
  45. package/dist/channels/dingtalk/index.js +3 -0
  46. package/dist/channels/discord/client.d.ts +9 -0
  47. package/dist/channels/discord/client.js +12 -0
  48. package/dist/channels/feishu/index.d.ts +167 -1
  49. package/dist/channels/feishu/index.js +432 -7
  50. package/dist/channels/gateway-lifecycle.d.ts +132 -0
  51. package/dist/channels/gateway-lifecycle.js +219 -0
  52. package/dist/channels/google-chat/index.d.ts +5 -1
  53. package/dist/channels/google-chat/index.js +5 -1
  54. package/dist/channels/imessage/index.d.ts +14 -0
  55. package/dist/channels/imessage/index.js +64 -17
  56. package/dist/channels/index.d.ts +4 -0
  57. package/dist/channels/index.js +4 -0
  58. package/dist/channels/irc/index.d.ts +73 -3
  59. package/dist/channels/irc/index.js +446 -11
  60. package/dist/channels/line/index.js +3 -0
  61. package/dist/channels/mattermost/index.d.ts +51 -4
  62. package/dist/channels/mattermost/index.js +303 -20
  63. package/dist/channels/nextcloud-talk/index.d.ts +89 -8
  64. package/dist/channels/nextcloud-talk/index.js +367 -16
  65. package/dist/channels/nostr/index.d.ts +121 -1
  66. package/dist/channels/nostr/index.js +396 -8
  67. package/dist/channels/ntfy/index.js +3 -0
  68. package/dist/channels/qq/index.js +3 -0
  69. package/dist/channels/slash-parity.d.ts +117 -0
  70. package/dist/channels/slash-parity.js +185 -0
  71. package/dist/channels/synology-chat/index.js +3 -0
  72. package/dist/channels/teams/index.d.ts +3 -0
  73. package/dist/channels/teams/index.js +3 -0
  74. package/dist/channels/telegram/client.d.ts +9 -0
  75. package/dist/channels/telegram/client.js +12 -0
  76. package/dist/channels/twilio-voice/index.js +3 -0
  77. package/dist/channels/webchat/index.d.ts +7 -1
  78. package/dist/channels/webchat/index.js +7 -1
  79. package/dist/channels/wecom/index.js +3 -0
  80. package/dist/channels/weixin/index.js +3 -0
  81. package/dist/channels/zalo/index.js +3 -0
  82. package/dist/cli/config-loader.js +1 -1
  83. package/dist/codebuddy/client.d.ts +30 -1
  84. package/dist/codebuddy/client.js +167 -20
  85. package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +5 -0
  86. package/dist/codebuddy/providers/provider-chatgpt-responses.js +1 -0
  87. package/dist/codebuddy/providers/provider-gemini-native.js +1 -1
  88. package/dist/codebuddy/providers/provider-openai-compat.d.ts +5 -0
  89. package/dist/codebuddy/providers/provider-openai-compat.js +82 -2
  90. package/dist/codebuddy/tool-definitions/computer-control-tools.js +32 -2
  91. package/dist/codebuddy/tool-definitions/core-tools.js +3 -3
  92. package/dist/codebuddy/tool-definitions/index.d.ts +2 -1
  93. package/dist/codebuddy/tool-definitions/index.js +2 -1
  94. package/dist/codebuddy/tool-definitions/multimodal-tools.d.ts +1 -0
  95. package/dist/codebuddy/tool-definitions/multimodal-tools.js +44 -1
  96. package/dist/codebuddy/tool-definitions/windows-tools.d.ts +2 -0
  97. package/dist/codebuddy/tool-definitions/windows-tools.js +34 -0
  98. package/dist/codebuddy/tools.d.ts +1 -1
  99. package/dist/codebuddy/tools.js +27 -24
  100. package/dist/commands/cli/hermes-commands.js +108 -0
  101. package/dist/commands/cli/native-engine-commands.js +102 -3
  102. package/dist/commands/cli/utility-commands.js +45 -0
  103. package/dist/commands/enhanced-command-handler.js +1 -1
  104. package/dist/commands/goal-cli.d.ts +30 -0
  105. package/dist/commands/goal-cli.js +210 -27
  106. package/dist/commands/handlers/goal-handler.d.ts +9 -2
  107. package/dist/commands/handlers/goal-handler.js +36 -9
  108. package/dist/commands/handlers/infra-handlers.js +36 -0
  109. package/dist/commands/handlers/memory-handlers.js +96 -1
  110. package/dist/commands/headless-slash.d.ts +2 -0
  111. package/dist/commands/headless-slash.js +12 -0
  112. package/dist/commands/llm-provider-resolution.d.ts +5 -3
  113. package/dist/commands/llm-provider-resolution.js +87 -33
  114. package/dist/commands/ollama.d.ts +25 -0
  115. package/dist/commands/ollama.js +100 -0
  116. package/dist/commands/provider.d.ts +5 -0
  117. package/dist/commands/provider.js +106 -55
  118. package/dist/commands/spec-next.js +2 -1
  119. package/dist/commands/spec-plan.js +5 -16
  120. package/dist/commands/tunnel.d.ts +2 -0
  121. package/dist/commands/tunnel.js +48 -0
  122. package/dist/config/config-resolver.d.ts +2 -1
  123. package/dist/config/config-resolver.js +54 -41
  124. package/dist/config/constants.d.ts +28 -0
  125. package/dist/config/constants.js +7 -0
  126. package/dist/config/env-schema.js +756 -0
  127. package/dist/config/model-tools.js +4 -4
  128. package/dist/config/toml-config.d.ts +16 -0
  129. package/dist/config/toml-config.js +3 -0
  130. package/dist/context/context-manager-v2.js +1 -0
  131. package/dist/daemon/autonomous-daemon.d.ts +1 -1
  132. package/dist/daemon/autonomous-daemon.js +3 -3
  133. package/dist/daemon/autonomous-loop.js +11 -2
  134. package/dist/daemon/colab-goal.js +9 -1
  135. package/dist/daemon/cron-agent-bridge.d.ts +12 -3
  136. package/dist/daemon/cron-agent-bridge.js +25 -9
  137. package/dist/desktop/codebuddy-engine-adapter.d.ts +32 -0
  138. package/dist/desktop/codebuddy-engine-adapter.js +257 -80
  139. package/dist/desktop/engine-adapter.d.ts +14 -0
  140. package/dist/desktop-automation/automation-manager.js +16 -0
  141. package/dist/desktop-automation/omniparser-runner.d.ts +63 -0
  142. package/dist/desktop-automation/omniparser-runner.js +115 -0
  143. package/dist/desktop-automation/smart-snapshot.d.ts +11 -0
  144. package/dist/desktop-automation/smart-snapshot.js +72 -5
  145. package/dist/doctor/index.js +40 -23
  146. package/dist/events/types.d.ts +11 -0
  147. package/dist/fleet/colab-store.js +14 -2
  148. package/dist/fleet/peer-session-bridge.js +50 -12
  149. package/dist/fleet/peer-tool-bridge.js +14 -0
  150. package/dist/goals/goal-decomposer.d.ts +27 -0
  151. package/dist/goals/goal-decomposer.js +293 -0
  152. package/dist/goals/goal-judge-client.d.ts +15 -0
  153. package/dist/goals/goal-judge-client.js +45 -0
  154. package/dist/goals/goal-judge.d.ts +2 -0
  155. package/dist/goals/goal-judge.js +35 -6
  156. package/dist/goals/goal-loop.d.ts +14 -0
  157. package/dist/goals/goal-loop.js +47 -2
  158. package/dist/goals/goal-manager.d.ts +6 -0
  159. package/dist/goals/goal-manager.js +67 -14
  160. package/dist/goals/goal-state.d.ts +13 -0
  161. package/dist/goals/goal-state.js +83 -13
  162. package/dist/goals/goal-store.d.ts +3 -0
  163. package/dist/goals/goal-store.js +31 -15
  164. package/dist/hooks/user-hooks.js +17 -3
  165. package/dist/index.js +464 -25
  166. package/dist/input/text-to-speech.d.ts +2 -6
  167. package/dist/input/text-to-speech.js +2 -27
  168. package/dist/integrations/screenpipe/screenpipe-client.d.ts +10 -0
  169. package/dist/integrations/screenpipe/screenpipe-client.js +12 -2
  170. package/dist/integrations/tailscale.d.ts +13 -0
  171. package/dist/integrations/tailscale.js +87 -34
  172. package/dist/knowledge/workspace-indexer.js +53 -9
  173. package/dist/mcp/client.js +1 -0
  174. package/dist/mcp/mcp-desktop-tools.d.ts +23 -0
  175. package/dist/mcp/mcp-desktop-tools.js +158 -0
  176. package/dist/mcp/mcp-server.js +2 -0
  177. package/dist/memory/enhanced-memory.d.ts +10 -0
  178. package/dist/memory/enhanced-memory.js +33 -2
  179. package/dist/memory/index.d.ts +3 -1
  180. package/dist/memory/index.js +4 -1
  181. package/dist/memory/memory-auto-proposer.d.ts +23 -0
  182. package/dist/memory/memory-auto-proposer.js +308 -0
  183. package/dist/memory/memory-candidate-queue.d.ts +92 -0
  184. package/dist/memory/memory-candidate-queue.js +261 -0
  185. package/dist/memory/persistent-memory.d.ts +45 -2
  186. package/dist/memory/persistent-memory.js +236 -40
  187. package/dist/ml/bayesian-qualifier.d.ts +1 -1
  188. package/dist/ml/bayesian-qualifier.js +35 -2
  189. package/dist/openclaw/gateway-bridge.js +5 -0
  190. package/dist/plugins/marketplace.d.ts +1 -0
  191. package/dist/plugins/marketplace.js +7 -0
  192. package/dist/prompts/execution-discipline.d.ts +14 -0
  193. package/dist/prompts/execution-discipline.js +29 -0
  194. package/dist/prompts/variation-injector.js +9 -2
  195. package/dist/protocols/acp/acp-agentic-runner.d.ts +7 -6
  196. package/dist/protocols/acp/acp-agentic-runner.js +115 -18
  197. package/dist/protocols/acp/acp-session-store.d.ts +23 -0
  198. package/dist/protocols/acp/acp-session-store.js +77 -0
  199. package/dist/protocols/acp/acp-stdio-server.d.ts +7 -0
  200. package/dist/protocols/acp/acp-stdio-server.js +71 -16
  201. package/dist/providers/active-llm-registry.d.ts +37 -0
  202. package/dist/providers/active-llm-registry.js +186 -0
  203. package/dist/providers/auxiliary-provider.d.ts +25 -0
  204. package/dist/providers/auxiliary-provider.js +192 -0
  205. package/dist/providers/codex-oauth.d.ts +1 -1
  206. package/dist/providers/codex-oauth.js +27 -4
  207. package/dist/providers/index.d.ts +3 -0
  208. package/dist/providers/index.js +4 -0
  209. package/dist/providers/provider-catalog.d.ts +70 -0
  210. package/dist/providers/provider-catalog.js +738 -0
  211. package/dist/providers/provider-fallback.d.ts +35 -0
  212. package/dist/providers/provider-fallback.js +171 -0
  213. package/dist/providers/xai-oauth.d.ts +128 -0
  214. package/dist/providers/xai-oauth.js +735 -0
  215. package/dist/scheduler/cron-scheduler.d.ts +15 -3
  216. package/dist/scheduler/cron-scheduler.js +53 -7
  217. package/dist/scheduler/script-runner.d.ts +2 -0
  218. package/dist/scheduler/script-runner.js +19 -4
  219. package/dist/scheduler/watchdog-handlers.js +7 -20
  220. package/dist/search/usearch-index.js +7 -2
  221. package/dist/security/tool-policy/tool-groups.js +2 -0
  222. package/dist/server/index.js +17 -2
  223. package/dist/server/routes/mobile.d.ts +12 -4
  224. package/dist/server/routes/mobile.js +116 -1
  225. package/dist/server/tls-config.d.ts +35 -0
  226. package/dist/server/tls-config.js +142 -0
  227. package/dist/server/tunnel-manager.d.ts +20 -0
  228. package/dist/server/tunnel-manager.js +58 -0
  229. package/dist/server/websocket/fleet-bridge.js +5 -0
  230. package/dist/services/prompt-builder.d.ts +1 -0
  231. package/dist/services/prompt-builder.js +49 -8
  232. package/dist/shared/engine-types.d.ts +15 -1
  233. package/dist/sidecar/sidecar-bridge.d.ts +1 -0
  234. package/dist/sidecar/sidecar-bridge.js +21 -10
  235. package/dist/spec/spec-store.js +8 -1
  236. package/dist/talk-mode/providers/audioreader-tts.d.ts +1 -1
  237. package/dist/talk-mode/providers/audioreader-tts.js +8 -8
  238. package/dist/templates/project-scaffolding.js +3 -2
  239. package/dist/themes/theme-schema.d.ts +10 -10
  240. package/dist/tools/application-profiles.js +38 -0
  241. package/dist/tools/bash/bash-tool.d.ts +1 -0
  242. package/dist/tools/bash/bash-tool.js +16 -2
  243. package/dist/tools/bash/command-validator.js +3 -0
  244. package/dist/tools/bash/rtk-rewrite.d.ts +16 -0
  245. package/dist/tools/bash/rtk-rewrite.js +100 -0
  246. package/dist/tools/bash/streaming-executor.js +14 -1
  247. package/dist/tools/computer-control-tool.d.ts +16 -1
  248. package/dist/tools/computer-control-tool.js +317 -5
  249. package/dist/tools/document-generator.d.ts +14 -0
  250. package/dist/tools/document-generator.js +79 -0
  251. package/dist/tools/execute-code-rpc-invoker.d.ts +26 -2
  252. package/dist/tools/execute-code-rpc-invoker.js +46 -5
  253. package/dist/tools/metadata.js +28 -0
  254. package/dist/tools/office-macro-tool.d.ts +10 -0
  255. package/dist/tools/office-macro-tool.js +93 -0
  256. package/dist/tools/registry/index.d.ts +2 -1
  257. package/dist/tools/registry/index.js +2 -1
  258. package/dist/tools/registry/memory-tools.d.ts +20 -0
  259. package/dist/tools/registry/memory-tools.js +225 -3
  260. package/dist/tools/registry/multimodal-tools.js +2 -2
  261. package/dist/tools/registry/vision-tools.d.ts +33 -0
  262. package/dist/tools/registry/vision-tools.js +308 -6
  263. package/dist/tools/registry/windows-tools.d.ts +2 -0
  264. package/dist/tools/registry/windows-tools.js +47 -0
  265. package/dist/tools/route-peer-tool.js +14 -0
  266. package/dist/tools/screenshot-tool.js +14 -2
  267. package/dist/tools/text-to-speech-tool.d.ts +1 -1
  268. package/dist/tools/text-to-speech-tool.js +2 -12
  269. package/dist/tools/tool-selector.js +3 -59
  270. package/dist/utils/config-validation/schema.d.ts +40 -7
  271. package/dist/utils/config-validation/schema.js +130 -1
  272. package/dist/utils/cost-tracker.js +21 -1
  273. package/dist/utils/disk-guard.d.ts +180 -0
  274. package/dist/utils/disk-guard.js +404 -0
  275. package/dist/utils/interactive-setup.js +35 -2
  276. package/dist/utils/model-utils.js +11 -2
  277. package/dist/utils/output-sanitizer.js +11 -0
  278. package/dist/utils/provider-detector.d.ts +8 -7
  279. package/dist/utils/provider-detector.js +19 -77
  280. package/dist/utils/settings-manager.d.ts +7 -0
  281. package/dist/utils/token-display.d.ts +3 -3
  282. package/dist/utils/token-display.js +22 -4
  283. package/dist/wizard/onboarding.d.ts +39 -0
  284. package/dist/wizard/onboarding.js +433 -21
  285. package/dist/wizard/provider-onboarding.d.ts +8 -23
  286. package/dist/wizard/provider-onboarding.js +39 -158
  287. package/package.json +4 -2
  288. package/dist/plugins/provider-onboarding.d.ts +0 -23
  289. package/dist/plugins/provider-onboarding.js +0 -116
  290. package/dist/utils/kokoro-tts.d.ts +0 -8
  291. package/dist/utils/kokoro-tts.js +0 -67
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Live model benchmarking for Tailnet Ollama peers.
3
+ *
4
+ * Measures actual request latency and simple compliance signals so the router
5
+ * can prefer the best network model instead of relying on a static ordering.
6
+ * The benchmark is intentionally lightweight: a few objective prompts, temp=0,
7
+ * streamed completions, and a persisted score cache that the model-tier ladder
8
+ * can use to rank candidates.
9
+ */
10
+ import fs from 'node:fs/promises';
11
+ import os from 'node:os';
12
+ import path from 'node:path';
13
+ import { normalizeBaseURL } from '../utils/base-url.js';
14
+ export const BENCHMARK_PROMPT_SETS = {
15
+ balanced: [
16
+ {
17
+ name: 'exact-ok',
18
+ prompt: 'Reply with exactly OK and nothing else.',
19
+ maxTokens: 8,
20
+ validate: (output) => output.trim() === 'OK',
21
+ },
22
+ {
23
+ name: 'json-object',
24
+ prompt: 'Return exactly this JSON object and nothing else: {"model":"darkstar","status":"ok"}',
25
+ maxTokens: 32,
26
+ validate: (output) => {
27
+ try {
28
+ const parsed = JSON.parse(stripCodeFences(output));
29
+ return parsed?.model === 'darkstar' && parsed?.status === 'ok';
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ },
35
+ },
36
+ {
37
+ name: 'code-dedupe',
38
+ prompt: 'Write a TypeScript function named dedupeById that removes duplicate objects by id using a Map. Output only the function.',
39
+ maxTokens: 96,
40
+ validate: (output) => /dedupeById/.test(output) && /Map/.test(output) && /return/.test(output),
41
+ },
42
+ ],
43
+ coding: [
44
+ {
45
+ name: 'json-object',
46
+ prompt: 'Return exactly this JSON object and nothing else: {"model":"darkstar","status":"ok"}',
47
+ maxTokens: 32,
48
+ validate: (output) => {
49
+ try {
50
+ const parsed = JSON.parse(stripCodeFences(output));
51
+ return parsed?.model === 'darkstar' && parsed?.status === 'ok';
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ },
57
+ },
58
+ {
59
+ name: 'code-dedupe',
60
+ prompt: 'Write a TypeScript function named dedupeById that removes duplicate objects by id using a Map. Output only the function.',
61
+ maxTokens: 96,
62
+ validate: (output) => /dedupeById/.test(output) && /Map/.test(output) && /return/.test(output),
63
+ },
64
+ ],
65
+ latency: [
66
+ {
67
+ name: 'exact-ok',
68
+ prompt: 'Reply with exactly OK and nothing else.',
69
+ maxTokens: 8,
70
+ validate: (output) => output.trim() === 'OK',
71
+ },
72
+ {
73
+ name: 'short-json',
74
+ prompt: 'Return exactly {"ok":true} and nothing else.',
75
+ maxTokens: 16,
76
+ validate: (output) => {
77
+ try {
78
+ const parsed = JSON.parse(stripCodeFences(output));
79
+ return parsed?.ok === true;
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ },
85
+ },
86
+ ],
87
+ };
88
+ export function defaultBenchmarkIndexPath(homeDir = os.homedir()) {
89
+ return path.join(homeDir, '.codebuddy', 'model-benchmarks.json');
90
+ }
91
+ export function benchmarkCandidateKey(candidate) {
92
+ return `${normalizeBaseURL(candidate.baseUrl)}::${candidate.model}`;
93
+ }
94
+ export async function benchmarkCandidates(candidates, options = {}) {
95
+ const promptSet = options.promptSet ?? 'balanced';
96
+ const prompts = BENCHMARK_PROMPT_SETS[promptSet];
97
+ const runs = Math.max(1, options.runs ?? 1);
98
+ const fetchImpl = options.fetchImpl ?? fetch;
99
+ const reports = [];
100
+ for (const candidate of candidates) {
101
+ const candidateRuns = [];
102
+ for (let runIndex = 0; runIndex < runs; runIndex += 1) {
103
+ for (const prompt of prompts) {
104
+ candidateRuns.push(await runSinglePrompt(candidate, prompt, fetchImpl, options.timeoutMs));
105
+ }
106
+ }
107
+ reports.push({
108
+ candidate,
109
+ runs: candidateRuns,
110
+ summary: summarizeBenchmarkRuns(candidateRuns),
111
+ });
112
+ }
113
+ return reports;
114
+ }
115
+ export function summarizeBenchmarkRuns(runs) {
116
+ const runsCount = runs.length;
117
+ const successes = runs.filter((run) => run.success).length;
118
+ const complianceRuns = runs.filter((run) => run.success && run.compliance).length;
119
+ const successfulRuns = runs.filter((run) => run.success);
120
+ const avgTtftMs = average(successfulRuns.map((run) => run.ttftMs));
121
+ const avgTotalMs = average(successfulRuns.map((run) => run.totalMs));
122
+ const avgOutputTokensEstimate = average(successfulRuns.map((run) => run.outputTokensEstimate));
123
+ const complianceRate = runsCount > 0 ? complianceRuns / runsCount : 0;
124
+ // Weighted score: correctness dominates, then speed. Higher is better.
125
+ const failurePenalty = (runsCount - successes) * 1000;
126
+ const score = successes === 0
127
+ ? -1_000_000 - failurePenalty
128
+ : (complianceRate * 1000)
129
+ - avgTtftMs
130
+ - (avgTotalMs * 0.5)
131
+ + (successes / Math.max(1, runsCount)) * 100
132
+ - failurePenalty;
133
+ return {
134
+ runs: runsCount,
135
+ successes,
136
+ complianceRate,
137
+ avgTtftMs,
138
+ avgTotalMs,
139
+ avgOutputTokensEstimate,
140
+ score,
141
+ };
142
+ }
143
+ export async function writeBenchmarkIndex(reports, suite, indexPath = defaultBenchmarkIndexPath()) {
144
+ const entries = reports
145
+ .map((report) => ({
146
+ model: report.candidate.model,
147
+ baseUrl: normalizeBaseURL(report.candidate.baseUrl),
148
+ ...(report.candidate.label ? { label: report.candidate.label } : {}),
149
+ score: report.summary.score,
150
+ complianceRate: report.summary.complianceRate,
151
+ avgTtftMs: report.summary.avgTtftMs,
152
+ avgTotalMs: report.summary.avgTotalMs,
153
+ updatedAt: new Date().toISOString(),
154
+ }))
155
+ .sort((a, b) => b.score - a.score);
156
+ const index = {
157
+ updatedAt: new Date().toISOString(),
158
+ suite,
159
+ entries,
160
+ };
161
+ await fs.mkdir(path.dirname(indexPath), { recursive: true });
162
+ await fs.writeFile(indexPath, `${JSON.stringify(index, null, 2)}\n`, 'utf8');
163
+ return index;
164
+ }
165
+ export async function loadBenchmarkIndex(indexPath = defaultBenchmarkIndexPath()) {
166
+ try {
167
+ const raw = await fs.readFile(indexPath, 'utf8');
168
+ const parsed = JSON.parse(raw);
169
+ if (!parsed || !Array.isArray(parsed.entries))
170
+ return null;
171
+ return parsed;
172
+ }
173
+ catch {
174
+ return null;
175
+ }
176
+ }
177
+ export async function loadBenchmarkScoreMap(indexPath = defaultBenchmarkIndexPath()) {
178
+ const index = await loadBenchmarkIndex(indexPath);
179
+ const map = new Map();
180
+ for (const entry of index?.entries ?? []) {
181
+ map.set(`${normalizeBaseURL(entry.baseUrl)}::${entry.model}`, entry.score);
182
+ }
183
+ return map;
184
+ }
185
+ async function runSinglePrompt(candidate, prompt, fetchImpl, timeoutMs) {
186
+ const endpoint = `${normalizeBaseURL(candidate.baseUrl)}/chat/completions`;
187
+ const startedAt = performance.now();
188
+ const headers = { 'Content-Type': 'application/json' };
189
+ try {
190
+ const response = await fetchImpl(endpoint, {
191
+ method: 'POST',
192
+ headers,
193
+ body: JSON.stringify({
194
+ model: candidate.model,
195
+ messages: [{ role: 'user', content: prompt.prompt }],
196
+ temperature: 0,
197
+ max_tokens: prompt.maxTokens,
198
+ stream: true,
199
+ stream_options: { include_usage: true },
200
+ }),
201
+ signal: AbortSignal.timeout(timeoutMs ?? 60_000),
202
+ });
203
+ if (!response.ok || !response.body) {
204
+ const body = await safeText(response);
205
+ return failureRun(prompt.name, startedAt, `HTTP ${response.status}: ${body.slice(0, 200)}`);
206
+ }
207
+ const streamed = await readOpenAIChatStream(response, startedAt);
208
+ const totalMs = performance.now() - startedAt;
209
+ const compliance = prompt.validate(streamed.output);
210
+ return {
211
+ promptName: prompt.name,
212
+ success: true,
213
+ compliance,
214
+ ttftMs: streamed.ttftMs ?? totalMs,
215
+ totalMs,
216
+ outputChars: streamed.output.length,
217
+ outputTokensEstimate: streamed.outputTokensEstimate ?? estimateTokens(streamed.output),
218
+ ...(streamed.output.length ? { outputPreview: trimPreview(streamed.output) } : {}),
219
+ };
220
+ }
221
+ catch (error) {
222
+ return failureRun(prompt.name, startedAt, error instanceof Error ? error.message : String(error));
223
+ }
224
+ }
225
+ async function readOpenAIChatStream(response, startedAt) {
226
+ const reader = response.body?.getReader();
227
+ if (!reader) {
228
+ return { output: '', ttftMs: null, outputTokensEstimate: null };
229
+ }
230
+ const decoder = new TextDecoder();
231
+ let buffer = '';
232
+ let output = '';
233
+ let ttftMs = null;
234
+ let firstContentSeen = false;
235
+ while (true) {
236
+ const { done, value } = await reader.read();
237
+ if (done)
238
+ break;
239
+ buffer += decoder.decode(value, { stream: true });
240
+ let newlineIndex = buffer.indexOf('\n');
241
+ while (newlineIndex >= 0) {
242
+ const rawLine = buffer.slice(0, newlineIndex);
243
+ buffer = buffer.slice(newlineIndex + 1);
244
+ const line = rawLine.trim();
245
+ if (line.startsWith('data:')) {
246
+ const payload = line.slice(5).trim();
247
+ if (payload && payload !== '[DONE]') {
248
+ try {
249
+ const parsed = JSON.parse(payload);
250
+ const delta = parsed.choices?.[0]?.delta?.content ?? '';
251
+ if (delta) {
252
+ output += delta;
253
+ if (!firstContentSeen) {
254
+ ttftMs = performance.now() - startedAt;
255
+ firstContentSeen = true;
256
+ }
257
+ }
258
+ }
259
+ catch {
260
+ // Ignore malformed SSE frames and continue reading the stream.
261
+ }
262
+ }
263
+ }
264
+ newlineIndex = buffer.indexOf('\n');
265
+ }
266
+ }
267
+ return {
268
+ output,
269
+ ttftMs,
270
+ outputTokensEstimate: estimateTokens(output),
271
+ };
272
+ }
273
+ function failureRun(promptName, startedAt, error) {
274
+ const totalMs = performance.now() - startedAt;
275
+ return {
276
+ promptName,
277
+ success: false,
278
+ compliance: false,
279
+ ttftMs: totalMs,
280
+ totalMs,
281
+ outputChars: 0,
282
+ outputTokensEstimate: 0,
283
+ error,
284
+ };
285
+ }
286
+ function average(values) {
287
+ if (values.length === 0)
288
+ return 0;
289
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
290
+ }
291
+ function estimateTokens(text) {
292
+ return Math.max(1, Math.ceil(text.trim().length / 4));
293
+ }
294
+ function trimPreview(text, maxChars = 200) {
295
+ const trimmed = text.trim().replace(/\s+/g, ' ');
296
+ return trimmed.length > maxChars ? `${trimmed.slice(0, maxChars)}...` : trimmed;
297
+ }
298
+ function stripCodeFences(text) {
299
+ return text.trim().replace(/^```(?:json|ts|typescript)?\s*/i, '').replace(/\s*```$/i, '');
300
+ }
301
+ async function safeText(response) {
302
+ try {
303
+ return await response.text();
304
+ }
305
+ catch {
306
+ return '';
307
+ }
308
+ }
309
+ //# sourceMappingURL=model-benchmark.js.map
@@ -32,6 +32,13 @@ export interface ModelTierConfig {
32
32
  /** Paid cloud model, last resort (undefined = never escalate to paid). */
33
33
  escalationModel?: string;
34
34
  }
35
+ export interface LiveModelTierDiscoveryOptions {
36
+ /**
37
+ * When true, the resolver keeps the static `CODEBUDDY_NETWORK_MODELS`
38
+ * entries and appends live Tailnet peers discovered at runtime.
39
+ */
40
+ augmentConfiguredNetworkModels?: boolean;
41
+ }
35
42
  export interface EscalationSignal {
36
43
  /** Caller explicitly wants the strongest available model for this turn. */
37
44
  escalate?: boolean;
@@ -65,6 +72,13 @@ export interface AutonomousModelChoice {
65
72
  * - escalation: `CODEBUDDY_ESCALATION_MODEL` → `GROK_MODEL` → none.
66
73
  */
67
74
  export declare function resolveModelTierConfig(env?: NodeJS.ProcessEnv): ModelTierConfig;
75
+ /**
76
+ * Resolve the ladder and enrich the network rung with live Tailnet Ollama peers
77
+ * when the `tailscale` CLI is available. This keeps the system dynamic: the
78
+ * network tier grows as peers come and go instead of relying on a hardcoded
79
+ * host list.
80
+ */
81
+ export declare function resolveLiveModelTierConfig(env?: NodeJS.ProcessEnv, options?: LiveModelTierDiscoveryOptions): Promise<ModelTierConfig>;
68
82
  export declare function parseNetworkModels(raw: string | undefined): NetworkModel[];
69
83
  /**
70
84
  * Choose the model for an autonomous turn. Free-first: local by default, the
@@ -14,6 +14,7 @@
14
14
  * Pure decision core: given the configured tiers and an escalation signal, it
15
15
  * returns which model to use and why. It performs no inference/routing itself.
16
16
  */
17
+ import { normalizeBaseURL } from '../utils/base-url.js';
17
18
  const PRIORITY_RANK = { critical: 3, high: 2, medium: 1, low: 0 };
18
19
  const DEFAULT_LOCAL_MODEL = 'llama3.2';
19
20
  const DEFAULT_LOCAL_BASE_URL = 'http://localhost:11434/v1';
@@ -41,6 +42,43 @@ export function resolveModelTierConfig(env = process.env) {
41
42
  ...(escalationModel ? { escalationModel } : {}),
42
43
  };
43
44
  }
45
+ /**
46
+ * Resolve the ladder and enrich the network rung with live Tailnet Ollama peers
47
+ * when the `tailscale` CLI is available. This keeps the system dynamic: the
48
+ * network tier grows as peers come and go instead of relying on a hardcoded
49
+ * host list.
50
+ */
51
+ export async function resolveLiveModelTierConfig(env = process.env, options = {}) {
52
+ const config = resolveModelTierConfig(env);
53
+ let discovered = [];
54
+ let benchmarkScores = new Map();
55
+ try {
56
+ const { TailscaleManager } = await import('../integrations/tailscale.js');
57
+ discovered = (await TailscaleManager.getInstance().discoverOllamaPeers()).flatMap((peer) => peer.models.map((model) => ({
58
+ model,
59
+ baseUrl: peer.baseURL,
60
+ label: peer.hostname,
61
+ })));
62
+ }
63
+ catch {
64
+ discovered = [];
65
+ }
66
+ try {
67
+ const { loadBenchmarkScoreMap } = await import('./model-benchmark.js');
68
+ benchmarkScores = await loadBenchmarkScoreMap();
69
+ }
70
+ catch {
71
+ benchmarkScores = new Map();
72
+ }
73
+ const configured = config.networkModels ?? [];
74
+ const merged = options.augmentConfiguredNetworkModels === false
75
+ ? discovered
76
+ : mergeNetworkModels(configured, discovered);
77
+ return {
78
+ ...config,
79
+ networkModels: rankNetworkModelsByBenchmark(merged, benchmarkScores),
80
+ };
81
+ }
44
82
  export function parseNetworkModels(raw) {
45
83
  if (!raw?.trim())
46
84
  return [];
@@ -56,6 +94,39 @@ export function parseNetworkModels(raw) {
56
94
  function normalizeBaseUrl(url) {
57
95
  return url.replace(/\/+$/, '');
58
96
  }
97
+ function mergeNetworkModels(primary, secondary) {
98
+ const seen = new Set();
99
+ const merged = [];
100
+ for (const item of [...primary, ...secondary]) {
101
+ const key = `${item.model}@${normalizeBaseUrl(item.baseUrl)}`;
102
+ if (seen.has(key))
103
+ continue;
104
+ seen.add(key);
105
+ merged.push(item);
106
+ }
107
+ return merged;
108
+ }
109
+ function rankNetworkModelsByBenchmark(networkModels, benchmarkScores) {
110
+ if (networkModels.length === 0 || benchmarkScores.size === 0) {
111
+ return networkModels;
112
+ }
113
+ return networkModels
114
+ .map((model, index) => ({
115
+ model,
116
+ index,
117
+ score: benchmarkScores.get(`${normalizeBaseURL(model.baseUrl)}::${model.model}`),
118
+ }))
119
+ .sort((a, b) => {
120
+ const aHas = typeof a.score === 'number';
121
+ const bHas = typeof b.score === 'number';
122
+ if (aHas && bHas && a.score !== b.score)
123
+ return (b.score ?? 0) - (a.score ?? 0);
124
+ if (aHas !== bHas)
125
+ return aHas ? -1 : 1;
126
+ return a.index - b.index;
127
+ })
128
+ .map((entry) => entry.model);
129
+ }
59
130
  /** Escalation rung 0 (local) / 1 (network) / 2 (paid) implied by the signal. */
60
131
  function escalationLevel(signal, policy) {
61
132
  let level = 0;
@@ -64,7 +64,12 @@ DECISION MAKING:
64
64
  "bash",
65
65
  "codebase_map",
66
66
  ],
67
- maxRounds: 20,
67
+ // Bounded so a model that over-explores during planning/synthesis can't loop
68
+ // indefinitely on a large repo. With the "plan directly" task prompt, planning
69
+ // normally terminates in 1 round; if a model still exhausts this cap without
70
+ // emitting a <plan>, parsePlan() falls back to a default coder task so the
71
+ // workflow still delegates to the worker agents.
72
+ maxRounds: 8,
68
73
  };
69
74
  export class OrchestratorAgent extends BaseAgent {
70
75
  executionPlan = null;
@@ -88,16 +93,18 @@ export class OrchestratorAgent extends BaseAgent {
88
93
  const planningTask = {
89
94
  id: createId("task"),
90
95
  title: "Create Execution Plan",
91
- description: `Analyze the following goal and create a detailed execution plan:
96
+ description: `Analyze the following goal and OUTPUT an execution plan using the <plan> XML format from your instructions.
92
97
 
93
98
  GOAL: ${goal}
94
99
 
95
- Explore the codebase to understand the current state, then create a comprehensive plan that:
96
- 1. Breaks down the goal into manageable phases
100
+ Plan directly. Do NOT explore the codebase unless it is strictly required to plan for self-contained tasks, skip exploration entirely and emit the plan in your first response. A good plan:
101
+ 1. Breaks the goal into manageable phases
97
102
  2. Identifies specific tasks within each phase
98
103
  3. Assigns each task to the most appropriate agent
99
104
  4. Considers dependencies and parallelization opportunities
100
- 5. Includes validation and review steps`,
105
+ 5. Includes validation and review steps
106
+
107
+ Respond with ONLY the <plan>...</plan> block and no other prose.`,
101
108
  status: "in_progress",
102
109
  priority: "critical",
103
110
  assignedTo: "orchestrator",
@@ -153,7 +160,11 @@ Explore the codebase to understand the current state, then create a comprehensiv
153
160
  description: taskDescMatch?.[1]?.trim() ?? "",
154
161
  status: "pending",
155
162
  priority: priority,
156
- assignedTo: agent,
163
+ // Normalize role casing: the planning prompt lists specialists as
164
+ // "Coder"/"Reviewer"/"Tester" (capitalized), so models often emit
165
+ // `agent="Coder"`, but the agent registry is keyed lowercase. Without
166
+ // this, `agents.get("Coder")` misses and the worker never runs.
167
+ assignedTo: (agent ?? "coder").toLowerCase(),
157
168
  dependencies: [],
158
169
  subtasks: [],
159
170
  artifacts: [],
@@ -507,7 +507,9 @@ export class MultiAgentSystem extends EventEmitter {
507
507
  this.addTimelineEvent("task_started", `Reallocated: ${task.title} → ${resolvedRole}`, { task, originalRole: task.assignedTo, resolvedRole });
508
508
  task.assignedTo = resolvedRole;
509
509
  }
510
- const agent = this.agents.get(task.assignedTo);
510
+ // Case-insensitive lookup: plans may carry capitalized roles ("Coder").
511
+ const agent = this.agents.get(task.assignedTo) ??
512
+ this.agents.get(String(task.assignedTo).toLowerCase());
511
513
  if (!agent) {
512
514
  errors.push(`No agent found for role: ${task.assignedTo}`);
513
515
  return;
@@ -28,6 +28,8 @@
28
28
  * could fill the WS send buffer.
29
29
  */
30
30
  import type { SessionRegistry } from './session-registry.js';
31
+ export type FleetEventBroadcaster = (type: any, payload: any, agentId?: string) => void;
32
+ export declare function registerSessionFleetBroadcaster(broadcaster: FleetEventBroadcaster): void;
31
33
  export interface SessionFleetBridgeHandle {
32
34
  /** Detach listeners. Idempotent. */
33
35
  disable: () => void;
@@ -27,7 +27,15 @@
27
27
  * - No back-pressure (same as (d).1 deferral). Bursty session activity
28
28
  * could fill the WS send buffer.
29
29
  */
30
- import { broadcastFleetEvent } from '../../server/websocket/fleet-bridge.js';
30
+ let _fleetBroadcaster = null;
31
+ export function registerSessionFleetBroadcaster(broadcaster) {
32
+ _fleetBroadcaster = broadcaster;
33
+ }
34
+ function broadcastFleetEvent(type, payload, agentId) {
35
+ if (_fleetBroadcaster) {
36
+ _fleetBroadcaster(type, payload, agentId);
37
+ }
38
+ }
31
39
  function isFleetStreamEnabled() {
32
40
  const v = process.env.CODEBUDDY_FLEET_STREAM;
33
41
  return v === '1' || v === 'true' || v === 'TRUE';
@@ -368,21 +368,36 @@ export class SessionToolExecutor {
368
368
  const limit = this.parseSessionSearchLimit(args.limit);
369
369
  const { getSessionStore } = await import('../../persistence/session-store.js');
370
370
  const sessions = (await getSessionStore().searchSessions(query)).slice(0, limit);
371
+ const citations = [];
371
372
  const formatted = sessions.map(session => {
372
373
  const metadata = session.metadata && typeof session.metadata === 'object'
373
374
  ? session.metadata
374
375
  : {};
376
+ const messageId = typeof metadata.searchMessageId === 'number' ? metadata.searchMessageId : undefined;
377
+ const role = typeof metadata.searchRole === 'string' ? metadata.searchRole : undefined;
378
+ const snippet = typeof metadata.searchSnippet === 'string' ? metadata.searchSnippet : '';
379
+ const citation = {
380
+ sessionId: session.id,
381
+ snippet,
382
+ label: messageId ? `session:${session.id}#message:${messageId}` : `session:${session.id}`,
383
+ };
384
+ if (messageId !== undefined)
385
+ citation.messageId = messageId;
386
+ if (role)
387
+ citation.role = role;
375
388
  const match = {
376
- snippet: metadata.searchSnippet ?? '',
389
+ snippet,
390
+ citation,
377
391
  };
378
- if (metadata.searchRole)
379
- match.role = metadata.searchRole;
392
+ if (role)
393
+ match.role = role;
380
394
  if (typeof metadata.searchScore === 'number')
381
395
  match.score = metadata.searchScore;
382
- if (typeof metadata.searchMessageId === 'number')
383
- match.messageId = metadata.searchMessageId;
396
+ if (messageId !== undefined)
397
+ match.messageId = messageId;
384
398
  if (metadata.parentSessionId)
385
399
  match.parentSessionId = metadata.parentSessionId;
400
+ citations.push(citation);
386
401
  return {
387
402
  id: session.id,
388
403
  name: session.name,
@@ -391,6 +406,7 @@ export class SessionToolExecutor {
391
406
  messageCount: Array.isArray(session.messages) ? session.messages.length : 0,
392
407
  createdAt: session.createdAt.toISOString(),
393
408
  lastAccessedAt: session.lastAccessedAt.toISOString(),
409
+ citation,
394
410
  match,
395
411
  };
396
412
  });
@@ -398,6 +414,7 @@ export class SessionToolExecutor {
398
414
  query,
399
415
  total: formatted.length,
400
416
  sessions: formatted,
417
+ citations,
401
418
  };
402
419
  return {
403
420
  success: true,
@@ -487,21 +487,35 @@ export class MCTS {
487
487
  findBestSolution() {
488
488
  if (!this.root)
489
489
  return null;
490
+ // Preferred tier: the best-scoring terminal-typed node (a concrete
491
+ // `implementation` or an explicit `conclusion`). Fallback tier: the
492
+ // best-scoring *derived* node of any type — many problems/models yield only
493
+ // analysis/verification/hypothesis thoughts, and a high-scoring one of those
494
+ // is still the best answer the search found. Without the fallback, such a
495
+ // search returns null even when a node scored 1.0. Both tiers exclude pruned
496
+ // nodes and the root (the problem statement, depth 0 — not a derived solution).
490
497
  let best = null;
491
498
  let bestScore = -1;
499
+ let fallback = null;
500
+ let fallbackScore = -1;
492
501
  const traverse = (node) => {
493
- if (node.state !== 'pruned' &&
494
- (node.type === 'implementation' || node.type === 'conclusion') &&
495
- node.score > bestScore) {
496
- bestScore = node.score;
497
- best = node;
502
+ if (node.state !== 'pruned' && node.depth > 0) {
503
+ if ((node.type === 'implementation' || node.type === 'conclusion') &&
504
+ node.score > bestScore) {
505
+ bestScore = node.score;
506
+ best = node;
507
+ }
508
+ if (node.score > fallbackScore) {
509
+ fallbackScore = node.score;
510
+ fallback = node;
511
+ }
498
512
  }
499
513
  for (const child of node.children) {
500
514
  traverse(child);
501
515
  }
502
516
  };
503
517
  traverse(this.root);
504
- return best;
518
+ return best ?? fallback;
505
519
  }
506
520
  /**
507
521
  * Get the path from root to a node
@@ -9,7 +9,10 @@
9
9
  * 2. reusable lesson candidates via the existing review-gated
10
10
  * auto-proposer (PENDING only — a human approves them into lessons.md,
11
11
  * which the per-turn `<lessons_context>` injection then re-injects on
12
- * future runs).
12
+ * future runs);
13
+ * 3. declarative long-term memory candidates via the review-gated memory
14
+ * queue. These do not enter prompt-injected persistent memory until a
15
+ * human accepts them.
13
16
  *
14
17
  * Guard-rails: trivial sessions are skipped (no LLM call, no file), and
15
18
  * everything written here passes the privacy lint first (PII/secret spans
@@ -32,6 +35,8 @@ export interface SessionEndFlushInput {
32
35
  export interface SessionEndFlushResult {
33
36
  /** Lesson candidates enqueued for review (`buddy lessons` to approve). */
34
37
  proposedLessons: number;
38
+ /** Declarative memory candidates enqueued for review (`/memory candidates`). */
39
+ proposedMemories: number;
35
40
  /** Absolute path of the handoff file, when one was (re)written. */
36
41
  handoffPath?: string;
37
42
  openRisks: string[];