@agentdeck/bridge 0.2.2 → 0.2.3

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 (383) hide show
  1. package/assets/fonts/IBMPlexSans-Bold.ttf +0 -0
  2. package/assets/fonts/IBMPlexSans-Regular.ttf +0 -0
  3. package/assets/fonts/JetBrainsMono-Bold.ttf +0 -0
  4. package/assets/fonts/JetBrainsMono-Regular.ttf +0 -0
  5. package/assets/fonts/LICENSES.md +22 -0
  6. package/dist/adapters/claude-code.d.ts +6 -0
  7. package/dist/adapters/claude-code.d.ts.map +1 -1
  8. package/dist/adapters/claude-code.js +8 -0
  9. package/dist/adapters/claude-code.js.map +1 -1
  10. package/dist/adapters/openclaw.d.ts +24 -0
  11. package/dist/adapters/openclaw.d.ts.map +1 -1
  12. package/dist/adapters/openclaw.js +291 -35
  13. package/dist/adapters/openclaw.js.map +1 -1
  14. package/dist/adapters/opencode-adapter.d.ts +59 -0
  15. package/dist/adapters/opencode-adapter.d.ts.map +1 -1
  16. package/dist/adapters/opencode-adapter.js +297 -35
  17. package/dist/adapters/opencode-adapter.js.map +1 -1
  18. package/dist/adb-reverse.d.ts +1 -1
  19. package/dist/adb-reverse.js +1 -1
  20. package/dist/antigravity-local.d.ts +1 -0
  21. package/dist/antigravity-local.d.ts.map +1 -1
  22. package/dist/antigravity-local.js +33 -0
  23. package/dist/antigravity-local.js.map +1 -1
  24. package/dist/apme/adapters/claude-hook.d.ts.map +1 -1
  25. package/dist/apme/adapters/claude-hook.js +5 -0
  26. package/dist/apme/adapters/claude-hook.js.map +1 -1
  27. package/dist/apme/adapters/codex-hook.d.ts +17 -0
  28. package/dist/apme/adapters/codex-hook.d.ts.map +1 -0
  29. package/dist/apme/adapters/codex-hook.js +95 -0
  30. package/dist/apme/adapters/codex-hook.js.map +1 -0
  31. package/dist/apme/adapters/codex-turn-manager.d.ts +86 -0
  32. package/dist/apme/adapters/codex-turn-manager.d.ts.map +1 -0
  33. package/dist/apme/adapters/codex-turn-manager.js +496 -0
  34. package/dist/apme/adapters/codex-turn-manager.js.map +1 -0
  35. package/dist/apme/adapters/openclaw-hook.d.ts +99 -0
  36. package/dist/apme/adapters/openclaw-hook.d.ts.map +1 -0
  37. package/dist/apme/adapters/openclaw-hook.js +234 -0
  38. package/dist/apme/adapters/openclaw-hook.js.map +1 -0
  39. package/dist/apme/adapters/opencode-hook.d.ts +56 -0
  40. package/dist/apme/adapters/opencode-hook.d.ts.map +1 -0
  41. package/dist/apme/adapters/opencode-hook.js +182 -0
  42. package/dist/apme/adapters/opencode-hook.js.map +1 -0
  43. package/dist/apme/adapters/timeline.d.ts.map +1 -1
  44. package/dist/apme/adapters/timeline.js +6 -1
  45. package/dist/apme/adapters/timeline.js.map +1 -1
  46. package/dist/apme/antigravity-transcript.d.ts +18 -0
  47. package/dist/apme/antigravity-transcript.d.ts.map +1 -0
  48. package/dist/apme/antigravity-transcript.js +94 -0
  49. package/dist/apme/antigravity-transcript.js.map +1 -0
  50. package/dist/apme/classify-turn.d.ts +13 -0
  51. package/dist/apme/classify-turn.d.ts.map +1 -0
  52. package/dist/apme/classify-turn.js +49 -0
  53. package/dist/apme/classify-turn.js.map +1 -0
  54. package/dist/apme/claude-transcript-reader.d.ts +10 -0
  55. package/dist/apme/claude-transcript-reader.d.ts.map +1 -1
  56. package/dist/apme/claude-transcript-reader.js +35 -0
  57. package/dist/apme/claude-transcript-reader.js.map +1 -1
  58. package/dist/apme/collector.d.ts +129 -6
  59. package/dist/apme/collector.d.ts.map +1 -1
  60. package/dist/apme/collector.js +470 -23
  61. package/dist/apme/collector.js.map +1 -1
  62. package/dist/apme/dashboard-html.d.ts.map +1 -1
  63. package/dist/apme/dashboard-html.js +87 -11
  64. package/dist/apme/dashboard-html.js.map +1 -1
  65. package/dist/apme/http.d.ts.map +1 -1
  66. package/dist/apme/http.js +43 -6
  67. package/dist/apme/http.js.map +1 -1
  68. package/dist/apme/index.d.ts +26 -5
  69. package/dist/apme/index.d.ts.map +1 -1
  70. package/dist/apme/index.js +135 -9
  71. package/dist/apme/index.js.map +1 -1
  72. package/dist/apme/pareto.d.ts +39 -0
  73. package/dist/apme/pareto.d.ts.map +1 -0
  74. package/dist/apme/pareto.js +59 -0
  75. package/dist/apme/pareto.js.map +1 -0
  76. package/dist/apme/recommend.d.ts +3 -0
  77. package/dist/apme/recommend.d.ts.map +1 -1
  78. package/dist/apme/recommend.js +43 -1
  79. package/dist/apme/recommend.js.map +1 -1
  80. package/dist/apme/runner.d.ts +92 -4
  81. package/dist/apme/runner.d.ts.map +1 -1
  82. package/dist/apme/runner.js +586 -70
  83. package/dist/apme/runner.js.map +1 -1
  84. package/dist/apme/sample-to-timeline.d.ts +26 -0
  85. package/dist/apme/sample-to-timeline.d.ts.map +1 -0
  86. package/dist/apme/sample-to-timeline.js +95 -0
  87. package/dist/apme/sample-to-timeline.js.map +1 -0
  88. package/dist/apme/scorers/index.d.ts +47 -0
  89. package/dist/apme/scorers/index.d.ts.map +1 -0
  90. package/dist/apme/scorers/index.js +96 -0
  91. package/dist/apme/scorers/index.js.map +1 -0
  92. package/dist/apme/settings.d.ts +14 -10
  93. package/dist/apme/settings.d.ts.map +1 -1
  94. package/dist/apme/settings.js +34 -8
  95. package/dist/apme/settings.js.map +1 -1
  96. package/dist/apme/store.d.ts +33 -1
  97. package/dist/apme/store.d.ts.map +1 -1
  98. package/dist/apme/store.js +334 -25
  99. package/dist/apme/store.js.map +1 -1
  100. package/dist/awaiting-overlay.d.ts +99 -0
  101. package/dist/awaiting-overlay.d.ts.map +1 -0
  102. package/dist/awaiting-overlay.js +148 -0
  103. package/dist/awaiting-overlay.js.map +1 -0
  104. package/dist/ble-sync-spawn.d.ts +63 -0
  105. package/dist/ble-sync-spawn.d.ts.map +1 -0
  106. package/dist/ble-sync-spawn.js +196 -0
  107. package/dist/ble-sync-spawn.js.map +1 -0
  108. package/dist/bridge-core.d.ts +24 -1
  109. package/dist/bridge-core.d.ts.map +1 -1
  110. package/dist/bridge-core.js +157 -24
  111. package/dist/bridge-core.js.map +1 -1
  112. package/dist/broadcast.d.ts +3 -0
  113. package/dist/broadcast.d.ts.map +1 -0
  114. package/dist/broadcast.js +122 -0
  115. package/dist/broadcast.js.map +1 -0
  116. package/dist/check-deps.d.ts.map +1 -1
  117. package/dist/check-deps.js +25 -9
  118. package/dist/check-deps.js.map +1 -1
  119. package/dist/cli.d.ts +15 -1
  120. package/dist/cli.d.ts.map +1 -1
  121. package/dist/cli.js +1021 -93
  122. package/dist/cli.js.map +1 -1
  123. package/dist/codex-auth.d.ts +21 -0
  124. package/dist/codex-auth.d.ts.map +1 -1
  125. package/dist/codex-auth.js +52 -1
  126. package/dist/codex-auth.js.map +1 -1
  127. package/dist/codex-output-parser.d.ts +8 -0
  128. package/dist/codex-output-parser.d.ts.map +1 -1
  129. package/dist/codex-output-parser.js +110 -25
  130. package/dist/codex-output-parser.js.map +1 -1
  131. package/dist/codex-rate-limits.d.ts +16 -0
  132. package/dist/codex-rate-limits.d.ts.map +1 -0
  133. package/dist/codex-rate-limits.js +197 -0
  134. package/dist/codex-rate-limits.js.map +1 -0
  135. package/dist/codex-rollout-response.d.ts +31 -0
  136. package/dist/codex-rollout-response.d.ts.map +1 -0
  137. package/dist/codex-rollout-response.js +135 -0
  138. package/dist/codex-rollout-response.js.map +1 -0
  139. package/dist/daemon-server.d.ts +85 -0
  140. package/dist/daemon-server.d.ts.map +1 -1
  141. package/dist/daemon-server.js +1627 -159
  142. package/dist/daemon-server.js.map +1 -1
  143. package/dist/daemon.js +15 -6
  144. package/dist/daemon.js.map +1 -1
  145. package/dist/display-dim.d.ts +9 -0
  146. package/dist/display-dim.d.ts.map +1 -0
  147. package/dist/display-dim.js +53 -0
  148. package/dist/display-dim.js.map +1 -0
  149. package/dist/display-monitor.d.ts +15 -2
  150. package/dist/display-monitor.d.ts.map +1 -1
  151. package/dist/display-monitor.js +104 -26
  152. package/dist/display-monitor.js.map +1 -1
  153. package/dist/esp32-serial.d.ts +160 -1
  154. package/dist/esp32-serial.d.ts.map +1 -1
  155. package/dist/esp32-serial.js +950 -153
  156. package/dist/esp32-serial.js.map +1 -1
  157. package/dist/fallback-task-timeline.d.ts +42 -0
  158. package/dist/fallback-task-timeline.d.ts.map +1 -0
  159. package/dist/fallback-task-timeline.js +112 -0
  160. package/dist/fallback-task-timeline.js.map +1 -0
  161. package/dist/foundation-models-helper.d.ts +11 -0
  162. package/dist/foundation-models-helper.d.ts.map +1 -0
  163. package/dist/foundation-models-helper.js +241 -0
  164. package/dist/foundation-models-helper.js.map +1 -0
  165. package/dist/hook-migration.js +1 -1
  166. package/dist/hook-migration.js.map +1 -1
  167. package/dist/hook-server.d.ts +2 -3
  168. package/dist/hook-server.d.ts.map +1 -1
  169. package/dist/hook-server.js +22 -10
  170. package/dist/hook-server.js.map +1 -1
  171. package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts +33 -0
  172. package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts.map +1 -0
  173. package/dist/idotmatrix/idotmatrix-daemon-sync.js +164 -0
  174. package/dist/idotmatrix/idotmatrix-daemon-sync.js.map +1 -0
  175. package/dist/idotmatrix/idotmatrix-discover.d.ts +18 -0
  176. package/dist/idotmatrix/idotmatrix-discover.d.ts.map +1 -0
  177. package/dist/idotmatrix/idotmatrix-discover.js +85 -0
  178. package/dist/idotmatrix/idotmatrix-discover.js.map +1 -0
  179. package/dist/idotmatrix/idotmatrix-settings.d.ts +20 -0
  180. package/dist/idotmatrix/idotmatrix-settings.d.ts.map +1 -0
  181. package/dist/idotmatrix/idotmatrix-settings.js +57 -0
  182. package/dist/idotmatrix/idotmatrix-settings.js.map +1 -0
  183. package/dist/index.d.ts.map +1 -1
  184. package/dist/index.js +215 -179
  185. package/dist/index.js.map +1 -1
  186. package/dist/log-stream.d.ts +15 -16
  187. package/dist/log-stream.d.ts.map +1 -1
  188. package/dist/log-stream.js +18 -96
  189. package/dist/log-stream.js.map +1 -1
  190. package/dist/logger.d.ts.map +1 -1
  191. package/dist/logger.js +12 -3
  192. package/dist/logger.js.map +1 -1
  193. package/dist/mdns.d.ts +17 -0
  194. package/dist/mdns.d.ts.map +1 -1
  195. package/dist/mdns.js +52 -18
  196. package/dist/mdns.js.map +1 -1
  197. package/dist/modules/adb-module.d.ts +1 -1
  198. package/dist/modules/adb-module.d.ts.map +1 -1
  199. package/dist/modules/adb-module.js +4 -3
  200. package/dist/modules/adb-module.js.map +1 -1
  201. package/dist/modules/broadcast-module.d.ts +17 -0
  202. package/dist/modules/broadcast-module.d.ts.map +1 -0
  203. package/dist/modules/broadcast-module.js +26 -0
  204. package/dist/modules/broadcast-module.js.map +1 -0
  205. package/dist/modules/idotmatrix-module.d.ts +9 -0
  206. package/dist/modules/idotmatrix-module.d.ts.map +1 -0
  207. package/dist/modules/idotmatrix-module.js +46 -0
  208. package/dist/modules/idotmatrix-module.js.map +1 -0
  209. package/dist/modules/index.d.ts +3 -1
  210. package/dist/modules/index.d.ts.map +1 -1
  211. package/dist/modules/index.js +9 -3
  212. package/dist/modules/index.js.map +1 -1
  213. package/dist/modules/pixoo-module.d.ts.map +1 -1
  214. package/dist/modules/pixoo-module.js +14 -4
  215. package/dist/modules/pixoo-module.js.map +1 -1
  216. package/dist/modules/serial-module.d.ts +4 -0
  217. package/dist/modules/serial-module.d.ts.map +1 -1
  218. package/dist/modules/serial-module.js +14 -12
  219. package/dist/modules/serial-module.js.map +1 -1
  220. package/dist/modules/timebox-module.d.ts +9 -0
  221. package/dist/modules/timebox-module.d.ts.map +1 -0
  222. package/dist/modules/timebox-module.js +46 -0
  223. package/dist/modules/timebox-module.js.map +1 -0
  224. package/dist/modules/types.d.ts +4 -2
  225. package/dist/modules/types.d.ts.map +1 -1
  226. package/dist/openclaw-session.d.ts +18 -0
  227. package/dist/openclaw-session.d.ts.map +1 -0
  228. package/dist/openclaw-session.js +34 -0
  229. package/dist/openclaw-session.js.map +1 -0
  230. package/dist/output-parser.d.ts +30 -0
  231. package/dist/output-parser.d.ts.map +1 -1
  232. package/dist/output-parser.js +125 -9
  233. package/dist/output-parser.js.map +1 -1
  234. package/dist/passive-observer.d.ts +26 -0
  235. package/dist/passive-observer.d.ts.map +1 -1
  236. package/dist/passive-observer.js +258 -49
  237. package/dist/passive-observer.js.map +1 -1
  238. package/dist/permission-resolver.d.ts +47 -0
  239. package/dist/permission-resolver.d.ts.map +1 -0
  240. package/dist/permission-resolver.js +103 -0
  241. package/dist/permission-resolver.js.map +1 -0
  242. package/dist/pixoo/micro-glyphs.d.ts +49 -0
  243. package/dist/pixoo/micro-glyphs.d.ts.map +1 -0
  244. package/dist/pixoo/micro-glyphs.js +276 -0
  245. package/dist/pixoo/micro-glyphs.js.map +1 -0
  246. package/dist/pixoo/pixoo-bridge.d.ts +2 -2
  247. package/dist/pixoo/pixoo-bridge.d.ts.map +1 -1
  248. package/dist/pixoo/pixoo-bridge.js +93 -25
  249. package/dist/pixoo/pixoo-bridge.js.map +1 -1
  250. package/dist/pixoo/pixoo-camera.d.ts +19 -3
  251. package/dist/pixoo/pixoo-camera.d.ts.map +1 -1
  252. package/dist/pixoo/pixoo-camera.js +62 -27
  253. package/dist/pixoo/pixoo-camera.js.map +1 -1
  254. package/dist/pixoo/pixoo-client.d.ts +13 -3
  255. package/dist/pixoo/pixoo-client.d.ts.map +1 -1
  256. package/dist/pixoo/pixoo-client.js +51 -22
  257. package/dist/pixoo/pixoo-client.js.map +1 -1
  258. package/dist/pixoo/pixoo-discover.d.ts +37 -0
  259. package/dist/pixoo/pixoo-discover.d.ts.map +1 -0
  260. package/dist/pixoo/pixoo-discover.js +105 -0
  261. package/dist/pixoo/pixoo-discover.js.map +1 -0
  262. package/dist/pixoo/pixoo-renderer.d.ts +8 -3
  263. package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
  264. package/dist/pixoo/pixoo-renderer.js +189 -60
  265. package/dist/pixoo/pixoo-renderer.js.map +1 -1
  266. package/dist/pixoo/pixoo-settings.d.ts +6 -0
  267. package/dist/pixoo/pixoo-settings.d.ts.map +1 -1
  268. package/dist/pixoo/pixoo-settings.js +9 -0
  269. package/dist/pixoo/pixoo-settings.js.map +1 -1
  270. package/dist/pixoo/pixoo-sprites.d.ts +60 -11
  271. package/dist/pixoo/pixoo-sprites.d.ts.map +1 -1
  272. package/dist/pixoo/pixoo-sprites.js +512 -209
  273. package/dist/pixoo/pixoo-sprites.js.map +1 -1
  274. package/dist/pty-manager.d.ts.map +1 -1
  275. package/dist/pty-manager.js +8 -2
  276. package/dist/pty-manager.js.map +1 -1
  277. package/dist/session-activity.d.ts +14 -0
  278. package/dist/session-activity.d.ts.map +1 -0
  279. package/dist/session-activity.js +160 -0
  280. package/dist/session-activity.js.map +1 -0
  281. package/dist/session-aggregator.d.ts +17 -0
  282. package/dist/session-aggregator.d.ts.map +1 -1
  283. package/dist/session-aggregator.js +48 -9
  284. package/dist/session-aggregator.js.map +1 -1
  285. package/dist/session-registry.d.ts +71 -3
  286. package/dist/session-registry.d.ts.map +1 -1
  287. package/dist/session-registry.js +137 -8
  288. package/dist/session-registry.js.map +1 -1
  289. package/dist/session-timeline-relay.d.ts.map +1 -1
  290. package/dist/session-timeline-relay.js +13 -4
  291. package/dist/session-timeline-relay.js.map +1 -1
  292. package/dist/session-transcript-timeline.d.ts +49 -0
  293. package/dist/session-transcript-timeline.d.ts.map +1 -0
  294. package/dist/session-transcript-timeline.js +276 -0
  295. package/dist/session-transcript-timeline.js.map +1 -0
  296. package/dist/state-machine.d.ts +6 -1
  297. package/dist/state-machine.d.ts.map +1 -1
  298. package/dist/state-machine.js +86 -28
  299. package/dist/state-machine.js.map +1 -1
  300. package/dist/timebox/timebox-daemon-sync.d.ts +19 -0
  301. package/dist/timebox/timebox-daemon-sync.d.ts.map +1 -0
  302. package/dist/timebox/timebox-daemon-sync.js +135 -0
  303. package/dist/timebox/timebox-daemon-sync.js.map +1 -0
  304. package/dist/timebox/timebox-discover.d.ts +19 -0
  305. package/dist/timebox/timebox-discover.d.ts.map +1 -0
  306. package/dist/timebox/timebox-discover.js +87 -0
  307. package/dist/timebox/timebox-discover.js.map +1 -0
  308. package/dist/timebox/timebox-settings.d.ts +32 -0
  309. package/dist/timebox/timebox-settings.d.ts.map +1 -0
  310. package/dist/timebox/timebox-settings.js +80 -0
  311. package/dist/timebox/timebox-settings.js.map +1 -0
  312. package/dist/timeline-store.d.ts +63 -3
  313. package/dist/timeline-store.d.ts.map +1 -1
  314. package/dist/timeline-store.js +292 -14
  315. package/dist/timeline-store.js.map +1 -1
  316. package/dist/timeline-summarizer.d.ts.map +1 -1
  317. package/dist/timeline-summarizer.js +29 -5
  318. package/dist/timeline-summarizer.js.map +1 -1
  319. package/dist/tui/ansi.d.ts +0 -4
  320. package/dist/tui/ansi.d.ts.map +1 -1
  321. package/dist/tui/ansi.js +14 -7
  322. package/dist/tui/ansi.js.map +1 -1
  323. package/dist/tui/dashboard.d.ts +1 -0
  324. package/dist/tui/dashboard.d.ts.map +1 -1
  325. package/dist/tui/dashboard.js +24 -7
  326. package/dist/tui/dashboard.js.map +1 -1
  327. package/dist/tui/renderer.d.ts +36 -0
  328. package/dist/tui/renderer.d.ts.map +1 -1
  329. package/dist/tui/renderer.js +303 -119
  330. package/dist/tui/renderer.js.map +1 -1
  331. package/dist/tui/terrarium.d.ts.map +1 -1
  332. package/dist/tui/terrarium.js +37 -49
  333. package/dist/tui/terrarium.js.map +1 -1
  334. package/dist/types.d.ts +2 -2
  335. package/dist/types.d.ts.map +1 -1
  336. package/dist/types.js +2 -2
  337. package/dist/types.js.map +1 -1
  338. package/dist/usage-api.d.ts.map +1 -1
  339. package/dist/usage-api.js +7 -1
  340. package/dist/usage-api.js.map +1 -1
  341. package/dist/usage-event.d.ts +10 -3
  342. package/dist/usage-event.d.ts.map +1 -1
  343. package/dist/usage-event.js +74 -12
  344. package/dist/usage-event.js.map +1 -1
  345. package/dist/utility-proxy.d.ts.map +1 -1
  346. package/dist/utility-proxy.js +8 -0
  347. package/dist/utility-proxy.js.map +1 -1
  348. package/dist/utils/project-name.d.ts +3 -0
  349. package/dist/utils/project-name.d.ts.map +1 -1
  350. package/dist/utils/project-name.js +44 -2
  351. package/dist/utils/project-name.js.map +1 -1
  352. package/dist/windows-service.d.ts +26 -0
  353. package/dist/windows-service.d.ts.map +1 -0
  354. package/dist/windows-service.js +145 -0
  355. package/dist/windows-service.js.map +1 -0
  356. package/dist/ws-server.d.ts +22 -1
  357. package/dist/ws-server.d.ts.map +1 -1
  358. package/dist/ws-server.js +89 -4
  359. package/dist/ws-server.js.map +1 -1
  360. package/fm-helper/AgentDeckFMHelper.swift +112 -0
  361. package/package.json +9 -3
  362. package/scripts/build-fm-helper.mjs +26 -0
  363. package/LICENSE +0 -21
  364. package/dist/apme/tuner.d.ts +0 -64
  365. package/dist/apme/tuner.d.ts.map +0 -1
  366. package/dist/apme/tuner.js +0 -297
  367. package/dist/apme/tuner.js.map +0 -1
  368. package/dist/d200h/hid-protocol.d.ts +0 -62
  369. package/dist/d200h/hid-protocol.d.ts.map +0 -1
  370. package/dist/d200h/hid-protocol.js +0 -124
  371. package/dist/d200h/hid-protocol.js.map +0 -1
  372. package/dist/d200h/image-renderer.d.ts +0 -36
  373. package/dist/d200h/image-renderer.d.ts.map +0 -1
  374. package/dist/d200h/image-renderer.js +0 -487
  375. package/dist/d200h/image-renderer.js.map +0 -1
  376. package/dist/modules/d200h-module.d.ts +0 -43
  377. package/dist/modules/d200h-module.d.ts.map +0 -1
  378. package/dist/modules/d200h-module.js +0 -360
  379. package/dist/modules/d200h-module.js.map +0 -1
  380. package/dist/modules/d200h-renderer.d.ts +0 -7
  381. package/dist/modules/d200h-renderer.d.ts.map +0 -1
  382. package/dist/modules/d200h-renderer.js +0 -321
  383. package/dist/modules/d200h-renderer.js.map +0 -1
