@agentdeck/bridge 0.2.2 → 1.0.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 (435) hide show
  1. package/assets/fm-helper/agentdeck-fm-helper +0 -0
  2. package/assets/fonts/IBMPlexSans-Bold.ttf +0 -0
  3. package/assets/fonts/IBMPlexSans-Regular.ttf +0 -0
  4. package/assets/fonts/JetBrainsMono-Bold.ttf +0 -0
  5. package/assets/fonts/JetBrainsMono-Regular.ttf +0 -0
  6. package/assets/fonts/LICENSES.md +22 -0
  7. package/dist/adapters/claude-code.d.ts +6 -0
  8. package/dist/adapters/claude-code.d.ts.map +1 -1
  9. package/dist/adapters/claude-code.js +8 -0
  10. package/dist/adapters/claude-code.js.map +1 -1
  11. package/dist/adapters/openclaw.d.ts +24 -0
  12. package/dist/adapters/openclaw.d.ts.map +1 -1
  13. package/dist/adapters/openclaw.js +291 -35
  14. package/dist/adapters/openclaw.js.map +1 -1
  15. package/dist/adapters/opencode-adapter.d.ts +59 -0
  16. package/dist/adapters/opencode-adapter.d.ts.map +1 -1
  17. package/dist/adapters/opencode-adapter.js +297 -35
  18. package/dist/adapters/opencode-adapter.js.map +1 -1
  19. package/dist/adb-reverse.d.ts +1 -1
  20. package/dist/adb-reverse.js +1 -1
  21. package/dist/antigravity-local.d.ts +1 -0
  22. package/dist/antigravity-local.d.ts.map +1 -1
  23. package/dist/antigravity-local.js +33 -0
  24. package/dist/antigravity-local.js.map +1 -1
  25. package/dist/apme/adapters/claude-hook.d.ts.map +1 -1
  26. package/dist/apme/adapters/claude-hook.js +5 -0
  27. package/dist/apme/adapters/claude-hook.js.map +1 -1
  28. package/dist/apme/adapters/codex-hook.d.ts +17 -0
  29. package/dist/apme/adapters/codex-hook.d.ts.map +1 -0
  30. package/dist/apme/adapters/codex-hook.js +95 -0
  31. package/dist/apme/adapters/codex-hook.js.map +1 -0
  32. package/dist/apme/adapters/codex-turn-manager.d.ts +86 -0
  33. package/dist/apme/adapters/codex-turn-manager.d.ts.map +1 -0
  34. package/dist/apme/adapters/codex-turn-manager.js +496 -0
  35. package/dist/apme/adapters/codex-turn-manager.js.map +1 -0
  36. package/dist/apme/adapters/openclaw-hook.d.ts +99 -0
  37. package/dist/apme/adapters/openclaw-hook.d.ts.map +1 -0
  38. package/dist/apme/adapters/openclaw-hook.js +234 -0
  39. package/dist/apme/adapters/openclaw-hook.js.map +1 -0
  40. package/dist/apme/adapters/opencode-hook.d.ts +56 -0
  41. package/dist/apme/adapters/opencode-hook.d.ts.map +1 -0
  42. package/dist/apme/adapters/opencode-hook.js +182 -0
  43. package/dist/apme/adapters/opencode-hook.js.map +1 -0
  44. package/dist/apme/adapters/timeline.d.ts.map +1 -1
  45. package/dist/apme/adapters/timeline.js +6 -1
  46. package/dist/apme/adapters/timeline.js.map +1 -1
  47. package/dist/apme/antigravity-transcript.d.ts +18 -0
  48. package/dist/apme/antigravity-transcript.d.ts.map +1 -0
  49. package/dist/apme/antigravity-transcript.js +94 -0
  50. package/dist/apme/antigravity-transcript.js.map +1 -0
  51. package/dist/apme/classify-turn.d.ts +13 -0
  52. package/dist/apme/classify-turn.d.ts.map +1 -0
  53. package/dist/apme/classify-turn.js +49 -0
  54. package/dist/apme/classify-turn.js.map +1 -0
  55. package/dist/apme/claude-transcript-reader.d.ts +10 -0
  56. package/dist/apme/claude-transcript-reader.d.ts.map +1 -1
  57. package/dist/apme/claude-transcript-reader.js +35 -0
  58. package/dist/apme/claude-transcript-reader.js.map +1 -1
  59. package/dist/apme/collector.d.ts +133 -6
  60. package/dist/apme/collector.d.ts.map +1 -1
  61. package/dist/apme/collector.js +471 -23
  62. package/dist/apme/collector.js.map +1 -1
  63. package/dist/apme/dashboard-html.d.ts.map +1 -1
  64. package/dist/apme/dashboard-html.js +117 -11
  65. package/dist/apme/dashboard-html.js.map +1 -1
  66. package/dist/apme/http.d.ts.map +1 -1
  67. package/dist/apme/http.js +52 -6
  68. package/dist/apme/http.js.map +1 -1
  69. package/dist/apme/index.d.ts +26 -5
  70. package/dist/apme/index.d.ts.map +1 -1
  71. package/dist/apme/index.js +146 -9
  72. package/dist/apme/index.js.map +1 -1
  73. package/dist/apme/judge-detect.d.ts +26 -0
  74. package/dist/apme/judge-detect.d.ts.map +1 -0
  75. package/dist/apme/judge-detect.js +69 -0
  76. package/dist/apme/judge-detect.js.map +1 -0
  77. package/dist/apme/pareto.d.ts +39 -0
  78. package/dist/apme/pareto.d.ts.map +1 -0
  79. package/dist/apme/pareto.js +59 -0
  80. package/dist/apme/pareto.js.map +1 -0
  81. package/dist/apme/recommend.d.ts +3 -0
  82. package/dist/apme/recommend.d.ts.map +1 -1
  83. package/dist/apme/recommend.js +43 -1
  84. package/dist/apme/recommend.js.map +1 -1
  85. package/dist/apme/runner.d.ts +99 -4
  86. package/dist/apme/runner.d.ts.map +1 -1
  87. package/dist/apme/runner.js +743 -72
  88. package/dist/apme/runner.js.map +1 -1
  89. package/dist/apme/sample-to-timeline.d.ts +26 -0
  90. package/dist/apme/sample-to-timeline.d.ts.map +1 -0
  91. package/dist/apme/sample-to-timeline.js +95 -0
  92. package/dist/apme/sample-to-timeline.js.map +1 -0
  93. package/dist/apme/scorers/index.d.ts +47 -0
  94. package/dist/apme/scorers/index.d.ts.map +1 -0
  95. package/dist/apme/scorers/index.js +96 -0
  96. package/dist/apme/scorers/index.js.map +1 -0
  97. package/dist/apme/settings.d.ts +17 -11
  98. package/dist/apme/settings.d.ts.map +1 -1
  99. package/dist/apme/settings.js +30 -9
  100. package/dist/apme/settings.js.map +1 -1
  101. package/dist/apme/store.d.ts +33 -1
  102. package/dist/apme/store.d.ts.map +1 -1
  103. package/dist/apme/store.js +334 -25
  104. package/dist/apme/store.js.map +1 -1
  105. package/dist/awaiting-overlay.d.ts +112 -0
  106. package/dist/awaiting-overlay.d.ts.map +1 -0
  107. package/dist/awaiting-overlay.js +179 -0
  108. package/dist/awaiting-overlay.js.map +1 -0
  109. package/dist/ble-sync-spawn.d.ts +63 -0
  110. package/dist/ble-sync-spawn.d.ts.map +1 -0
  111. package/dist/ble-sync-spawn.js +196 -0
  112. package/dist/ble-sync-spawn.js.map +1 -0
  113. package/dist/bridge-core.d.ts +34 -1
  114. package/dist/bridge-core.d.ts.map +1 -1
  115. package/dist/bridge-core.js +207 -24
  116. package/dist/bridge-core.js.map +1 -1
  117. package/dist/broadcast.d.ts +3 -0
  118. package/dist/broadcast.d.ts.map +1 -0
  119. package/dist/broadcast.js +122 -0
  120. package/dist/broadcast.js.map +1 -0
  121. package/dist/check-deps.d.ts.map +1 -1
  122. package/dist/check-deps.js +25 -9
  123. package/dist/check-deps.js.map +1 -1
  124. package/dist/claude-permission-rules.d.ts +55 -0
  125. package/dist/claude-permission-rules.d.ts.map +1 -0
  126. package/dist/claude-permission-rules.js +197 -0
  127. package/dist/claude-permission-rules.js.map +1 -0
  128. package/dist/cli.d.ts +15 -1
  129. package/dist/cli.d.ts.map +1 -1
  130. package/dist/cli.js +1030 -93
  131. package/dist/cli.js.map +1 -1
  132. package/dist/codex-auth.d.ts +21 -0
  133. package/dist/codex-auth.d.ts.map +1 -1
  134. package/dist/codex-auth.js +52 -1
  135. package/dist/codex-auth.js.map +1 -1
  136. package/dist/codex-output-parser.d.ts +8 -0
  137. package/dist/codex-output-parser.d.ts.map +1 -1
  138. package/dist/codex-output-parser.js +110 -25
  139. package/dist/codex-output-parser.js.map +1 -1
  140. package/dist/codex-rate-limits.d.ts +16 -0
  141. package/dist/codex-rate-limits.d.ts.map +1 -0
  142. package/dist/codex-rate-limits.js +197 -0
  143. package/dist/codex-rate-limits.js.map +1 -0
  144. package/dist/codex-rollout-response.d.ts +31 -0
  145. package/dist/codex-rollout-response.d.ts.map +1 -0
  146. package/dist/codex-rollout-response.js +135 -0
  147. package/dist/codex-rollout-response.js.map +1 -0
  148. package/dist/d200h/image-renderer.d.ts +4 -16
  149. package/dist/d200h/image-renderer.d.ts.map +1 -1
  150. package/dist/d200h/image-renderer.js +46 -212
  151. package/dist/d200h/image-renderer.js.map +1 -1
  152. package/dist/daemon-server.d.ts +85 -0
  153. package/dist/daemon-server.d.ts.map +1 -1
  154. package/dist/daemon-server.js +1975 -159
  155. package/dist/daemon-server.js.map +1 -1
  156. package/dist/daemon.js +15 -6
  157. package/dist/daemon.js.map +1 -1
  158. package/dist/display-dim.d.ts +9 -0
  159. package/dist/display-dim.d.ts.map +1 -0
  160. package/dist/display-dim.js +53 -0
  161. package/dist/display-dim.js.map +1 -0
  162. package/dist/display-monitor.d.ts +15 -2
  163. package/dist/display-monitor.d.ts.map +1 -1
  164. package/dist/display-monitor.js +104 -26
  165. package/dist/display-monitor.js.map +1 -1
  166. package/dist/esp32-serial.d.ts +190 -1
  167. package/dist/esp32-serial.d.ts.map +1 -1
  168. package/dist/esp32-serial.js +1090 -156
  169. package/dist/esp32-serial.js.map +1 -1
  170. package/dist/fallback-task-timeline.d.ts +42 -0
  171. package/dist/fallback-task-timeline.d.ts.map +1 -0
  172. package/dist/fallback-task-timeline.js +119 -0
  173. package/dist/fallback-task-timeline.js.map +1 -0
  174. package/dist/foundation-models-helper.d.ts +11 -0
  175. package/dist/foundation-models-helper.d.ts.map +1 -0
  176. package/dist/foundation-models-helper.js +241 -0
  177. package/dist/foundation-models-helper.js.map +1 -0
  178. package/dist/hook-migration.js +1 -1
  179. package/dist/hook-migration.js.map +1 -1
  180. package/dist/hook-server.d.ts +2 -3
  181. package/dist/hook-server.d.ts.map +1 -1
  182. package/dist/hook-server.js +23 -10
  183. package/dist/hook-server.js.map +1 -1
  184. package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts +33 -0
  185. package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts.map +1 -0
  186. package/dist/idotmatrix/idotmatrix-daemon-sync.js +164 -0
  187. package/dist/idotmatrix/idotmatrix-daemon-sync.js.map +1 -0
  188. package/dist/idotmatrix/idotmatrix-discover.d.ts +18 -0
  189. package/dist/idotmatrix/idotmatrix-discover.d.ts.map +1 -0
  190. package/dist/idotmatrix/idotmatrix-discover.js +85 -0
  191. package/dist/idotmatrix/idotmatrix-discover.js.map +1 -0
  192. package/dist/idotmatrix/idotmatrix-settings.d.ts +20 -0
  193. package/dist/idotmatrix/idotmatrix-settings.d.ts.map +1 -0
  194. package/dist/idotmatrix/idotmatrix-settings.js +57 -0
  195. package/dist/idotmatrix/idotmatrix-settings.js.map +1 -0
  196. package/dist/index.d.ts.map +1 -1
  197. package/dist/index.js +217 -179
  198. package/dist/index.js.map +1 -1
  199. package/dist/log-stream.d.ts +15 -16
  200. package/dist/log-stream.d.ts.map +1 -1
  201. package/dist/log-stream.js +18 -96
  202. package/dist/log-stream.js.map +1 -1
  203. package/dist/logger.d.ts +3 -1
  204. package/dist/logger.d.ts.map +1 -1
  205. package/dist/logger.js +18 -5
  206. package/dist/logger.js.map +1 -1
  207. package/dist/mdns.d.ts +17 -0
  208. package/dist/mdns.d.ts.map +1 -1
  209. package/dist/mdns.js +52 -18
  210. package/dist/mdns.js.map +1 -1
  211. package/dist/model-catalog.d.ts +1 -1
  212. package/dist/model-catalog.d.ts.map +1 -1
  213. package/dist/model-catalog.js +2 -4
  214. package/dist/model-catalog.js.map +1 -1
  215. package/dist/modules/adb-module.d.ts +1 -1
  216. package/dist/modules/adb-module.d.ts.map +1 -1
  217. package/dist/modules/adb-module.js +4 -3
  218. package/dist/modules/adb-module.js.map +1 -1
  219. package/dist/modules/broadcast-module.d.ts +17 -0
  220. package/dist/modules/broadcast-module.d.ts.map +1 -0
  221. package/dist/modules/broadcast-module.js +26 -0
  222. package/dist/modules/broadcast-module.js.map +1 -0
  223. package/dist/modules/d200h-module.d.ts +22 -0
  224. package/dist/modules/d200h-module.d.ts.map +1 -1
  225. package/dist/modules/d200h-module.js +200 -65
  226. package/dist/modules/d200h-module.js.map +1 -1
  227. package/dist/modules/idotmatrix-module.d.ts +9 -0
  228. package/dist/modules/idotmatrix-module.d.ts.map +1 -0
  229. package/dist/modules/idotmatrix-module.js +46 -0
  230. package/dist/modules/idotmatrix-module.js.map +1 -0
  231. package/dist/modules/index.d.ts +3 -1
  232. package/dist/modules/index.d.ts.map +1 -1
  233. package/dist/modules/index.js +9 -3
  234. package/dist/modules/index.js.map +1 -1
  235. package/dist/modules/pixoo-module.d.ts.map +1 -1
  236. package/dist/modules/pixoo-module.js +14 -4
  237. package/dist/modules/pixoo-module.js.map +1 -1
  238. package/dist/modules/serial-module.d.ts +4 -0
  239. package/dist/modules/serial-module.d.ts.map +1 -1
  240. package/dist/modules/serial-module.js +14 -12
  241. package/dist/modules/serial-module.js.map +1 -1
  242. package/dist/modules/timebox-module.d.ts +9 -0
  243. package/dist/modules/timebox-module.d.ts.map +1 -0
  244. package/dist/modules/timebox-module.js +46 -0
  245. package/dist/modules/timebox-module.js.map +1 -0
  246. package/dist/modules/trmnl-module.d.ts +38 -0
  247. package/dist/modules/trmnl-module.d.ts.map +1 -0
  248. package/dist/modules/trmnl-module.js +111 -0
  249. package/dist/modules/trmnl-module.js.map +1 -0
  250. package/dist/modules/types.d.ts +4 -2
  251. package/dist/modules/types.d.ts.map +1 -1
  252. package/dist/observed-steering.d.ts +93 -0
  253. package/dist/observed-steering.d.ts.map +1 -0
  254. package/dist/observed-steering.js +247 -0
  255. package/dist/observed-steering.js.map +1 -0
  256. package/dist/openclaw-session.d.ts +18 -0
  257. package/dist/openclaw-session.d.ts.map +1 -0
  258. package/dist/openclaw-session.js +34 -0
  259. package/dist/openclaw-session.js.map +1 -0
  260. package/dist/opencode-steering.d.ts +36 -0
  261. package/dist/opencode-steering.d.ts.map +1 -0
  262. package/dist/opencode-steering.js +84 -0
  263. package/dist/opencode-steering.js.map +1 -0
  264. package/dist/output-parser.d.ts +30 -0
  265. package/dist/output-parser.d.ts.map +1 -1
  266. package/dist/output-parser.js +172 -18
  267. package/dist/output-parser.js.map +1 -1
  268. package/dist/passive-observer.d.ts +32 -0
  269. package/dist/passive-observer.d.ts.map +1 -1
  270. package/dist/passive-observer.js +288 -49
  271. package/dist/passive-observer.js.map +1 -1
  272. package/dist/permission-resolver.d.ts +55 -0
  273. package/dist/permission-resolver.d.ts.map +1 -0
  274. package/dist/permission-resolver.js +112 -0
  275. package/dist/permission-resolver.js.map +1 -0
  276. package/dist/pixoo/micro-glyphs.d.ts +18 -0
  277. package/dist/pixoo/micro-glyphs.d.ts.map +1 -0
  278. package/dist/pixoo/micro-glyphs.js +143 -0
  279. package/dist/pixoo/micro-glyphs.js.map +1 -0
  280. package/dist/pixoo/official-dot-glyphs.generated.d.ts +8 -0
  281. package/dist/pixoo/official-dot-glyphs.generated.d.ts.map +1 -0
  282. package/dist/pixoo/official-dot-glyphs.generated.js +247 -0
  283. package/dist/pixoo/official-dot-glyphs.generated.js.map +1 -0
  284. package/dist/pixoo/pixoo-bridge.d.ts +16 -8
  285. package/dist/pixoo/pixoo-bridge.d.ts.map +1 -1
  286. package/dist/pixoo/pixoo-bridge.js +127 -35
  287. package/dist/pixoo/pixoo-bridge.js.map +1 -1
  288. package/dist/pixoo/pixoo-camera.d.ts +19 -3
  289. package/dist/pixoo/pixoo-camera.d.ts.map +1 -1
  290. package/dist/pixoo/pixoo-camera.js +62 -27
  291. package/dist/pixoo/pixoo-camera.js.map +1 -1
  292. package/dist/pixoo/pixoo-client.d.ts +13 -3
  293. package/dist/pixoo/pixoo-client.d.ts.map +1 -1
  294. package/dist/pixoo/pixoo-client.js +51 -22
  295. package/dist/pixoo/pixoo-client.js.map +1 -1
  296. package/dist/pixoo/pixoo-discover.d.ts +37 -0
  297. package/dist/pixoo/pixoo-discover.d.ts.map +1 -0
  298. package/dist/pixoo/pixoo-discover.js +105 -0
  299. package/dist/pixoo/pixoo-discover.js.map +1 -0
  300. package/dist/pixoo/pixoo-renderer.d.ts +27 -3
  301. package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
  302. package/dist/pixoo/pixoo-renderer.js +533 -135
  303. package/dist/pixoo/pixoo-renderer.js.map +1 -1
  304. package/dist/pixoo/pixoo-settings.d.ts +6 -0
  305. package/dist/pixoo/pixoo-settings.d.ts.map +1 -1
  306. package/dist/pixoo/pixoo-settings.js +9 -0
  307. package/dist/pixoo/pixoo-settings.js.map +1 -1
  308. package/dist/pixoo/pixoo-sprites.d.ts +84 -11
  309. package/dist/pixoo/pixoo-sprites.d.ts.map +1 -1
  310. package/dist/pixoo/pixoo-sprites.js +620 -209
  311. package/dist/pixoo/pixoo-sprites.js.map +1 -1
  312. package/dist/pty-manager.d.ts.map +1 -1
  313. package/dist/pty-manager.js +8 -2
  314. package/dist/pty-manager.js.map +1 -1
  315. package/dist/review-runner.d.ts +113 -0
  316. package/dist/review-runner.d.ts.map +1 -0
  317. package/dist/review-runner.js +428 -0
  318. package/dist/review-runner.js.map +1 -0
  319. package/dist/session-activity.d.ts +14 -0
  320. package/dist/session-activity.d.ts.map +1 -0
  321. package/dist/session-activity.js +160 -0
  322. package/dist/session-activity.js.map +1 -0
  323. package/dist/session-aggregator.d.ts +25 -0
  324. package/dist/session-aggregator.d.ts.map +1 -1
  325. package/dist/session-aggregator.js +48 -9
  326. package/dist/session-aggregator.js.map +1 -1
  327. package/dist/session-focus-relay.d.ts.map +1 -1
  328. package/dist/session-focus-relay.js +7 -1
  329. package/dist/session-focus-relay.js.map +1 -1
  330. package/dist/session-registry.d.ts +74 -3
  331. package/dist/session-registry.d.ts.map +1 -1
  332. package/dist/session-registry.js +140 -8
  333. package/dist/session-registry.js.map +1 -1
  334. package/dist/session-timeline-relay.d.ts.map +1 -1
  335. package/dist/session-timeline-relay.js +13 -4
  336. package/dist/session-timeline-relay.js.map +1 -1
  337. package/dist/session-transcript-timeline.d.ts +49 -0
  338. package/dist/session-transcript-timeline.d.ts.map +1 -0
  339. package/dist/session-transcript-timeline.js +276 -0
  340. package/dist/session-transcript-timeline.js.map +1 -0
  341. package/dist/state-machine.d.ts +6 -1
  342. package/dist/state-machine.d.ts.map +1 -1
  343. package/dist/state-machine.js +86 -28
  344. package/dist/state-machine.js.map +1 -1
  345. package/dist/timebox/timebox-daemon-sync.d.ts +19 -0
  346. package/dist/timebox/timebox-daemon-sync.d.ts.map +1 -0
  347. package/dist/timebox/timebox-daemon-sync.js +135 -0
  348. package/dist/timebox/timebox-daemon-sync.js.map +1 -0
  349. package/dist/timebox/timebox-discover.d.ts +19 -0
  350. package/dist/timebox/timebox-discover.d.ts.map +1 -0
  351. package/dist/timebox/timebox-discover.js +87 -0
  352. package/dist/timebox/timebox-discover.js.map +1 -0
  353. package/dist/timebox/timebox-settings.d.ts +32 -0
  354. package/dist/timebox/timebox-settings.d.ts.map +1 -0
  355. package/dist/timebox/timebox-settings.js +80 -0
  356. package/dist/timebox/timebox-settings.js.map +1 -0
  357. package/dist/timeline-store.d.ts +112 -3
  358. package/dist/timeline-store.d.ts.map +1 -1
  359. package/dist/timeline-store.js +460 -14
  360. package/dist/timeline-store.js.map +1 -1
  361. package/dist/timeline-summarizer.d.ts.map +1 -1
  362. package/dist/timeline-summarizer.js +29 -5
  363. package/dist/timeline-summarizer.js.map +1 -1
  364. package/dist/trmnl/byos-server.d.ts +39 -0
  365. package/dist/trmnl/byos-server.d.ts.map +1 -0
  366. package/dist/trmnl/byos-server.js +301 -0
  367. package/dist/trmnl/byos-server.js.map +1 -0
  368. package/dist/trmnl/frame-cache.d.ts +37 -0
  369. package/dist/trmnl/frame-cache.d.ts.map +1 -0
  370. package/dist/trmnl/frame-cache.js +157 -0
  371. package/dist/trmnl/frame-cache.js.map +1 -0
  372. package/dist/trmnl/image-renderer.d.ts +27 -0
  373. package/dist/trmnl/image-renderer.d.ts.map +1 -0
  374. package/dist/trmnl/image-renderer.js +235 -0
  375. package/dist/trmnl/image-renderer.js.map +1 -0
  376. package/dist/trmnl/trmnl-settings.d.ts +96 -0
  377. package/dist/trmnl/trmnl-settings.d.ts.map +1 -0
  378. package/dist/trmnl/trmnl-settings.js +177 -0
  379. package/dist/trmnl/trmnl-settings.js.map +1 -0
  380. package/dist/trmnl/trmnl-telemetry.d.ts +47 -0
  381. package/dist/trmnl/trmnl-telemetry.d.ts.map +1 -0
  382. package/dist/trmnl/trmnl-telemetry.js +56 -0
  383. package/dist/trmnl/trmnl-telemetry.js.map +1 -0
  384. package/dist/tui/ansi.d.ts +0 -4
  385. package/dist/tui/ansi.d.ts.map +1 -1
  386. package/dist/tui/ansi.js +14 -7
  387. package/dist/tui/ansi.js.map +1 -1
  388. package/dist/tui/dashboard.d.ts +1 -0
  389. package/dist/tui/dashboard.d.ts.map +1 -1
  390. package/dist/tui/dashboard.js +38 -11
  391. package/dist/tui/dashboard.js.map +1 -1
  392. package/dist/tui/renderer.d.ts +36 -0
  393. package/dist/tui/renderer.d.ts.map +1 -1
  394. package/dist/tui/renderer.js +355 -121
  395. package/dist/tui/renderer.js.map +1 -1
  396. package/dist/tui/terrarium.d.ts.map +1 -1
  397. package/dist/tui/terrarium.js +44 -51
  398. package/dist/tui/terrarium.js.map +1 -1
  399. package/dist/types.d.ts +2 -2
  400. package/dist/types.d.ts.map +1 -1
  401. package/dist/types.js +2 -2
  402. package/dist/types.js.map +1 -1
  403. package/dist/usage-api.d.ts.map +1 -1
  404. package/dist/usage-api.js +7 -1
  405. package/dist/usage-api.js.map +1 -1
  406. package/dist/usage-event.d.ts +10 -3
  407. package/dist/usage-event.d.ts.map +1 -1
  408. package/dist/usage-event.js +98 -13
  409. package/dist/usage-event.js.map +1 -1
  410. package/dist/utility-proxy.d.ts.map +1 -1
  411. package/dist/utility-proxy.js +8 -0
  412. package/dist/utility-proxy.js.map +1 -1
  413. package/dist/utils/project-name.d.ts +3 -0
  414. package/dist/utils/project-name.d.ts.map +1 -1
  415. package/dist/utils/project-name.js +44 -2
  416. package/dist/utils/project-name.js.map +1 -1
  417. package/dist/version-check.d.ts.map +1 -1
  418. package/dist/version-check.js +1 -17
  419. package/dist/version-check.js.map +1 -1
  420. package/dist/windows-service.d.ts +26 -0
  421. package/dist/windows-service.d.ts.map +1 -0
  422. package/dist/windows-service.js +145 -0
  423. package/dist/windows-service.js.map +1 -0
  424. package/dist/ws-server.d.ts +17 -3
  425. package/dist/ws-server.d.ts.map +1 -1
  426. package/dist/ws-server.js +78 -8
  427. package/dist/ws-server.js.map +1 -1
  428. package/fm-helper/AgentDeckFMHelper.swift +112 -0
  429. package/package.json +10 -4
  430. package/scripts/build-fm-helper.mjs +26 -0
  431. package/LICENSE +0 -21
  432. package/dist/apme/tuner.d.ts +0 -64
  433. package/dist/apme/tuner.d.ts.map +0 -1
  434. package/dist/apme/tuner.js +0 -297
  435. package/dist/apme/tuner.js.map +0 -1