@@ -6,16 +6,35 @@
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
+ import { createRequire } from 'module';
15
16
  import { debug } from '../logger.js';
16
- import { loadApmeConfig, shouldJudge } from './settings.js';
17
+ import { loadApmeConfig, shouldJudge, DEFAULT_APME_CONFIG } from './settings.js';
17
18
  import { loadMlxSettings, mlxChatUrl } from '@agentdeck/shared';
19
+ import { callFoundationModelsHelper, probeFoundationModelsHelper } from '../foundation-models-helper.js';
20
+ import { runSampleScorers } from './scorers/index.js';
18
21
  import { execSync } from 'child_process';
22
+ function deriveTaskOutcome(score) {
23
+ if (score == null)
24
+ return 'pending';
25
+ if (score >= 0.75)
26
+ return 'success';
27
+ if (score >= 0.5)
28
+ return 'partial';
29
+ return 'fail';
30
+ }
31
+ /** Narrow an arbitrary DB `outcome` string to the typed union, so a row
32
+ * written via the manual path (`closeTaskExternal` outcome override) can
33
+ * flow back into `TaskEvaluatedEvent` without losing type-safety. */
34
+ function isPreservableOutcome(value) {
35
+ return value === 'success' || value === 'partial' || value === 'fail'
36
+ || value === 'pending' || value === 'abandoned';
37
+ }
19
38
  const DEFAULT_COMMANDS = {
20
39
  typescript: {
21
40
  lint: 'pnpm -w lint',
@@ -38,13 +57,27 @@ export class ApmeRunner {
38
57
  store;
39
58
  queue = [];
40
59
  drainPromise = null;
60
+ queuedRunIds = new Set();
61
+ runningRunIds = new Set();
62
+ runningTurnIds = new Set();
63
+ runningTaskIds = new Set();
41
64
  listeners = new Set();
65
+ taskListeners = new Set();
42
66
  configOverride = null;
43
67
  judgeOverride = null;
44
68
  detOverride = null;
69
+ /** Cached startup judge readiness probe. Populated by `refreshBackendProbe`,
70
+ * surfaced on /health. Null until the first probe completes. */
71
+ lastBackendProbe = null;
45
72
  constructor(store) {
46
73
  this.store = store;
47
74
  }
75
+ /** Probe the configured judge backend and cache the result. Safe to call
76
+ * fire-and-forget at daemon startup — failures don't throw. */
77
+ async refreshBackendProbe(cfg) {
78
+ this.lastBackendProbe = await probeJudgeBackend(cfg);
79
+ return this.lastBackendProbe;
80
+ }
48
81
  _setConfig(cfg) { this.configOverride = cfg; }
49
82
  _setJudgeFn(fn) {
50
83
  this.judgeOverride = fn;
@@ -56,9 +89,23 @@ export class ApmeRunner {
56
89
  this.listeners.add(fn);
57
90
  return () => this.listeners.delete(fn);
58
91
  }
92
+ /** Subscribe to per-task judge completions. The event fires AFTER the
93
+ * task_judge axis scores and summary are persisted, so listeners can read
94
+ * the final state directly from the event payload without re-querying.
95
+ * Used by the timeline emitter to upsert the corresponding `task_end` row
96
+ * with score + outcome metadata. */
97
+ onTaskEvaluated(fn) {
98
+ this.taskListeners.add(fn);
99
+ return () => this.taskListeners.delete(fn);
100
+ }
59
101
  enqueue(job) {
60
102
  if (!this.store.enabled)
61
103
  return;
104
+ if (this.queuedRunIds.has(job.runId) || this.runningRunIds.has(job.runId)) {
105
+ debug('APME', `skip duplicate eval enqueue runId=${job.runId}`);
106
+ return;
107
+ }
108
+ this.queuedRunIds.add(job.runId);
62
109
  this.queue.push(job);
63
110
  debug('APME', `enqueue eval runId=${job.runId} (queue=${this.queue.length})`);
64
111
  void this.drain();
@@ -69,7 +116,14 @@ export class ApmeRunner {
69
116
  enqueueTurn(job) {
70
117
  if (!this.store.enabled)
71
118
  return;
72
- void this.runTurnEval(job);
119
+ if (this.runningTurnIds.has(job.turnId)) {
120
+ debug('APME', `skip duplicate turn eval turnId=${job.turnId}`);
121
+ return;
122
+ }
123
+ this.runningTurnIds.add(job.turnId);
124
+ void this.runTurnEval(job).finally(() => {
125
+ this.runningTurnIds.delete(job.turnId);
126
+ });
73
127
  }
74
128
  /** Judge a closed task (group of turns between boundary signals —
75
129
  * TodoWrite all-completed, /clear, session_end). Fires-and-forgets;
@@ -77,7 +131,14 @@ export class ApmeRunner {
77
131
  enqueueTask(job) {
78
132
  if (!this.store.enabled)
79
133
  return;
80
- void this.runTaskEval(job);
134
+ if (this.runningTaskIds.has(job.taskId)) {
135
+ debug('APME', `skip duplicate task eval taskId=${job.taskId}`);
136
+ return;
137
+ }
138
+ this.runningTaskIds.add(job.taskId);
139
+ void this.runTaskEval(job).finally(() => {
140
+ this.runningTaskIds.delete(job.taskId);
141
+ });
81
142
  }
82
143
  async runTaskEval({ runId, taskId, category, boundarySignal }) {
83
144
  const cfg = this.configOverride ?? loadApmeConfig();
@@ -121,6 +182,13 @@ export class ApmeRunner {
121
182
  if (turns.length > TURN_CAP) {
122
183
  lines.push(`… (${turns.length - TURN_CAP} more turns omitted)`);
123
184
  }
185
+ // The canonical SessionSample gives the judge the typed tool trajectory
186
+ // (req #5/#6) and per-sample cost (req #7) — context the turn rows lack.
187
+ const sample = this.store.getSample(taskId);
188
+ const trajectoryLines = sample ? buildTrajectoryLines(sample) : [];
189
+ const costLine = sample
190
+ ? `cost: ${sample.cost.inputTokens}in/${sample.cost.outputTokens}out tok, $${sample.cost.costUsd.toFixed(4)}, model ${sample.model.modelId}`
191
+ : '';
124
192
  const judgePrompt = [
125
193
  rubric.prompt,
126
194
  '',
@@ -128,23 +196,28 @@ export class ApmeRunner {
128
196
  `task_category: ${category ?? task.taskCategory ?? 'unknown'}`,
129
197
  `turn_count: ${turns.length}`,
130
198
  `boundary_signal: ${boundarySignal ?? task.boundarySignal}`,
199
+ ...(costLine ? [costLine] : []),
131
200
  '',
132
201
  '--- TURNS ---',
133
202
  ...lines,
203
+ ...(trajectoryLines.length ? ['', '--- TOOL TRAJECTORY ---', ...trajectoryLines] : []),
134
204
  '',
135
205
  'Respond with strict JSON only.',
136
206
  ].join('\n');
137
207
  try {
138
- const judgeText = this.judgeOverride
139
- ? await this.judgeOverride(judgePrompt, cfg.judge)
140
- : await callJudge(judgePrompt, cfg.judge);
141
- const parsed = parseJudgeJson(judgeText);
208
+ // callJudgeWithMeta carries the effective backend label across the
209
+ // FM→MLX fallback path. Without it, eval rows produced by the MLX
210
+ // fallback would be misattributed to foundationModels in the DB.
211
+ const judgeResult = this.judgeOverride
212
+ ? { text: await this.judgeOverride(judgePrompt, cfg.judge), effectiveLabel: effectiveJudgeModelTag(cfg.judge) }
213
+ : await callJudgeWithMeta(judgePrompt, cfg.judge);
214
+ const parsed = parseJudgeJson(judgeResult.text);
142
215
  if (!parsed) {
143
216
  debug('APME', `runTaskEval parse failed task=${taskId.slice(0, 8)}`);
144
217
  return;
145
218
  }
146
219
  const now = Date.now();
147
- const judgeModel = effectiveJudgeModelTag(cfg.judge);
220
+ const judgeModel = judgeResult.effectiveLabel;
148
221
  for (const [axis, score] of Object.entries(parsed.scores)) {
149
222
  this.store.insertEvalForTask({
150
223
  id: 0,
@@ -165,22 +238,82 @@ export class ApmeRunner {
165
238
  createdAt: now,
166
239
  });
167
240
  }
241
+ // Pure sample-trajectory scorers (tool churn, error rate) — they add
242
+ // signal the LLM judge can miss and are cheap/deterministic. Stored under
243
+ // layer='trajectory' so they don't collide with task_judge axes.
244
+ if (sample) {
245
+ try {
246
+ for (const r of runSampleScorers(sample)) {
247
+ this.store.insertEvalForTask({
248
+ id: 0, runId, taskId,
249
+ layer: r.layer,
250
+ metric: r.metric,
251
+ score: r.score,
252
+ raw: r.reasoning ? JSON.stringify({ reasoning: r.reasoning, scorer: r.scorer }) : null,
253
+ rubricVer: null,
254
+ judgeModel: `scorer:${r.scorer}`,
255
+ createdAt: now,
256
+ });
257
+ }
258
+ }
259
+ catch (err) {
260
+ debug('APME', `sample scorers failed task=${taskId.slice(0, 8)}: ${String(err)}`);
261
+ }
262
+ }
263
+ const compositeScore = parsed.scores.overall ?? null;
264
+ const derivedOutcome = deriveTaskOutcome(compositeScore);
265
+ // Preserve a previously-set outcome — that only happens when the user
266
+ // explicitly closed the task via `agentdeck task done/cancel` (or the
267
+ // macOS detail-pane button) with an outcome override. Without this
268
+ // guard the async judge resolves 5–30 s after the manual close and
269
+ // overwrites e.g. `abandoned` with `partial`, silently losing the
270
+ // user's gesture. `closeTask` itself never writes outcome — only the
271
+ // manual path does — so a non-null read here is unambiguous.
272
+ const existingOutcome = this.store.getTask(taskId)?.outcome ?? null;
273
+ const taskOutcome = isPreservableOutcome(existingOutcome)
274
+ ? existingOutcome
275
+ : derivedOutcome;
168
276
  this.store.updateTask(taskId, {
169
277
  summary: parsed.summary ?? null,
170
- compositeScore: parsed.scores.overall ?? null,
278
+ compositeScore,
279
+ outcome: taskOutcome,
171
280
  notesJson: JSON.stringify({
172
281
  reasoning: parsed.reasoning,
173
282
  done: parsed.done,
174
283
  missed: parsed.missed,
175
284
  }),
176
285
  });
177
- debug('APME', `task eval ${taskId.slice(0, 8)}: overall=${parsed.scores.overall} summary=${parsed.summary?.slice(0, 40) ?? '-'}`);
286
+ debug('APME', `task eval ${taskId.slice(0, 8)}: overall=${compositeScore} outcome=${taskOutcome} summary=${parsed.summary?.slice(0, 40) ?? '-'}`);
178
287
  for (const fn of this.listeners) {
179
288
  try {
180
- fn({ runId, taskId, layer1Ran: false, layer2Ran: true, overall: parsed.scores.overall });
289
+ fn({ runId, taskId, layer1Ran: false, layer2Ran: true, overall: compositeScore ?? undefined });
181
290
  }
182
291
  catch { /* ignore */ }
183
292
  }
293
+ if (this.taskListeners.size > 0) {
294
+ const run = this.store.getRun(runId);
295
+ const updatedTask = this.store.getTask(taskId) ?? task;
296
+ const event = {
297
+ runId,
298
+ taskId,
299
+ sessionId: run?.sessionId ?? '',
300
+ agentType: run?.agentType ?? undefined,
301
+ projectName: run?.projectName ?? undefined,
302
+ startedAt: updatedTask.startedAt,
303
+ endedAt: updatedTask.endedAt ?? Date.now(),
304
+ compositeScore,
305
+ outcome: taskOutcome,
306
+ taskCategory: category ?? updatedTask.taskCategory ?? undefined,
307
+ summary: parsed.summary ?? undefined,
308
+ boundarySignal: boundarySignal ?? updatedTask.boundarySignal,
309
+ };
310
+ for (const fn of this.taskListeners) {
311
+ try {
312
+ fn(event);
313
+ }
314
+ catch { /* ignore */ }
315
+ }
316
+ }
184
317
  }
185
318
  catch (err) {
186
319
  debug('APME', `task eval error taskId=${taskId.slice(0, 8)}: ${String(err)}`);
@@ -226,14 +359,15 @@ export class ApmeRunner {
226
359
  'Respond with strict JSON only.',
227
360
  ].join('\n');
228
361
  try {
229
- const judgeText = this.judgeOverride
230
- ? await this.judgeOverride(judgePrompt, cfg.judge)
231
- : await callJudge(judgePrompt, cfg.judge);
232
- const parsed = parseJudgeJson(judgeText);
362
+ // Same fallback-aware labelling rule as runTaskEval — see comment there.
363
+ const judgeResult = this.judgeOverride
364
+ ? { text: await this.judgeOverride(judgePrompt, cfg.judge), effectiveLabel: effectiveJudgeModelTag(cfg.judge) }
365
+ : await callJudgeWithMeta(judgePrompt, cfg.judge);
366
+ const parsed = parseJudgeJson(judgeResult.text);
233
367
  if (!parsed)
234
368
  return;
235
369
  const now = Date.now();
236
- const judgeModel = effectiveJudgeModelTag(cfg.judge);
370
+ const judgeModel = judgeResult.effectiveLabel;
237
371
  for (const [axis, score] of Object.entries(parsed.scores)) {
238
372
  this.store.insertEvalForTurn({
239
373
  runId, turnId,
@@ -274,8 +408,14 @@ export class ApmeRunner {
274
408
  async doDrain() {
275
409
  while (this.queue.length > 0) {
276
410
  const job = this.queue.shift();
411
+ this.queuedRunIds.delete(job.runId);
412
+ this.runningRunIds.add(job.runId);
277
413
  try {
278
414
  const result = await this.runOne(job);
415
+ if (!result.layer1Ran && !result.layer2Ran && result.overall === undefined) {
416
+ debug('APME', `runner no-op runId=${job.runId} (no eval rows produced)`);
417
+ continue;
418
+ }
279
419
  for (const fn of this.listeners) {
280
420
  try {
281
421
  fn(result);
@@ -286,6 +426,9 @@ export class ApmeRunner {
286
426
  catch (err) {
287
427
  debug('APME', `runner error runId=${job.runId}: ${String(err)}`);
288
428
  }
429
+ finally {
430
+ this.runningRunIds.delete(job.runId);
431
+ }
289
432
  }
290
433
  }
291
434
  async runOne(job) {
@@ -295,6 +438,10 @@ export class ApmeRunner {
295
438
  debug('APME', `runOne: run ${job.runId} not found`);
296
439
  return { runId: job.runId, layer1Ran: false, layer2Ran: false };
297
440
  }
441
+ if (this.store.listEvalsForRun(run.id).length > 0) {
442
+ debug('APME', `runOne: run ${run.id} already has eval rows; skip duplicate`);
443
+ return { runId: job.runId, layer1Ran: false, layer2Ran: false };
444
+ }
298
445
  // ── Layer 1 — deterministic ───────────────────────────────────────────────
299
446
  let layer1Ran = false;
300
447
  let layer1Passed = null;
@@ -339,13 +486,14 @@ export class ApmeRunner {
339
486
  if (rubric) {
340
487
  try {
341
488
  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);
489
+ // Same fallback-aware labelling rule as runTaskEval — see comment there.
490
+ const judgeResult = this.judgeOverride
491
+ ? { text: await this.judgeOverride(prompt, cfg.judge), effectiveLabel: effectiveJudgeModelTag(cfg.judge) }
492
+ : await callJudgeWithMeta(prompt, cfg.judge);
493
+ const parsed = parseJudgeJson(judgeResult.text);
346
494
  if (parsed) {
347
495
  const now = Date.now();
348
- const judgeModel = effectiveJudgeModelTag(cfg.judge);
496
+ const judgeModel = judgeResult.effectiveLabel;
349
497
  for (const [axis, score] of Object.entries(parsed.scores)) {
350
498
  this.store.insertEval({
351
499
  runId: run.id,
@@ -530,6 +678,38 @@ function runCommand(command, cwd, timeoutMs) {
530
678
  });
531
679
  }
532
680
  // ─── Layer 2 execution ────────────────────────────────────────────────────────
681
+ /** Render a SessionSample's typed trajectory as compact judge-prompt lines.
682
+ * Gives the judge tool-call sequences + model usage the turn rows omit. */
683
+ export function buildTrajectoryLines(sample, cap = 30) {
684
+ const lines = [];
685
+ const events = sample.events.slice(0, cap);
686
+ for (const e of events) {
687
+ switch (e.kind) {
688
+ case 'tool': {
689
+ let input = '';
690
+ try {
691
+ input = e.input == null ? '' : JSON.stringify(e.input).slice(0, 120);
692
+ }
693
+ catch {
694
+ input = '';
695
+ }
696
+ lines.push(` tool ${e.name}(${input})${e.status ? ` → ${e.status}` : ''}${e.error ? ` [err: ${String(e.error).slice(0, 80)}]` : ''}`);
697
+ break;
698
+ }
699
+ case 'model':
700
+ lines.push(` model ${e.model}: ${e.inputTokens}in/${e.outputTokens}out tok${e.costUsd ? ` ($${e.costUsd.toFixed(4)})` : ''}`);
701
+ break;
702
+ case 'state':
703
+ lines.push(` state → ${e.to}`);
704
+ break;
705
+ default:
706
+ break; // user/assistant messages already shown in the TURNS section
707
+ }
708
+ }
709
+ if (sample.events.length > cap)
710
+ lines.push(` … (${sample.events.length - cap} more events)`);
711
+ return lines;
712
+ }
533
713
  // `ParsedJudge` is the canonical eval v1 type imported from `@agentdeck/shared`.
534
714
  const NON_CODE_CATEGORIES = new Set(['conversation', 'planning', 'research', 'review']);
535
715
  export function buildJudgePrompt(run, rubricPrompt, layer1Passed, store) {
@@ -619,29 +799,312 @@ export function effectiveJudgeModelTag(cfg) {
619
799
  return 'foundationModels:apple-intelligence';
620
800
  return `${cfg.backend}:${cfg.model}`;
621
801
  }
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);
802
+ /** Strip backend-specific fields when forcing a cfg through a different
803
+ * adapter. Without this, a FM cfg (`endpoint:"http://.../apme/judge/foundation-models"`,
804
+ * `model:"apple-intelligence"`) handed to `callMlx` would POST to the FM URL
805
+ * and request a model the MLX server has never heard of — silent failure.
806
+ * Mirrors the `resetBackendCoupledFields` path inside `loadApmeConfig`. */
807
+ export function sanitizeForMlx(judgeCfg) {
808
+ if (judgeCfg.backend === 'mlx' && !judgeCfg.endpoint && (!judgeCfg.model || judgeCfg.model === DEFAULT_APME_CONFIG.judge.model)) {
809
+ return judgeCfg;
810
+ }
811
+ return {
812
+ ...judgeCfg,
813
+ backend: 'mlx',
814
+ endpoint: undefined,
815
+ model: DEFAULT_APME_CONFIG.judge.model,
816
+ };
817
+ }
818
+ const FOUNDATION_MODELS_MISSING_DAEMON_TTL_MS = 15_000;
819
+ const FOUNDATION_MODELS_UNAVAILABLE_TTL_MS = 60_000;
820
+ const FOUNDATION_MODELS_READY_TTL_MS = 60_000;
821
+ let foundationModelsAutoCache = null;
822
+ let foundationModelsResolveInFlight = null;
823
+ function getFoundationModelsAutoCache(now = Date.now()) {
824
+ if (!foundationModelsAutoCache)
825
+ return null;
826
+ if (foundationModelsAutoCache.expiresAt <= now) {
827
+ foundationModelsAutoCache = null;
828
+ return null;
829
+ }
830
+ return foundationModelsAutoCache;
831
+ }
832
+ function markFoundationModelsAutoReady(url) {
833
+ foundationModelsAutoCache = {
834
+ state: 'ready',
835
+ url,
836
+ expiresAt: Date.now() + FOUNDATION_MODELS_READY_TTL_MS,
837
+ };
838
+ }
839
+ function markFoundationModelsAutoUnavailable(reason, ttlMs = FOUNDATION_MODELS_UNAVAILABLE_TTL_MS) {
840
+ foundationModelsAutoCache = {
841
+ state: 'unavailable',
842
+ reason,
843
+ expiresAt: Date.now() + ttlMs,
844
+ };
845
+ }
846
+ /** Test hook for cache-sensitive runner tests. Production code should not call this. */
847
+ export function clearFoundationModelsAutoCacheForTests() {
848
+ foundationModelsAutoCache = null;
849
+ foundationModelsResolveInFlight = null;
850
+ }
851
+ /** Like `callJudge`, but returns the effective backend + label so callers
852
+ * can record `judge_model` correctly across fallback paths. */
853
+ export async function callJudgeWithMeta(prompt, judgeCfg) {
629
854
  if (judgeCfg.backend === 'foundationModels') {
630
855
  try {
631
- return await callFoundationModels(prompt, judgeCfg);
856
+ const text = await callFoundationModels(prompt, judgeCfg);
857
+ return { text, effectiveBackend: 'foundationModels', effectiveLabel: effectiveJudgeModelTag(judgeCfg) };
632
858
  }
633
859
  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
860
+ // Cost-sensitive default: never route to a paid/network backend.
861
+ // Retry via local MLX only when fallbackToMlx is enabled; otherwise
636
862
  // propagate the error so the runner's try/catch skips this eval.
637
863
  if (judgeCfg.fallbackToMlx) {
638
864
  debug('APME', `foundationModels unavailable, fallback to MLX: ${String(err)}`);
639
- return callMlx(prompt, judgeCfg);
865
+ // sanitizeForMlx wipes the FM-specific endpoint/model so callMlx
866
+ // never POSTs to the FM endpoint or asks MLX for `apple-intelligence`.
867
+ // We then derive the label from the SANITIZED cfg — recording the
868
+ // effective backend, not the cfg the user originally requested.
869
+ const mlxCfg = sanitizeForMlx(judgeCfg);
870
+ const text = await callMlx(prompt, mlxCfg);
871
+ return { text, effectiveBackend: 'mlx', effectiveLabel: effectiveJudgeModelTag(mlxCfg) };
640
872
  }
641
873
  throw err;
642
874
  }
643
875
  }
644
- throw new Error(`unknown judge backend: ${String(judgeCfg.backend)}`);
876
+ let text;
877
+ if (judgeCfg.backend === 'mlx')
878
+ text = await callMlx(prompt, judgeCfg);
879
+ else if (judgeCfg.backend === 'openclaw')
880
+ text = await callOpenClaw(prompt, judgeCfg);
881
+ else if (judgeCfg.backend === 'api')
882
+ text = await callApi(prompt, judgeCfg);
883
+ else
884
+ throw new Error(`unknown judge backend: ${String(judgeCfg.backend)}`);
885
+ return { text, effectiveBackend: judgeCfg.backend, effectiveLabel: effectiveJudgeModelTag(judgeCfg) };
886
+ }
887
+ /** Backwards-compatible thin wrapper. Internal callers that need the actual
888
+ * effective backend (for DB labelling) should call `callJudgeWithMeta`. */
889
+ export async function callJudge(prompt, judgeCfg) {
890
+ return (await callJudgeWithMeta(prompt, judgeCfg)).text;
891
+ }
892
+ export async function probeJudgeBackend(cfg) {
893
+ const start = Date.now();
894
+ const checkedAt = start;
895
+ // Each branch must establish that the backend can ACTUALLY produce a judge
896
+ // response — not just that some HTTP port answers. Reachability without a
897
+ // usable model / SDK / on-device LLM yields false-positive "ready" that
898
+ // hides the real failure mode (silent eval skip later). Follow the rule:
899
+ // either invoke (cheap ping) or downgrade to `unavailable` with a reason.
900
+ try {
901
+ if (cfg.backend === 'mlx') {
902
+ const mlx = loadMlxSettings();
903
+ const url = cfg.endpoint ?? mlx.endpoint;
904
+ const base = url.replace(/\/v1\/chat\/completions$/, '').replace(/\/chat\/completions$/, '');
905
+ let model;
906
+ let modelsReachable = false;
907
+ for (const path of ['/v1/models', '/models']) {
908
+ const resp = await fetch(`${base}${path}`, { signal: AbortSignal.timeout(5000) }).catch(() => null);
909
+ if (resp?.ok) {
910
+ modelsReachable = true;
911
+ const json = await resp.json().catch(() => ({}));
912
+ model = json.data?.find(m => m.id && !m.id.toLowerCase().includes('nanollava'))?.id;
913
+ break;
914
+ }
915
+ }
916
+ if (!modelsReachable) {
917
+ return {
918
+ backend: 'mlx', status: 'unavailable',
919
+ reason: `MLX server unreachable at ${base}. Start with \`mlx_lm.server\` or set apme.judge.endpoint.`,
920
+ endpoint: base, checkedAt,
921
+ };
922
+ }
923
+ // Pinned/configured model overrides catalog discovery — the real call uses
924
+ // the same fallback chain as callMlx().
925
+ const pickedModel = mlx.model ?? cfg.model ?? model;
926
+ if (!pickedModel) {
927
+ return {
928
+ backend: 'mlx', status: 'unavailable',
929
+ 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 …\`.`,
930
+ endpoint: base, checkedAt,
931
+ };
932
+ }
933
+ // Cheapest possible inference probe: max_tokens=1, temperature=0. If MLX
934
+ // accepts this without a model error, callMlx() will succeed too.
935
+ const ping = await fetch(`${base}/v1/chat/completions`, {
936
+ method: 'POST',
937
+ headers: { 'Content-Type': 'application/json' },
938
+ body: JSON.stringify({
939
+ model: pickedModel,
940
+ messages: [{ role: 'user', content: 'ping' }],
941
+ max_tokens: 1, temperature: 0,
942
+ }),
943
+ signal: AbortSignal.timeout(8000),
944
+ }).catch((e) => ({ ok: false, status: 0, statusText: String(e).slice(0, 80) }));
945
+ if (!ping.ok) {
946
+ const detail = ping.status ? `HTTP ${ping.status}` : ping.statusText ?? 'no response';
947
+ return {
948
+ backend: 'mlx', status: 'unavailable',
949
+ reason: `MLX inference failed for model "${pickedModel}" (${detail}). Check that the model is actually loaded.`,
950
+ endpoint: base, model: pickedModel, checkedAt,
951
+ };
952
+ }
953
+ return { backend: 'mlx', status: 'ready', latencyMs: Date.now() - start, model: pickedModel, endpoint: base, checkedAt };
954
+ }
955
+ if (cfg.backend === 'openclaw') {
956
+ // OpenClaw Gateway: /health proves the gateway socket is up but does NOT
957
+ // prove /chat will route. The bridge's own gateway adapter performs an
958
+ // Ed25519 handshake against the same gateway; we don't replicate it
959
+ // here, but we DO require both /health AND a model catalog response so
960
+ // a stub gateway with /health = 200 doesn't pass.
961
+ const url = cfg.endpoint ?? 'http://127.0.0.1:18789';
962
+ const base = url.replace(/\/chat$/, '');
963
+ const health = await fetch(`${base}/health`, { signal: AbortSignal.timeout(5000) }).catch(() => null);
964
+ if (!health?.ok) {
965
+ return {
966
+ backend: 'openclaw', status: 'unavailable',
967
+ reason: `OpenClaw Gateway /health unreachable at ${base}.`,
968
+ endpoint: base, checkedAt,
969
+ };
970
+ }
971
+ const models = await fetch(`${base}/models`, { signal: AbortSignal.timeout(5000) }).catch(() => null);
972
+ if (!models?.ok) {
973
+ return {
974
+ backend: 'openclaw', status: 'unavailable',
975
+ reason: `OpenClaw Gateway /health responds but /models does not — gateway not fully initialised. Wait for handshake or check apme.judge.endpoint.`,
976
+ endpoint: base, checkedAt,
977
+ };
978
+ }
979
+ const json = await models.json().catch(() => ({}));
980
+ const list = json.data ?? json.models ?? [];
981
+ const requested = cfg.model;
982
+ if (requested && !list.some(m => m.id === requested)) {
983
+ return {
984
+ backend: 'openclaw', status: 'unavailable',
985
+ reason: `OpenClaw Gateway is up but model "${requested}" is not advertised. Available: ${list.slice(0, 5).map(m => m.id).join(', ')}`,
986
+ endpoint: base, checkedAt,
987
+ };
988
+ }
989
+ return {
990
+ backend: 'openclaw', status: 'ready',
991
+ latencyMs: Date.now() - start,
992
+ model: requested ?? list[0]?.id, endpoint: base, checkedAt,
993
+ };
994
+ }
995
+ if (cfg.backend === 'foundationModels') {
996
+ // Mirror callFoundationModels: explicit endpoint wins over auto-resolve.
997
+ const url = cfg.endpoint ?? await resolveFoundationModelsUrl();
998
+ if (!url) {
999
+ const helper = await probeFoundationModelsHelper();
1000
+ if (helper.available) {
1001
+ return {
1002
+ backend: 'foundationModels',
1003
+ status: 'ready',
1004
+ latencyMs: Date.now() - start,
1005
+ endpoint: helper.path ? `helper:${helper.path}` : 'helper',
1006
+ checkedAt,
1007
+ };
1008
+ }
1009
+ return {
1010
+ backend: 'foundationModels', status: 'unavailable',
1011
+ reason: `Swift daemon not found and helper unavailable: ${helper.reason ?? 'unknown'}`,
1012
+ checkedAt,
1013
+ };
1014
+ }
1015
+ // Foundation Models adapter on the Swift side returns either { text }
1016
+ // (ready) or { error: "unavailable", reason } (Apple Intelligence not
1017
+ // downloaded, model still warming, etc). A trivial ping forces that
1018
+ // signal up to us so we don't claim ready when the on-device LLM is
1019
+ // actually unusable.
1020
+ const ping = await fetch(url, {
1021
+ method: 'POST',
1022
+ headers: { 'Content-Type': 'application/json' },
1023
+ body: JSON.stringify({ prompt: 'ping' }),
1024
+ signal: AbortSignal.timeout(8000),
1025
+ }).catch((e) => ({ ok: false, status: 0, statusText: String(e).slice(0, 80) }));
1026
+ if (!ping.ok) {
1027
+ const detail = ping.status ? `HTTP ${ping.status}` : ping.statusText ?? 'no response';
1028
+ if (!cfg.endpoint) {
1029
+ markFoundationModelsAutoUnavailable(`Swift daemon FM endpoint did not accept probe (${detail}).`);
1030
+ const helper = await probeFoundationModelsHelper();
1031
+ if (helper.available) {
1032
+ return {
1033
+ backend: 'foundationModels',
1034
+ status: 'ready',
1035
+ latencyMs: Date.now() - start,
1036
+ endpoint: helper.path ? `helper:${helper.path}` : 'helper',
1037
+ checkedAt,
1038
+ };
1039
+ }
1040
+ }
1041
+ return {
1042
+ backend: 'foundationModels', status: 'unavailable',
1043
+ reason: `Swift daemon FM endpoint did not accept probe (${detail}).`,
1044
+ endpoint: url, checkedAt,
1045
+ };
1046
+ }
1047
+ const json = await ping.json().catch(() => ({}));
1048
+ if (json.error) {
1049
+ if (!cfg.endpoint) {
1050
+ markFoundationModelsAutoUnavailable(`Foundation Models ${json.error}: ${json.reason ?? 'no reason given'}.`);
1051
+ const helper = await probeFoundationModelsHelper();
1052
+ if (helper.available) {
1053
+ return {
1054
+ backend: 'foundationModels',
1055
+ status: 'ready',
1056
+ latencyMs: Date.now() - start,
1057
+ endpoint: helper.path ? `helper:${helper.path}` : 'helper',
1058
+ checkedAt,
1059
+ };
1060
+ }
1061
+ }
1062
+ return {
1063
+ backend: 'foundationModels', status: 'unavailable',
1064
+ reason: `Foundation Models ${json.error}: ${json.reason ?? 'no reason given'}. Apple Intelligence may not be downloaded yet.`,
1065
+ endpoint: url, checkedAt,
1066
+ };
1067
+ }
1068
+ if (!cfg.endpoint)
1069
+ markFoundationModelsAutoReady(url);
1070
+ return { backend: 'foundationModels', status: 'ready', latencyMs: Date.now() - start, endpoint: url, checkedAt };
1071
+ }
1072
+ if (cfg.backend === 'api') {
1073
+ // The 'api' backend is currently a STUB — callApi() always throws,
1074
+ // regardless of ANTHROPIC_API_KEY or @anthropic-ai/sdk presence. A probe
1075
+ // that returned 'ready' here would be a lie: the next judge call still
1076
+ // throws. Per cost-sensitive-defaults policy (memory:
1077
+ // feedback_cost_sensitive_defaults.md), local MLX is the supported path;
1078
+ // wiring real API judging is deliberately deferred. Surface that gap
1079
+ // honestly so users redirect to MLX/OpenClaw instead of debugging a
1080
+ // never-firing eval.
1081
+ const hasKey = !!process.env.ANTHROPIC_API_KEY;
1082
+ let sdkPresent = false;
1083
+ try {
1084
+ const r = createRequire(import.meta.url);
1085
+ r.resolve('@anthropic-ai/sdk');
1086
+ sdkPresent = true;
1087
+ }
1088
+ catch { /* not installed */ }
1089
+ const env = hasKey
1090
+ ? (sdkPresent ? 'key+SDK present' : 'key set, SDK missing')
1091
+ : 'no ANTHROPIC_API_KEY';
1092
+ return {
1093
+ backend: 'api', status: 'unavailable',
1094
+ reason: `Anthropic API judge backend is not implemented — callApi() is a stub that always throws. Switch apme.judge.backend to "mlx" or "openclaw". Environment: ${env}.`,
1095
+ checkedAt,
1096
+ };
1097
+ }
1098
+ return { backend: cfg.backend, status: 'unknown', checkedAt };
1099
+ }
1100
+ catch (err) {
1101
+ return {
1102
+ backend: cfg.backend, status: 'unavailable',
1103
+ reason: String(err).slice(0, 200),
1104
+ latencyMs: Date.now() - start,
1105
+ checkedAt,
1106
+ };
1107
+ }
645
1108
  }
646
1109
  async function callMlx(prompt, cfg) {
647
1110
  // MLX server speaks OpenAI chat-completions. The llm.mlx pin (shared with
@@ -709,58 +1172,111 @@ async function callOpenClaw(prompt, cfg) {
709
1172
  return json.text;
710
1173
  }
711
1174
  /**
712
- * Route a judge call to the Swift daemon's Foundation Models adapter.
1175
+ * Route a judge call to Foundation Models.
713
1176
  *
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.
1177
+ * Prefer the Swift daemon HTTP adapter when it is running; otherwise use the
1178
+ * bundled CLI Swift helper process. Default CLI config enables
1179
+ * `fallbackToMlx`, while callers can set it false to force a skip when
1180
+ * neither Foundation Models path works.
720
1181
  *
721
1182
  * Shape contract:
722
1183
  * Request : POST /apme/judge/foundation-models { prompt: string }
723
1184
  * Response : { text: string } | { error: "unavailable", reason: string }
724
1185
  */
725
1186
  async function callFoundationModels(prompt, cfg) {
1187
+ const explicitEndpoint = Boolean(cfg.endpoint);
1188
+ const cached = explicitEndpoint ? null : getFoundationModelsAutoCache();
1189
+ if (cached?.state === 'unavailable') {
1190
+ try {
1191
+ return await callFoundationModelsHelper(prompt);
1192
+ }
1193
+ catch (helperErr) {
1194
+ throw new Error(`foundationModels cached unavailable: ${cached.reason}; helper unavailable: ${String(helperErr)}`);
1195
+ }
1196
+ }
726
1197
  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'}`);
1198
+ if (!url) {
1199
+ try {
1200
+ return await callFoundationModelsHelper(prompt);
1201
+ }
1202
+ catch (helperErr) {
1203
+ throw new Error(`foundationModels: no Swift daemon found and helper unavailable: ${String(helperErr)}`);
1204
+ }
740
1205
  }
741
- if (typeof json.text !== 'string' || json.text.length === 0) {
742
- throw new Error('foundationModels returned empty text');
1206
+ try {
1207
+ const resp = await fetch(url, {
1208
+ method: 'POST',
1209
+ headers: { 'Content-Type': 'application/json' },
1210
+ body: JSON.stringify({ prompt }),
1211
+ signal: AbortSignal.timeout(60_000),
1212
+ });
1213
+ if (!resp.ok)
1214
+ throw new Error(`foundationModels HTTP ${resp.status}`);
1215
+ const json = await resp.json();
1216
+ if (json.error) {
1217
+ throw new Error(`foundationModels ${json.error}: ${json.reason ?? 'no reason'}`);
1218
+ }
1219
+ if (typeof json.text !== 'string' || json.text.length === 0) {
1220
+ throw new Error('foundationModels returned empty text');
1221
+ }
1222
+ if (!explicitEndpoint)
1223
+ markFoundationModelsAutoReady(url);
1224
+ return json.text;
1225
+ }
1226
+ catch (err) {
1227
+ if (!explicitEndpoint) {
1228
+ markFoundationModelsAutoUnavailable(String(err));
1229
+ try {
1230
+ return await callFoundationModelsHelper(prompt);
1231
+ }
1232
+ catch (helperErr) {
1233
+ throw new Error(`${String(err)}; helper unavailable: ${String(helperErr)}`);
1234
+ }
1235
+ }
1236
+ throw err;
743
1237
  }
744
- return json.text;
745
1238
  }
746
1239
  /** Best-effort resolver for the Swift daemon's FM endpoint. Returns null when
747
1240
  * no Swift daemon (httpPort ≠ port) is reachable. */
748
1241
  async function resolveFoundationModelsUrl() {
1242
+ const cached = getFoundationModelsAutoCache();
1243
+ if (cached?.state === 'ready')
1244
+ return cached.url;
1245
+ if (cached?.state === 'unavailable')
1246
+ return null;
1247
+ if (foundationModelsResolveInFlight)
1248
+ return foundationModelsResolveInFlight;
749
1249
  // Lazy-require to avoid pulling session-registry into every test bundle
750
1250
  // 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`;
1251
+ foundationModelsResolveInFlight = (async () => {
1252
+ try {
1253
+ const { findDaemonPortAsync } = await import('../session-registry.js');
1254
+ const info = await findDaemonPortAsync();
1255
+ if (!info) {
1256
+ markFoundationModelsAutoUnavailable('Swift daemon not found', FOUNDATION_MODELS_MISSING_DAEMON_TTL_MS);
1257
+ return null;
1258
+ }
1259
+ const port = info.httpPort ?? info.port;
1260
+ const url = `http://127.0.0.1:${port}/apme/judge/foundation-models`;
1261
+ markFoundationModelsAutoReady(url);
1262
+ return url;
1263
+ }
1264
+ finally {
1265
+ foundationModelsResolveInFlight = null;
1266
+ }
1267
+ })();
1268
+ return foundationModelsResolveInFlight;
757
1269
  }
758
1270
  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.');
1271
+ // Anthropic API judging is NOT implemented on the Node bridge. The macOS
1272
+ // Swift daemon (apple/AgentDeck/Daemon/Apme/ApmeJudgeApi.swift) is the only
1273
+ // place this backend works today. `loadApmeConfig` silently downgrades
1274
+ // settings.json `backend:"api"` to `"mlx"`, so reaching this stub means
1275
+ // either (a) a programmatic caller bypassed loadApmeConfig, or (b) something
1276
+ // raced between settings load and dispatch. Either way, throw with the
1277
+ // SAME diagnostic the probe surfaces so logs remain consistent.
1278
+ throw new Error('APME judge backend "api" is a stub on the Node bridge — callApi() always throws. ' +
1279
+ 'Use the macOS app for Anthropic API judging, or set apme.judge.backend to "mlx" / "openclaw" / "foundationModels".');
764
1280
  }
765
1281
  export function parseJudgeJson(text) {
766
1282
  // Models often wrap JSON in prose or code fences — grab the first {...} block.