@@ -6,16 +6,34 @@
6
6
  * 0/1 in `evals` (metrics: lint_clean, build_ok, tests_pass).
7
7
  *
8
8
  * Layer 2 (llm_judge): G-Eval style rubric against the latest `rubrics` row.
9
- * Backend is pluggable — default is local MLX (cost-free), API/OpenClaw are
10
- * opt-in via `~/.agentdeck/settings.json`. Gated by `shouldJudge()` so the
11
- * common "clear pass" case skips layer 2 entirely.
9
+ * Backend is pluggable — default is Foundation Models via the Swift daemon or
10
+ * bundled CLI Swift helper, with local MLX fallback for CLI-only runs. Gated
11
+ * by `shouldJudge()` so the common "clear pass" case skips layer 2 entirely.
12
12
  */
13
13
  import { spawn } from 'child_process';
14
14
  import { existsSync, readdirSync } from 'fs';
15
15
  import { debug } from '../logger.js';
16
- import { loadApmeConfig, shouldJudge } from './settings.js';
16
+ import { loadApmeConfig, shouldJudge, DEFAULT_APME_CONFIG } from './settings.js';
17
17
  import { loadMlxSettings, mlxChatUrl } from '@agentdeck/shared';
18
+ import { callFoundationModelsHelper, probeFoundationModelsHelper } from '../foundation-models-helper.js';
19
+ import { runSampleScorers } from './scorers/index.js';
18
20
  import { execSync } from 'child_process';
21
+ function deriveTaskOutcome(score) {
22
+ if (score == null)
23
+ return 'pending';
24
+ if (score >= 0.75)
25
+ return 'success';
26
+ if (score >= 0.5)
27
+ return 'partial';
28
+ return 'fail';
29
+ }
30
+ /** Narrow an arbitrary DB `outcome` string to the typed union, so a row
31
+ * written via the manual path (`closeTaskExternal` outcome override) can
32
+ * flow back into `TaskEvaluatedEvent` without losing type-safety. */
33
+ function isPreservableOutcome(value) {
34
+ return value === 'success' || value === 'partial' || value === 'fail'
35
+ || value === 'pending' || value === 'abandoned';
36
+ }
19
37
  const DEFAULT_COMMANDS = {
20
38
  typescript: {
21
39
  lint: 'pnpm -w lint',
@@ -38,13 +56,27 @@ export class ApmeRunner {
38
56
  store;
39
57
  queue = [];
40
58
  drainPromise = null;
59
+ queuedRunIds = new Set();
60
+ runningRunIds = new Set();
61
+ runningTurnIds = new Set();
62
+ runningTaskIds = new Set();
41
63
  listeners = new Set();
64
+ taskListeners = new Set();
42
65
  configOverride = null;
43
66
  judgeOverride = null;
44
67
  detOverride = null;
68
+ /** Cached startup judge readiness probe. Populated by `refreshBackendProbe`,
69
+ * surfaced on /health. Null until the first probe completes. */
70
+ lastBackendProbe = null;
45
71
  constructor(store) {
46
72
  this.store = store;
47
73
  }
74
+ /** Probe the configured judge backend and cache the result. Safe to call
75
+ * fire-and-forget at daemon startup — failures don't throw. */
76
+ async refreshBackendProbe(cfg) {
77
+ this.lastBackendProbe = await probeJudgeBackend(cfg);
78
+ return this.lastBackendProbe;
79
+ }
48
80
  _setConfig(cfg) { this.configOverride = cfg; }
49
81
  _setJudgeFn(fn) {
50
82
  this.judgeOverride = fn;
@@ -56,9 +88,23 @@ export class ApmeRunner {
56
88
  this.listeners.add(fn);
57
89
  return () => this.listeners.delete(fn);
58
90
  }
91
+ /** Subscribe to per-task judge completions. The event fires AFTER the
92
+ * task_judge axis scores and summary are persisted, so listeners can read
93
+ * the final state directly from the event payload without re-querying.
94
+ * Used by the timeline emitter to upsert the corresponding `task_end` row
95
+ * with score + outcome metadata. */
96
+ onTaskEvaluated(fn) {
97
+ this.taskListeners.add(fn);
98
+ return () => this.taskListeners.delete(fn);
99
+ }
59
100
  enqueue(job) {
60
101
  if (!this.store.enabled)
61
102
  return;
103
+ if (this.queuedRunIds.has(job.runId) || this.runningRunIds.has(job.runId)) {
104
+ debug('APME', `skip duplicate eval enqueue runId=${job.runId}`);
105
+ return;
106
+ }
107
+ this.queuedRunIds.add(job.runId);
62
108
  this.queue.push(job);
63
109
  debug('APME', `enqueue eval runId=${job.runId} (queue=${this.queue.length})`);
64
110
  void this.drain();
@@ -69,7 +115,14 @@ export class ApmeRunner {
69
115
  enqueueTurn(job) {
70
116
  if (!this.store.enabled)
71
117
  return;
72
- void this.runTurnEval(job);
118
+ if (this.runningTurnIds.has(job.turnId)) {
119
+ debug('APME', `skip duplicate turn eval turnId=${job.turnId}`);
120
+ return;
121
+ }
122
+ this.runningTurnIds.add(job.turnId);
123
+ void this.runTurnEval(job).finally(() => {
124
+ this.runningTurnIds.delete(job.turnId);
125
+ });
73
126
  }
74
127
  /** Judge a closed task (group of turns between boundary signals —
75
128
  * TodoWrite all-completed, /clear, session_end). Fires-and-forgets;
@@ -77,7 +130,14 @@ export class ApmeRunner {
77
130
  enqueueTask(job) {
78
131
  if (!this.store.enabled)
79
132
  return;
80
- void this.runTaskEval(job);
133
+ if (this.runningTaskIds.has(job.taskId)) {
134
+ debug('APME', `skip duplicate task eval taskId=${job.taskId}`);
135
+ return;
136
+ }
137
+ this.runningTaskIds.add(job.taskId);
138
+ void this.runTaskEval(job).finally(() => {
139
+ this.runningTaskIds.delete(job.taskId);
140
+ });
81
141
  }
82
142
  async runTaskEval({ runId, taskId, category, boundarySignal }) {
83
143
  const cfg = this.configOverride ?? loadApmeConfig();
@@ -121,6 +181,13 @@ export class ApmeRunner {
121
181
  if (turns.length > TURN_CAP) {
122
182
  lines.push(`… (${turns.length - TURN_CAP} more turns omitted)`);
123
183
  }
184
+ // The canonical SessionSample gives the judge the typed tool trajectory
185
+ // (req #5/#6) and per-sample cost (req #7) — context the turn rows lack.
186
+ const sample = this.store.getSample(taskId);
187
+ const trajectoryLines = sample ? buildTrajectoryLines(sample) : [];
188
+ const costLine = sample
189
+ ? `cost: ${sample.cost.inputTokens}in/${sample.cost.outputTokens}out tok, $${sample.cost.costUsd.toFixed(4)}, model ${sample.model.modelId}`
190
+ : '';
124
191
  const judgePrompt = [
125
192
  rubric.prompt,
126
193
  '',
@@ -128,23 +195,28 @@ export class ApmeRunner {
128
195
  `task_category: ${category ?? task.taskCategory ?? 'unknown'}`,
129
196
  `turn_count: ${turns.length}`,
130
197
  `boundary_signal: ${boundarySignal ?? task.boundarySignal}`,
198
+ ...(costLine ? [costLine] : []),
131
199
  '',
132
200
  '--- TURNS ---',
133
201
  ...lines,
202
+ ...(trajectoryLines.length ? ['', '--- TOOL TRAJECTORY ---', ...trajectoryLines] : []),
134
203
  '',
135
204
  'Respond with strict JSON only.',
136
205
  ].join('\n');
137
206
  try {
138
- const judgeText = this.judgeOverride
139
- ? await this.judgeOverride(judgePrompt, cfg.judge)
140
- : await callJudge(judgePrompt, cfg.judge);
141
- const parsed = parseJudgeJson(judgeText);
207
+ // callJudgeWithMeta carries the effective backend label across the
208
+ // FM→MLX fallback path. Without it, eval rows produced by the MLX
209
+ // fallback would be misattributed to foundationModels in the DB.
210
+ const judgeResult = this.judgeOverride
211
+ ? { text: await this.judgeOverride(judgePrompt, cfg.judge), effectiveLabel: effectiveJudgeModelTag(cfg.judge) }
212
+ : await callJudgeWithMeta(judgePrompt, cfg.judge);
213
+ const parsed = parseJudgeJson(judgeResult.text);
142
214
  if (!parsed) {
143
215
  debug('APME', `runTaskEval parse failed task=${taskId.slice(0, 8)}`);
144
216
  return;
145
217
  }
146
218
  const now = Date.now();
147
- const judgeModel = effectiveJudgeModelTag(cfg.judge);
219
+ const judgeModel = judgeResult.effectiveLabel;
148
220
  for (const [axis, score] of Object.entries(parsed.scores)) {
149
221
  this.store.insertEvalForTask({
150
222
  id: 0,
@@ -165,22 +237,85 @@ export class ApmeRunner {
165
237
  createdAt: now,
166
238
  });
167
239
  }
240
+ // Pure sample-trajectory scorers (tool churn, error rate) — they add
241
+ // signal the LLM judge can miss and are cheap/deterministic. Stored under
242
+ // layer='trajectory' so they don't collide with task_judge axes.
243
+ if (sample) {
244
+ try {
245
+ for (const r of runSampleScorers(sample)) {
246
+ this.store.insertEvalForTask({
247
+ id: 0, runId, taskId,
248
+ layer: r.layer,
249
+ metric: r.metric,
250
+ score: r.score,
251
+ raw: r.reasoning ? JSON.stringify({ reasoning: r.reasoning, scorer: r.scorer }) : null,
252
+ rubricVer: null,
253
+ judgeModel: `scorer:${r.scorer}`,
254
+ createdAt: now,
255
+ });
256
+ }
257
+ }
258
+ catch (err) {
259
+ debug('APME', `sample scorers failed task=${taskId.slice(0, 8)}: ${String(err)}`);
260
+ }
261
+ }
262
+ const compositeScore = parsed.scores.overall ?? null;
263
+ const derivedOutcome = deriveTaskOutcome(compositeScore);
264
+ // Preserve a previously-set outcome — that only happens when the user
265
+ // explicitly closed the task via `agentdeck task done/cancel` (or the
266
+ // macOS detail-pane button) with an outcome override. Without this
267
+ // guard the async judge resolves 5–30 s after the manual close and
268
+ // overwrites e.g. `abandoned` with `partial`, silently losing the
269
+ // user's gesture. `closeTask` itself never writes outcome — only the
270
+ // manual path does — so a non-null read here is unambiguous.
271
+ const existingOutcome = this.store.getTask(taskId)?.outcome ?? null;
272
+ const taskOutcome = isPreservableOutcome(existingOutcome)
273
+ ? existingOutcome
274
+ : derivedOutcome;
168
275
  this.store.updateTask(taskId, {
169
276
  summary: parsed.summary ?? null,
170
- compositeScore: parsed.scores.overall ?? null,
277
+ compositeScore,
278
+ outcome: taskOutcome,
171
279
  notesJson: JSON.stringify({
172
280
  reasoning: parsed.reasoning,
173
281
  done: parsed.done,
174
282
  missed: parsed.missed,
175
283
  }),
176
284
  });
177
- debug('APME', `task eval ${taskId.slice(0, 8)}: overall=${parsed.scores.overall} summary=${parsed.summary?.slice(0, 40) ?? '-'}`);
285
+ debug('APME', `task eval ${taskId.slice(0, 8)}: overall=${compositeScore} outcome=${taskOutcome} summary=${parsed.summary?.slice(0, 40) ?? '-'}`);
178
286
  for (const fn of this.listeners) {
179
287
  try {
180
- fn({ runId, taskId, layer1Ran: false, layer2Ran: true, overall: parsed.scores.overall });
288
+ fn({ runId, taskId, layer1Ran: false, layer2Ran: true, overall: compositeScore ?? undefined });
181
289
  }
182
290
  catch { /* ignore */ }
183
291
  }
292
+ if (this.taskListeners.size > 0) {
293
+ const run = this.store.getRun(runId);
294
+ const updatedTask = this.store.getTask(taskId) ?? task;
295
+ const event = {
296
+ runId,
297
+ taskId,
298
+ sessionId: run?.sessionId ?? '',
299
+ agentType: run?.agentType ?? undefined,
300
+ projectName: run?.projectName ?? undefined,
301
+ startedAt: updatedTask.startedAt,
302
+ endedAt: updatedTask.endedAt ?? Date.now(),
303
+ compositeScore,
304
+ outcome: taskOutcome,
305
+ taskCategory: category ?? updatedTask.taskCategory ?? undefined,
306
+ summary: parsed.summary ?? undefined,
307
+ boundarySignal: boundarySignal ?? updatedTask.boundarySignal,
308
+ turns: updatedTask.firstTurnIndex != null
309
+ ? (updatedTask.lastTurnIndex ?? updatedTask.firstTurnIndex) - updatedTask.firstTurnIndex + 1
310
+ : undefined,
311
+ };
312
+ for (const fn of this.taskListeners) {
313
+ try {
314
+ fn(event);
315
+ }
316
+ catch { /* ignore */ }
317
+ }
318
+ }
184
319
  }
185
320
  catch (err) {
186
321
  debug('APME', `task eval error taskId=${taskId.slice(0, 8)}: ${String(err)}`);
@@ -226,14 +361,15 @@ export class ApmeRunner {
226
361
  'Respond with strict JSON only.',
227
362
  ].join('\n');
228
363
  try {
229
- const judgeText = this.judgeOverride
230
- ? await this.judgeOverride(judgePrompt, cfg.judge)
231
- : await callJudge(judgePrompt, cfg.judge);
232
- const parsed = parseJudgeJson(judgeText);
364
+ // Same fallback-aware labelling rule as runTaskEval — see comment there.
365
+ const judgeResult = this.judgeOverride
366
+ ? { text: await this.judgeOverride(judgePrompt, cfg.judge), effectiveLabel: effectiveJudgeModelTag(cfg.judge) }
367
+ : await callJudgeWithMeta(judgePrompt, cfg.judge);
368
+ const parsed = parseJudgeJson(judgeResult.text);
233
369
  if (!parsed)
234
370
  return;
235
371
  const now = Date.now();
236
- const judgeModel = effectiveJudgeModelTag(cfg.judge);
372
+ const judgeModel = judgeResult.effectiveLabel;
237
373
  for (const [axis, score] of Object.entries(parsed.scores)) {
238
374
  this.store.insertEvalForTurn({
239
375
  runId, turnId,
@@ -274,8 +410,14 @@ export class ApmeRunner {
274
410
  async doDrain() {
275
411
  while (this.queue.length > 0) {
276
412
  const job = this.queue.shift();
413
+ this.queuedRunIds.delete(job.runId);
414
+ this.runningRunIds.add(job.runId);
277
415
  try {
278
416
  const result = await this.runOne(job);
417
+ if (!result.layer1Ran && !result.layer2Ran && result.overall === undefined) {
418
+ debug('APME', `runner no-op runId=${job.runId} (no eval rows produced)`);
419
+ continue;
420
+ }
279
421
  for (const fn of this.listeners) {
280
422
  try {
281
423
  fn(result);
@@ -286,6 +428,9 @@ export class ApmeRunner {
286
428
  catch (err) {
287
429
  debug('APME', `runner error runId=${job.runId}: ${String(err)}`);
288
430
  }
431
+ finally {
432
+ this.runningRunIds.delete(job.runId);
433
+ }
289
434
  }
290
435
  }
291
436
  async runOne(job) {
@@ -295,6 +440,10 @@ export class ApmeRunner {
295
440
  debug('APME', `runOne: run ${job.runId} not found`);
296
441
  return { runId: job.runId, layer1Ran: false, layer2Ran: false };
297
442
  }
443
+ if (this.store.listEvalsForRun(run.id).length > 0) {
444
+ debug('APME', `runOne: run ${run.id} already has eval rows; skip duplicate`);
445
+ return { runId: job.runId, layer1Ran: false, layer2Ran: false };
446
+ }
298
447
  // ── Layer 1 — deterministic ───────────────────────────────────────────────
299
448
  let layer1Ran = false;
300
449
  let layer1Passed = null;
@@ -339,13 +488,14 @@ export class ApmeRunner {
339
488
  if (rubric) {
340
489
  try {
341
490
  const prompt = buildJudgePrompt(run, rubric.prompt, layer1Passed, this.store);
342
- const judgeText = this.judgeOverride
343
- ? await this.judgeOverride(prompt, cfg.judge)
344
- : await callJudge(prompt, cfg.judge);
345
- const parsed = parseJudgeJson(judgeText);
491
+ // Same fallback-aware labelling rule as runTaskEval — see comment there.
492
+ const judgeResult = this.judgeOverride
493
+ ? { text: await this.judgeOverride(prompt, cfg.judge), effectiveLabel: effectiveJudgeModelTag(cfg.judge) }
494
+ : await callJudgeWithMeta(prompt, cfg.judge);
495
+ const parsed = parseJudgeJson(judgeResult.text);
346
496
  if (parsed) {
347
497
  const now = Date.now();
348
- const judgeModel = effectiveJudgeModelTag(cfg.judge);
498
+ const judgeModel = judgeResult.effectiveLabel;
349
499
  for (const [axis, score] of Object.entries(parsed.scores)) {
350
500
  this.store.insertEval({
351
501
  runId: run.id,
@@ -530,6 +680,38 @@ function runCommand(command, cwd, timeoutMs) {
530
680
  });
531
681
  }
532
682
  // ─── Layer 2 execution ────────────────────────────────────────────────────────
683
+ /** Render a SessionSample's typed trajectory as compact judge-prompt lines.
684
+ * Gives the judge tool-call sequences + model usage the turn rows omit. */
685
+ export function buildTrajectoryLines(sample, cap = 30) {
686
+ const lines = [];
687
+ const events = sample.events.slice(0, cap);
688
+ for (const e of events) {
689
+ switch (e.kind) {
690
+ case 'tool': {
691
+ let input = '';
692
+ try {
693
+ input = e.input == null ? '' : JSON.stringify(e.input).slice(0, 120);
694
+ }
695
+ catch {
696
+ input = '';
697
+ }
698
+ lines.push(` tool ${e.name}(${input})${e.status ? ` → ${e.status}` : ''}${e.error ? ` [err: ${String(e.error).slice(0, 80)}]` : ''}`);
699
+ break;
700
+ }
701
+ case 'model':
702
+ lines.push(` model ${e.model}: ${e.inputTokens}in/${e.outputTokens}out tok${e.costUsd ? ` ($${e.costUsd.toFixed(4)})` : ''}`);
703
+ break;
704
+ case 'state':
705
+ lines.push(` state → ${e.to}`);
706
+ break;
707
+ default:
708
+ break; // user/assistant messages already shown in the TURNS section
709
+ }
710
+ }
711
+ if (sample.events.length > cap)
712
+ lines.push(` … (${sample.events.length - cap} more events)`);
713
+ return lines;
714
+ }
533
715
  // `ParsedJudge` is the canonical eval v1 type imported from `@agentdeck/shared`.
534
716
  const NON_CODE_CATEGORIES = new Set(['conversation', 'planning', 'research', 'review']);
535
717
  export function buildJudgePrompt(run, rubricPrompt, layer1Passed, store) {
@@ -619,29 +801,351 @@ export function effectiveJudgeModelTag(cfg) {
619
801
  return 'foundationModels:apple-intelligence';
620
802
  return `${cfg.backend}:${cfg.model}`;
621
803
  }
622
- export async function callJudge(prompt, judgeCfg) {
623
- if (judgeCfg.backend === 'mlx')
624
- return callMlx(prompt, judgeCfg);
625
- if (judgeCfg.backend === 'openclaw')
626
- return callOpenClaw(prompt, judgeCfg);
627
- if (judgeCfg.backend === 'api')
628
- return callApi(prompt, judgeCfg);
804
+ /** Strip backend-specific fields when forcing a cfg through a different
805
+ * adapter. Without this, a FM cfg (`endpoint:"http://.../apme/judge/foundation-models"`,
806
+ * `model:"apple-intelligence"`) handed to `callMlx` would POST to the FM URL
807
+ * and request a model the MLX server has never heard of — silent failure.
808
+ * Mirrors the `resetBackendCoupledFields` path inside `loadApmeConfig`. */
809
+ export function sanitizeForMlx(judgeCfg) {
810
+ if (judgeCfg.backend === 'mlx' && !judgeCfg.endpoint && (!judgeCfg.model || judgeCfg.model === DEFAULT_APME_CONFIG.judge.model)) {
811
+ return judgeCfg;
812
+ }
813
+ return {
814
+ ...judgeCfg,
815
+ backend: 'mlx',
816
+ endpoint: undefined,
817
+ model: DEFAULT_APME_CONFIG.judge.model,
818
+ };
819
+ }
820
+ const FOUNDATION_MODELS_MISSING_DAEMON_TTL_MS = 15_000;
821
+ const FOUNDATION_MODELS_UNAVAILABLE_TTL_MS = 60_000;
822
+ const FOUNDATION_MODELS_READY_TTL_MS = 60_000;
823
+ let foundationModelsAutoCache = null;
824
+ let foundationModelsResolveInFlight = null;
825
+ function getFoundationModelsAutoCache(now = Date.now()) {
826
+ if (!foundationModelsAutoCache)
827
+ return null;
828
+ if (foundationModelsAutoCache.expiresAt <= now) {
829
+ foundationModelsAutoCache = null;
830
+ return null;
831
+ }
832
+ return foundationModelsAutoCache;
833
+ }
834
+ function markFoundationModelsAutoReady(url) {
835
+ foundationModelsAutoCache = {
836
+ state: 'ready',
837
+ url,
838
+ expiresAt: Date.now() + FOUNDATION_MODELS_READY_TTL_MS,
839
+ };
840
+ }
841
+ function markFoundationModelsAutoUnavailable(reason, ttlMs = FOUNDATION_MODELS_UNAVAILABLE_TTL_MS) {
842
+ foundationModelsAutoCache = {
843
+ state: 'unavailable',
844
+ reason,
845
+ expiresAt: Date.now() + ttlMs,
846
+ };
847
+ }
848
+ /** Test hook for cache-sensitive runner tests. Production code should not call this. */
849
+ export function clearFoundationModelsAutoCacheForTests() {
850
+ foundationModelsAutoCache = null;
851
+ foundationModelsResolveInFlight = null;
852
+ }
853
+ /** Like `callJudge`, but returns the effective backend + label so callers
854
+ * can record `judge_model` correctly across fallback paths. */
855
+ export async function callJudgeWithMeta(prompt, judgeCfg) {
629
856
  if (judgeCfg.backend === 'foundationModels') {
630
857
  try {
631
- return await callFoundationModels(prompt, judgeCfg);
858
+ const text = await callFoundationModels(prompt, judgeCfg);
859
+ return { text, effectiveBackend: 'foundationModels', effectiveLabel: effectiveJudgeModelTag(judgeCfg) };
632
860
  }
633
861
  catch (err) {
634
- // Cost-sensitive default: do not silently route to a network backend.
635
- // Only retry via MLX when the user explicitly opts in — otherwise
862
+ // Cost-sensitive default: never route to a paid/network backend.
863
+ // Retry via local MLX only when fallbackToMlx is enabled; otherwise
636
864
  // propagate the error so the runner's try/catch skips this eval.
637
865
  if (judgeCfg.fallbackToMlx) {
638
866
  debug('APME', `foundationModels unavailable, fallback to MLX: ${String(err)}`);
639
- return callMlx(prompt, judgeCfg);
867
+ // sanitizeForMlx wipes the FM-specific endpoint/model so callMlx
868
+ // never POSTs to the FM endpoint or asks MLX for `apple-intelligence`.
869
+ // We then derive the label from the SANITIZED cfg — recording the
870
+ // effective backend, not the cfg the user originally requested.
871
+ const mlxCfg = sanitizeForMlx(judgeCfg);
872
+ const text = await callMlx(prompt, mlxCfg);
873
+ return { text, effectiveBackend: 'mlx', effectiveLabel: effectiveJudgeModelTag(mlxCfg) };
640
874
  }
641
875
  throw err;
642
876
  }
643
877
  }
644
- throw new Error(`unknown judge backend: ${String(judgeCfg.backend)}`);
878
+ let text;
879
+ if (judgeCfg.backend === 'mlx')
880
+ text = await callMlx(prompt, judgeCfg);
881
+ else if (judgeCfg.backend === 'openai')
882
+ text = await callOpenAICompatible(prompt, judgeCfg);
883
+ else if (judgeCfg.backend === 'openclaw')
884
+ text = await callOpenClaw(prompt, judgeCfg);
885
+ else if (judgeCfg.backend === 'api')
886
+ text = await callApi(prompt, judgeCfg);
887
+ else
888
+ throw new Error(`unknown judge backend: ${String(judgeCfg.backend)}`);
889
+ return { text, effectiveBackend: judgeCfg.backend, effectiveLabel: effectiveJudgeModelTag(judgeCfg) };
890
+ }
891
+ /** Backwards-compatible thin wrapper. Internal callers that need the actual
892
+ * effective backend (for DB labelling) should call `callJudgeWithMeta`. */
893
+ export async function callJudge(prompt, judgeCfg) {
894
+ return (await callJudgeWithMeta(prompt, judgeCfg)).text;
895
+ }
896
+ export async function probeJudgeBackend(cfg) {
897
+ const start = Date.now();
898
+ const checkedAt = start;
899
+ // Each branch must establish that the backend can ACTUALLY produce a judge
900
+ // response — not just that some HTTP port answers. Reachability without a
901
+ // usable model / SDK / on-device LLM yields false-positive "ready" that
902
+ // hides the real failure mode (silent eval skip later). Follow the rule:
903
+ // either invoke (cheap ping) or downgrade to `unavailable` with a reason.
904
+ try {
905
+ if (cfg.backend === 'mlx') {
906
+ const mlx = loadMlxSettings();
907
+ const url = cfg.endpoint ?? mlx.endpoint;
908
+ const base = url.replace(/\/v1\/chat\/completions$/, '').replace(/\/chat\/completions$/, '');
909
+ let model;
910
+ let modelsReachable = false;
911
+ for (const path of ['/v1/models', '/models']) {
912
+ const resp = await fetch(`${base}${path}`, { signal: AbortSignal.timeout(5000) }).catch(() => null);
913
+ if (resp?.ok) {
914
+ modelsReachable = true;
915
+ const json = await resp.json().catch(() => ({}));
916
+ model = json.data?.find(m => m.id && !m.id.toLowerCase().includes('nanollava'))?.id;
917
+ break;
918
+ }
919
+ }
920
+ if (!modelsReachable) {
921
+ return {
922
+ backend: 'mlx', status: 'unavailable',
923
+ reason: `MLX server unreachable at ${base}. Start with \`mlx_lm.server\` or set apme.judge.endpoint.`,
924
+ endpoint: base, checkedAt,
925
+ };
926
+ }
927
+ // Pinned/configured model overrides catalog discovery — the real call uses
928
+ // the same fallback chain as callMlx().
929
+ const pickedModel = mlx.model ?? cfg.model ?? model;
930
+ if (!pickedModel) {
931
+ return {
932
+ backend: 'mlx', status: 'unavailable',
933
+ reason: `MLX server reachable at ${base} but advertises no chat-capable model (only nanollava-class found). Load a chat model with \`mlx_lm.server --model …\`.`,
934
+ endpoint: base, checkedAt,
935
+ };
936
+ }
937
+ // Cheapest possible inference probe: max_tokens=1, temperature=0. If MLX
938
+ // accepts this without a model error, callMlx() will succeed too.
939
+ const ping = await fetch(`${base}/v1/chat/completions`, {
940
+ method: 'POST',
941
+ headers: { 'Content-Type': 'application/json' },
942
+ body: JSON.stringify({
943
+ model: pickedModel,
944
+ messages: [{ role: 'user', content: 'ping' }],
945
+ max_tokens: 1, temperature: 0,
946
+ }),
947
+ signal: AbortSignal.timeout(8000),
948
+ }).catch((e) => ({ ok: false, status: 0, statusText: String(e).slice(0, 80) }));
949
+ if (!ping.ok) {
950
+ const detail = ping.status ? `HTTP ${ping.status}` : ping.statusText ?? 'no response';
951
+ return {
952
+ backend: 'mlx', status: 'unavailable',
953
+ reason: `MLX inference failed for model "${pickedModel}" (${detail}). Check that the model is actually loaded.`,
954
+ endpoint: base, model: pickedModel, checkedAt,
955
+ };
956
+ }
957
+ return { backend: 'mlx', status: 'ready', latencyMs: Date.now() - start, model: pickedModel, endpoint: base, checkedAt };
958
+ }
959
+ if (cfg.backend === 'openai') {
960
+ // Generic OpenAI-compatible (Ollama / OpenRouter / LM Studio / vLLM / …).
961
+ // "Ready" requires a reachable model catalog and a resolvable model id.
962
+ if (!cfg.endpoint) {
963
+ return {
964
+ backend: 'openai', status: 'unavailable',
965
+ reason: 'set apme.judge.endpoint (e.g. http://127.0.0.1:11434/v1 for Ollama, or https://openrouter.ai/api/v1 for OpenRouter)',
966
+ checkedAt,
967
+ };
968
+ }
969
+ const base = openAIBase(cfg.endpoint);
970
+ const isRemote = /^https?:\/\/(?!127\.0\.0\.1|localhost|\[::1\])/.test(cfg.endpoint);
971
+ if (isRemote && !cfg.apiKey) {
972
+ return {
973
+ backend: 'openai', status: 'unavailable',
974
+ reason: `remote endpoint ${base} needs an API key — set apme.judge.apiKey (OpenRouter etc.)`,
975
+ endpoint: base, checkedAt,
976
+ };
977
+ }
978
+ const model = await resolveOpenAIModel(base, cfg.apiKey, cfg.model);
979
+ if (!model || model === 'default') {
980
+ return {
981
+ backend: 'openai', status: 'unavailable',
982
+ reason: `endpoint ${base} unreachable or advertises no model — is the server running / the key valid?`,
983
+ endpoint: base, checkedAt,
984
+ };
985
+ }
986
+ return { backend: 'openai', status: 'ready', latencyMs: Date.now() - start, model, endpoint: base, checkedAt };
987
+ }
988
+ if (cfg.backend === 'openclaw') {
989
+ // OpenClaw Gateway: /health proves the gateway socket is up but does NOT
990
+ // prove /chat will route. The bridge's own gateway adapter performs an
991
+ // Ed25519 handshake against the same gateway; we don't replicate it
992
+ // here, but we DO require both /health AND a model catalog response so
993
+ // a stub gateway with /health = 200 doesn't pass.
994
+ const url = cfg.endpoint ?? 'http://127.0.0.1:18789';
995
+ const base = url.replace(/\/chat$/, '');
996
+ const health = await fetch(`${base}/health`, { signal: AbortSignal.timeout(5000) }).catch(() => null);
997
+ if (!health?.ok) {
998
+ return {
999
+ backend: 'openclaw', status: 'unavailable',
1000
+ reason: `OpenClaw Gateway /health unreachable at ${base}.`,
1001
+ endpoint: base, checkedAt,
1002
+ };
1003
+ }
1004
+ const models = await fetch(`${base}/models`, { signal: AbortSignal.timeout(5000) }).catch(() => null);
1005
+ if (!models?.ok) {
1006
+ return {
1007
+ backend: 'openclaw', status: 'unavailable',
1008
+ reason: `OpenClaw Gateway /health responds but /models does not — gateway not fully initialised. Wait for handshake or check apme.judge.endpoint.`,
1009
+ endpoint: base, checkedAt,
1010
+ };
1011
+ }
1012
+ const json = await models.json().catch(() => ({}));
1013
+ const list = json.data ?? json.models ?? [];
1014
+ const requested = cfg.model;
1015
+ if (requested && !list.some(m => m.id === requested)) {
1016
+ return {
1017
+ backend: 'openclaw', status: 'unavailable',
1018
+ reason: `OpenClaw Gateway is up but model "${requested}" is not advertised. Available: ${list.slice(0, 5).map(m => m.id).join(', ')}`,
1019
+ endpoint: base, checkedAt,
1020
+ };
1021
+ }
1022
+ return {
1023
+ backend: 'openclaw', status: 'ready',
1024
+ latencyMs: Date.now() - start,
1025
+ model: requested ?? list[0]?.id, endpoint: base, checkedAt,
1026
+ };
1027
+ }
1028
+ if (cfg.backend === 'foundationModels') {
1029
+ // Mirror callFoundationModels: explicit endpoint wins over auto-resolve.
1030
+ const url = cfg.endpoint ?? await resolveFoundationModelsUrl();
1031
+ if (!url) {
1032
+ const helper = await probeFoundationModelsHelper();
1033
+ if (helper.available) {
1034
+ return {
1035
+ backend: 'foundationModels',
1036
+ status: 'ready',
1037
+ latencyMs: Date.now() - start,
1038
+ endpoint: helper.path ? `helper:${helper.path}` : 'helper',
1039
+ checkedAt,
1040
+ };
1041
+ }
1042
+ return {
1043
+ backend: 'foundationModels', status: 'unavailable',
1044
+ reason: `Swift daemon not found and helper unavailable: ${helper.reason ?? 'unknown'}`,
1045
+ checkedAt,
1046
+ };
1047
+ }
1048
+ // Foundation Models adapter on the Swift side returns either { text }
1049
+ // (ready) or { error: "unavailable", reason } (Apple Intelligence not
1050
+ // downloaded, model still warming, etc). A trivial ping forces that
1051
+ // signal up to us so we don't claim ready when the on-device LLM is
1052
+ // actually unusable.
1053
+ const ping = await fetch(url, {
1054
+ method: 'POST',
1055
+ headers: { 'Content-Type': 'application/json' },
1056
+ body: JSON.stringify({ prompt: 'ping' }),
1057
+ signal: AbortSignal.timeout(8000),
1058
+ }).catch((e) => ({ ok: false, status: 0, statusText: String(e).slice(0, 80) }));
1059
+ if (!ping.ok) {
1060
+ const detail = ping.status ? `HTTP ${ping.status}` : ping.statusText ?? 'no response';
1061
+ if (!cfg.endpoint) {
1062
+ markFoundationModelsAutoUnavailable(`Swift daemon FM endpoint did not accept probe (${detail}).`);
1063
+ const helper = await probeFoundationModelsHelper();
1064
+ if (helper.available) {
1065
+ return {
1066
+ backend: 'foundationModels',
1067
+ status: 'ready',
1068
+ latencyMs: Date.now() - start,
1069
+ endpoint: helper.path ? `helper:${helper.path}` : 'helper',
1070
+ checkedAt,
1071
+ };
1072
+ }
1073
+ }
1074
+ return {
1075
+ backend: 'foundationModels', status: 'unavailable',
1076
+ reason: `Swift daemon FM endpoint did not accept probe (${detail}).`,
1077
+ endpoint: url, checkedAt,
1078
+ };
1079
+ }
1080
+ const json = await ping.json().catch(() => ({}));
1081
+ if (json.error) {
1082
+ if (!cfg.endpoint) {
1083
+ markFoundationModelsAutoUnavailable(`Foundation Models ${json.error}: ${json.reason ?? 'no reason given'}.`);
1084
+ const helper = await probeFoundationModelsHelper();
1085
+ if (helper.available) {
1086
+ return {
1087
+ backend: 'foundationModels',
1088
+ status: 'ready',
1089
+ latencyMs: Date.now() - start,
1090
+ endpoint: helper.path ? `helper:${helper.path}` : 'helper',
1091
+ checkedAt,
1092
+ };
1093
+ }
1094
+ }
1095
+ return {
1096
+ backend: 'foundationModels', status: 'unavailable',
1097
+ reason: `Foundation Models ${json.error}: ${json.reason ?? 'no reason given'}. Apple Intelligence may not be downloaded yet.`,
1098
+ endpoint: url, checkedAt,
1099
+ };
1100
+ }
1101
+ if (!cfg.endpoint)
1102
+ markFoundationModelsAutoReady(url);
1103
+ return { backend: 'foundationModels', status: 'ready', latencyMs: Date.now() - start, endpoint: url, checkedAt };
1104
+ }
1105
+ if (cfg.backend === 'api') {
1106
+ // Opt-in Anthropic API judge. "Ready" must mean a judge call can
1107
+ // actually succeed, so verify a credential exists and the model id
1108
+ // resolves via the free Models endpoint (no token spend).
1109
+ const hasCredential = Boolean(cfg.apiKey || process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN);
1110
+ if (!hasCredential) {
1111
+ return {
1112
+ backend: 'api', status: 'unavailable',
1113
+ reason: 'no Anthropic API credential — set apme.judge.apiKey in settings.json, export ANTHROPIC_API_KEY, or run `ant auth login`',
1114
+ checkedAt,
1115
+ };
1116
+ }
1117
+ try {
1118
+ const { default: Anthropic } = await import('@anthropic-ai/sdk');
1119
+ const client = new Anthropic({
1120
+ ...(cfg.apiKey ? { apiKey: cfg.apiKey } : {}),
1121
+ timeout: 8_000,
1122
+ maxRetries: 0,
1123
+ });
1124
+ const model = apiJudgeModel(cfg);
1125
+ await client.models.retrieve(model);
1126
+ return {
1127
+ backend: 'api', status: 'ready', model,
1128
+ latencyMs: Date.now() - start, checkedAt,
1129
+ };
1130
+ }
1131
+ catch (err) {
1132
+ return {
1133
+ backend: 'api', status: 'unavailable',
1134
+ reason: `Anthropic API probe failed: ${String(err).slice(0, 200)}`,
1135
+ checkedAt,
1136
+ };
1137
+ }
1138
+ }
1139
+ return { backend: cfg.backend, status: 'unknown', checkedAt };
1140
+ }
1141
+ catch (err) {
1142
+ return {
1143
+ backend: cfg.backend, status: 'unavailable',
1144
+ reason: String(err).slice(0, 200),
1145
+ latencyMs: Date.now() - start,
1146
+ checkedAt,
1147
+ };
1148
+ }
645
1149
  }
646
1150
  async function callMlx(prompt, cfg) {
647
1151
  // MLX server speaks OpenAI chat-completions. The llm.mlx pin (shared with
@@ -691,6 +1195,95 @@ async function callMlx(prompt, cfg) {
691
1195
  }
692
1196
  return text;
693
1197
  }
1198
+ /** Normalize a user-supplied base/endpoint to the chat-completions URL.
1199
+ * Accepts a bare host (`http://127.0.0.1:11434`), a base with `/v1`, or the
1200
+ * full `/v1/chat/completions` — all resolve to the same POST target. */
1201
+ export function openAIChatUrl(endpoint) {
1202
+ let e = endpoint.trim().replace(/\/+$/, '');
1203
+ if (/\/chat\/completions$/.test(e))
1204
+ return e;
1205
+ if (/\/v1$/.test(e))
1206
+ return `${e}/chat/completions`;
1207
+ return `${e}/v1/chat/completions`;
1208
+ }
1209
+ function openAIBase(endpoint) {
1210
+ return endpoint.trim().replace(/\/+$/, '')
1211
+ .replace(/\/chat\/completions$/, '')
1212
+ .replace(/\/v1$/, '');
1213
+ }
1214
+ /** Resolve a model id for an OpenAI-compatible server when the user left it
1215
+ * unset. Ollama exposes `/api/tags`; everything else exposes `/v1/models`. */
1216
+ async function resolveOpenAIModel(base, apiKey, configured) {
1217
+ if (configured && configured !== 'qwen3-30b' && configured !== 'default')
1218
+ return configured;
1219
+ const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
1220
+ // Ollama first (its /v1/models also works, but /api/tags is the canonical list).
1221
+ try {
1222
+ const r = await fetch(`${base}/api/tags`, { headers, signal: AbortSignal.timeout(3000) }).catch(() => null);
1223
+ if (r?.ok) {
1224
+ const j = await r.json();
1225
+ const first = j.models?.find((m) => m.name)?.name;
1226
+ if (first)
1227
+ return first;
1228
+ }
1229
+ }
1230
+ catch { /* try openai path */ }
1231
+ for (const path of ['/v1/models', '/models']) {
1232
+ try {
1233
+ const r = await fetch(`${base}${path}`, { headers, signal: AbortSignal.timeout(3000) }).catch(() => null);
1234
+ if (r?.ok) {
1235
+ const j = await r.json();
1236
+ const first = j.data?.find((m) => m.id && !m.id.toLowerCase().includes('nanollava'))?.id;
1237
+ if (first)
1238
+ return first;
1239
+ }
1240
+ }
1241
+ catch { /* next */ }
1242
+ }
1243
+ return configured || 'default';
1244
+ }
1245
+ /**
1246
+ * Generic OpenAI-compatible chat-completions judge. One implementation covers
1247
+ * the de-facto standard local + cloud providers:
1248
+ * - Ollama endpoint http://127.0.0.1:11434/v1 (no key)
1249
+ * - LM Studio endpoint http://127.0.0.1:1234/v1 (no key)
1250
+ * - vLLM/llama.cpp/LiteLLM/MLX (local OpenAI servers, no key)
1251
+ * - OpenRouter endpoint https://openrouter.ai/api/v1 (Bearer apiKey)
1252
+ * - any other OpenAI-compatible endpoint
1253
+ * `apiKey` is sent as a Bearer only when set — local servers ignore it.
1254
+ */
1255
+ async function callOpenAICompatible(prompt, cfg) {
1256
+ if (!cfg.endpoint) {
1257
+ throw new Error('openai judge: apme.judge.endpoint is required (e.g. http://127.0.0.1:11434/v1 for Ollama)');
1258
+ }
1259
+ const url = openAIChatUrl(cfg.endpoint);
1260
+ const base = openAIBase(cfg.endpoint);
1261
+ const model = await resolveOpenAIModel(base, cfg.apiKey, cfg.model);
1262
+ const headers = { 'Content-Type': 'application/json' };
1263
+ if (cfg.apiKey)
1264
+ headers.Authorization = `Bearer ${cfg.apiKey}`;
1265
+ const resp = await fetch(url, {
1266
+ method: 'POST',
1267
+ headers,
1268
+ body: JSON.stringify({
1269
+ model,
1270
+ messages: [
1271
+ { role: 'system', content: 'You are an exacting code evaluator. Reply with strict JSON only.' },
1272
+ { role: 'user', content: prompt },
1273
+ ],
1274
+ temperature: 0,
1275
+ max_tokens: 1024,
1276
+ }),
1277
+ signal: AbortSignal.timeout(90_000),
1278
+ });
1279
+ if (!resp.ok)
1280
+ throw new Error(`openai judge HTTP ${resp.status} (${url})`);
1281
+ const json = await resp.json();
1282
+ const text = json.choices?.[0]?.message?.content;
1283
+ if (typeof text !== 'string' || text.trim().length === 0)
1284
+ throw new Error('openai judge returned empty content');
1285
+ return text;
1286
+ }
694
1287
  async function callOpenClaw(prompt, cfg) {
695
1288
  // OpenClaw Gateway exposes the user's configured models. Route through it
696
1289
  // when the user wants to reuse their existing subscription models for judge.
@@ -709,58 +1302,136 @@ async function callOpenClaw(prompt, cfg) {
709
1302
  return json.text;
710
1303
  }
711
1304
  /**
712
- * Route a judge call to the Swift daemon's Foundation Models adapter.
1305
+ * Route a judge call to Foundation Models.
713
1306
  *
714
- * The actual on-device LLMSession lives in
715
- * `apple/AgentDeck/Daemon/Apme/ApmeJudgeFoundationModels.swift`; this TS path
716
- * just forwards the prompt. Available only when a Swift in-process daemon is
717
- * running on the same machine (App Store macOS build). Node-only setups don't
718
- * ship Foundation Models — callers should either opt into `fallbackToMlx` or
719
- * accept the resulting eval skip.
1307
+ * Prefer the Swift daemon HTTP adapter when it is running; otherwise use the
1308
+ * bundled CLI Swift helper process. Default CLI config enables
1309
+ * `fallbackToMlx`, while callers can set it false to force a skip when
1310
+ * neither Foundation Models path works.
720
1311
  *
721
1312
  * Shape contract:
722
1313
  * Request : POST /apme/judge/foundation-models { prompt: string }
723
1314
  * Response : { text: string } | { error: "unavailable", reason: string }
724
1315
  */
725
1316
  async function callFoundationModels(prompt, cfg) {
1317
+ const explicitEndpoint = Boolean(cfg.endpoint);
1318
+ const cached = explicitEndpoint ? null : getFoundationModelsAutoCache();
1319
+ if (cached?.state === 'unavailable') {
1320
+ try {
1321
+ return await callFoundationModelsHelper(prompt);
1322
+ }
1323
+ catch (helperErr) {
1324
+ throw new Error(`foundationModels cached unavailable: ${cached.reason}; helper unavailable: ${String(helperErr)}`);
1325
+ }
1326
+ }
726
1327
  const url = cfg.endpoint ?? await resolveFoundationModelsUrl();
727
- if (!url)
728
- throw new Error('foundationModels: no Swift daemon found — FM is only available in App Store macOS builds');
729
- const resp = await fetch(url, {
730
- method: 'POST',
731
- headers: { 'Content-Type': 'application/json' },
732
- body: JSON.stringify({ prompt }),
733
- signal: AbortSignal.timeout(60_000),
734
- });
735
- if (!resp.ok)
736
- throw new Error(`foundationModels HTTP ${resp.status}`);
737
- const json = await resp.json();
738
- if (json.error) {
739
- throw new Error(`foundationModels ${json.error}: ${json.reason ?? 'no reason'}`);
1328
+ if (!url) {
1329
+ try {
1330
+ return await callFoundationModelsHelper(prompt);
1331
+ }
1332
+ catch (helperErr) {
1333
+ throw new Error(`foundationModels: no Swift daemon found and helper unavailable: ${String(helperErr)}`);
1334
+ }
740
1335
  }
741
- if (typeof json.text !== 'string' || json.text.length === 0) {
742
- throw new Error('foundationModels returned empty text');
1336
+ try {
1337
+ const resp = await fetch(url, {
1338
+ method: 'POST',
1339
+ headers: { 'Content-Type': 'application/json' },
1340
+ body: JSON.stringify({ prompt }),
1341
+ signal: AbortSignal.timeout(60_000),
1342
+ });
1343
+ if (!resp.ok)
1344
+ throw new Error(`foundationModels HTTP ${resp.status}`);
1345
+ const json = await resp.json();
1346
+ if (json.error) {
1347
+ throw new Error(`foundationModels ${json.error}: ${json.reason ?? 'no reason'}`);
1348
+ }
1349
+ if (typeof json.text !== 'string' || json.text.length === 0) {
1350
+ throw new Error('foundationModels returned empty text');
1351
+ }
1352
+ if (!explicitEndpoint)
1353
+ markFoundationModelsAutoReady(url);
1354
+ return json.text;
1355
+ }
1356
+ catch (err) {
1357
+ if (!explicitEndpoint) {
1358
+ markFoundationModelsAutoUnavailable(String(err));
1359
+ try {
1360
+ return await callFoundationModelsHelper(prompt);
1361
+ }
1362
+ catch (helperErr) {
1363
+ throw new Error(`${String(err)}; helper unavailable: ${String(helperErr)}`);
1364
+ }
1365
+ }
1366
+ throw err;
743
1367
  }
744
- return json.text;
745
1368
  }
746
1369
  /** Best-effort resolver for the Swift daemon's FM endpoint. Returns null when
747
1370
  * no Swift daemon (httpPort ≠ port) is reachable. */
748
1371
  async function resolveFoundationModelsUrl() {
1372
+ const cached = getFoundationModelsAutoCache();
1373
+ if (cached?.state === 'ready')
1374
+ return cached.url;
1375
+ if (cached?.state === 'unavailable')
1376
+ return null;
1377
+ if (foundationModelsResolveInFlight)
1378
+ return foundationModelsResolveInFlight;
749
1379
  // Lazy-require to avoid pulling session-registry into every test bundle
750
1380
  // that imports runner.ts for its pure helpers.
751
- const { findDaemonPortAsync } = await import('../session-registry.js');
752
- const info = await findDaemonPortAsync();
753
- if (!info)
754
- return null;
755
- const port = info.httpPort ?? info.port;
756
- return `http://127.0.0.1:${port}/apme/judge/foundation-models`;
757
- }
758
- async function callApi(_prompt, _cfg) {
759
- // Anthropic API opt-in only. Requires ANTHROPIC_API_KEY and the @anthropic-ai/sdk
760
- // package, neither of which we depend on by default. Surface a clear error so
761
- // users understand they enabled API mode but didn't wire credentials.
762
- throw new Error('APME judge backend "api" requires explicit setup: install @anthropic-ai/sdk and set ANTHROPIC_API_KEY. ' +
763
- 'Phase 2 ships with MLX (local, free) by default.');
1381
+ foundationModelsResolveInFlight = (async () => {
1382
+ try {
1383
+ const { findDaemonPortAsync } = await import('../session-registry.js');
1384
+ const info = await findDaemonPortAsync();
1385
+ if (!info) {
1386
+ markFoundationModelsAutoUnavailable('Swift daemon not found', FOUNDATION_MODELS_MISSING_DAEMON_TTL_MS);
1387
+ return null;
1388
+ }
1389
+ const port = info.httpPort ?? info.port;
1390
+ const url = `http://127.0.0.1:${port}/apme/judge/foundation-models`;
1391
+ markFoundationModelsAutoReady(url);
1392
+ return url;
1393
+ }
1394
+ finally {
1395
+ foundationModelsResolveInFlight = null;
1396
+ }
1397
+ })();
1398
+ return foundationModelsResolveInFlight;
1399
+ }
1400
+ /** Default model for the opt-in Anthropic API judge when the configured
1401
+ * `model` belongs to another backend (e.g. an MLX id left over from a
1402
+ * backend switch). */
1403
+ const API_JUDGE_DEFAULT_MODEL = 'claude-opus-4-8';
1404
+ function apiJudgeModel(cfg) {
1405
+ return cfg.model && cfg.model.startsWith('claude') ? cfg.model : API_JUDGE_DEFAULT_MODEL;
1406
+ }
1407
+ async function callApi(prompt, cfg) {
1408
+ // Opt-in Anthropic API judge (mirrors the Swift ApmeJudgeApi adapter).
1409
+ // Credential chain: settings.json apme.judge.apiKey -> the SDK's standard
1410
+ // resolution (ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN / `ant auth login`
1411
+ // profile). Hard client timeout — a wedged judge must never wedge an eval.
1412
+ const { default: Anthropic } = await import('@anthropic-ai/sdk');
1413
+ const client = new Anthropic({
1414
+ ...(cfg.apiKey ? { apiKey: cfg.apiKey } : {}),
1415
+ timeout: 90_000,
1416
+ maxRetries: 1,
1417
+ });
1418
+ const response = await client.messages.create({
1419
+ model: apiJudgeModel(cfg),
1420
+ max_tokens: 8192,
1421
+ thinking: { type: 'adaptive' },
1422
+ messages: [{ role: 'user', content: prompt }],
1423
+ });
1424
+ if (response.stop_reason === 'refusal') {
1425
+ throw new Error('API judge refused the request (stop_reason=refusal)');
1426
+ }
1427
+ const text = response.content
1428
+ .filter((b) => b.type === 'text')
1429
+ .map((b) => b.text)
1430
+ .join('\n')
1431
+ .trim();
1432
+ if (!text)
1433
+ throw new Error(`API judge returned no text (stop_reason=${response.stop_reason})`);
1434
+ return text;
764
1435
  }
765
1436
  export function parseJudgeJson(text) {
766
1437
  // Models often wrap JSON in prose or code fences — grab the first {...} block.