@agentdeck/bridge 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/adapters/claude-code.d.ts +11 -27
- package/dist/adapters/claude-code.d.ts.map +1 -1
- package/dist/adapters/claude-code.js +29 -123
- package/dist/adapters/claude-code.js.map +1 -1
- package/dist/adapters/codex-cli.d.ts +21 -0
- package/dist/adapters/codex-cli.d.ts.map +1 -0
- package/dist/adapters/codex-cli.js +53 -0
- package/dist/adapters/codex-cli.js.map +1 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +13 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/monitor.d.ts +34 -0
- package/dist/adapters/monitor.d.ts.map +1 -0
- package/dist/adapters/monitor.js +86 -0
- package/dist/adapters/monitor.js.map +1 -0
- package/dist/adapters/openclaw.d.ts +36 -2
- package/dist/adapters/openclaw.d.ts.map +1 -1
- package/dist/adapters/openclaw.js +329 -59
- package/dist/adapters/openclaw.js.map +1 -1
- package/dist/adapters/opencode-adapter.d.ts +48 -0
- package/dist/adapters/opencode-adapter.d.ts.map +1 -0
- package/dist/adapters/opencode-adapter.js +345 -0
- package/dist/adapters/opencode-adapter.js.map +1 -0
- package/dist/adapters/pty-adapter.d.ts +56 -0
- package/dist/adapters/pty-adapter.d.ts.map +1 -0
- package/dist/adapters/pty-adapter.js +143 -0
- package/dist/adapters/pty-adapter.js.map +1 -0
- package/dist/adb-reverse.d.ts +25 -0
- package/dist/adb-reverse.d.ts.map +1 -0
- package/dist/adb-reverse.js +129 -0
- package/dist/adb-reverse.js.map +1 -0
- package/dist/antigravity-local.d.ts +7 -0
- package/dist/antigravity-local.d.ts.map +1 -0
- package/dist/antigravity-local.js +153 -0
- package/dist/antigravity-local.js.map +1 -0
- package/dist/apme/adapters/claude-hook.d.ts +15 -0
- package/dist/apme/adapters/claude-hook.d.ts.map +1 -0
- package/dist/apme/adapters/claude-hook.js +85 -0
- package/dist/apme/adapters/claude-hook.js.map +1 -0
- package/dist/apme/adapters/claude-pty.d.ts +28 -0
- package/dist/apme/adapters/claude-pty.d.ts.map +1 -0
- package/dist/apme/adapters/claude-pty.js +88 -0
- package/dist/apme/adapters/claude-pty.js.map +1 -0
- package/dist/apme/adapters/timeline.d.ts +16 -0
- package/dist/apme/adapters/timeline.d.ts.map +1 -0
- package/dist/apme/adapters/timeline.js +66 -0
- package/dist/apme/adapters/timeline.js.map +1 -0
- package/dist/apme/classifier.d.ts +53 -0
- package/dist/apme/classifier.d.ts.map +1 -0
- package/dist/apme/classifier.js +266 -0
- package/dist/apme/classifier.js.map +1 -0
- package/dist/apme/claude-transcript-reader.d.ts +38 -0
- package/dist/apme/claude-transcript-reader.d.ts.map +1 -0
- package/dist/apme/claude-transcript-reader.js +127 -0
- package/dist/apme/claude-transcript-reader.js.map +1 -0
- package/dist/apme/collector.d.ts +108 -0
- package/dist/apme/collector.d.ts.map +1 -0
- package/dist/apme/collector.js +659 -0
- package/dist/apme/collector.js.map +1 -0
- package/dist/apme/dashboard-html.d.ts +9 -0
- package/dist/apme/dashboard-html.d.ts.map +1 -0
- package/dist/apme/dashboard-html.js +416 -0
- package/dist/apme/dashboard-html.js.map +1 -0
- package/dist/apme/http.d.ts +19 -0
- package/dist/apme/http.d.ts.map +1 -0
- package/dist/apme/http.js +210 -0
- package/dist/apme/http.js.map +1 -0
- package/dist/apme/hw-sampler.d.ts +30 -0
- package/dist/apme/hw-sampler.d.ts.map +1 -0
- package/dist/apme/hw-sampler.js +106 -0
- package/dist/apme/hw-sampler.js.map +1 -0
- package/dist/apme/index.d.ts +43 -0
- package/dist/apme/index.d.ts.map +1 -0
- package/dist/apme/index.js +75 -0
- package/dist/apme/index.js.map +1 -0
- package/dist/apme/outcome.d.ts +48 -0
- package/dist/apme/outcome.d.ts.map +1 -0
- package/dist/apme/outcome.js +232 -0
- package/dist/apme/outcome.js.map +1 -0
- package/dist/apme/recommend.d.ts +32 -0
- package/dist/apme/recommend.d.ts.map +1 -0
- package/dist/apme/recommend.js +41 -0
- package/dist/apme/recommend.js.map +1 -0
- package/dist/apme/runner.d.ts +94 -0
- package/dist/apme/runner.d.ts.map +1 -0
- package/dist/apme/runner.js +812 -0
- package/dist/apme/runner.js.map +1 -0
- package/dist/apme/settings.d.ts +65 -0
- package/dist/apme/settings.d.ts.map +1 -0
- package/dist/apme/settings.js +97 -0
- package/dist/apme/settings.js.map +1 -0
- package/dist/apme/store.d.ts +99 -0
- package/dist/apme/store.d.ts.map +1 -0
- package/dist/apme/store.js +911 -0
- package/dist/apme/store.js.map +1 -0
- package/dist/apme/tuner.d.ts +64 -0
- package/dist/apme/tuner.d.ts.map +1 -0
- package/dist/apme/tuner.js +297 -0
- package/dist/apme/tuner.js.map +1 -0
- package/dist/apme/types.d.ts +11 -0
- package/dist/apme/types.d.ts.map +1 -0
- package/dist/apme/types.js +10 -0
- package/dist/apme/types.js.map +1 -0
- package/dist/auth.d.ts +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +15 -5
- package/dist/auth.js.map +1 -1
- package/dist/bridge-core.d.ts +162 -0
- package/dist/bridge-core.d.ts.map +1 -0
- package/dist/bridge-core.js +627 -0
- package/dist/bridge-core.js.map +1 -0
- package/dist/check-deps.d.ts +3 -1
- package/dist/check-deps.d.ts.map +1 -1
- package/dist/check-deps.js +34 -13
- package/dist/check-deps.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1235 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex-auth.d.ts +10 -0
- package/dist/codex-auth.d.ts.map +1 -0
- package/dist/codex-auth.js +63 -0
- package/dist/codex-auth.js.map +1 -0
- package/dist/codex-output-parser.d.ts +21 -0
- package/dist/codex-output-parser.d.ts.map +1 -0
- package/dist/codex-output-parser.js +253 -0
- package/dist/codex-output-parser.js.map +1 -0
- package/dist/d200h/hid-protocol.d.ts +62 -0
- package/dist/d200h/hid-protocol.d.ts.map +1 -0
- package/dist/d200h/hid-protocol.js +124 -0
- package/dist/d200h/hid-protocol.js.map +1 -0
- package/dist/d200h/image-renderer.d.ts +36 -0
- package/dist/d200h/image-renderer.d.ts.map +1 -0
- package/dist/d200h/image-renderer.js +487 -0
- package/dist/d200h/image-renderer.js.map +1 -0
- package/dist/daemon-server.d.ts +19 -0
- package/dist/daemon-server.d.ts.map +1 -0
- package/dist/daemon-server.js +1168 -0
- package/dist/daemon-server.js.map +1 -0
- package/dist/daemon-ws-client.d.ts +82 -0
- package/dist/daemon-ws-client.d.ts.map +1 -0
- package/dist/daemon-ws-client.js +179 -0
- package/dist/daemon-ws-client.js.map +1 -0
- package/dist/daemon.d.ts +10 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +56 -0
- package/dist/daemon.js.map +1 -0
- package/dist/display-monitor.d.ts +25 -0
- package/dist/display-monitor.d.ts.map +1 -0
- package/dist/display-monitor.js +143 -0
- package/dist/display-monitor.js.map +1 -0
- package/dist/esp32-serial.d.ts +112 -0
- package/dist/esp32-serial.d.ts.map +1 -0
- package/dist/esp32-serial.js +502 -0
- package/dist/esp32-serial.js.map +1 -0
- package/dist/gateway-probe.d.ts +12 -0
- package/dist/gateway-probe.d.ts.map +1 -0
- package/dist/gateway-probe.js +39 -0
- package/dist/gateway-probe.js.map +1 -0
- package/dist/hook-server.d.ts +29 -0
- package/dist/hook-server.d.ts.map +1 -1
- package/dist/hook-server.js +366 -8
- package/dist/hook-server.js.map +1 -1
- package/dist/index.d.ts +20 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1305 -562
- package/dist/index.js.map +1 -1
- package/dist/log-stream.d.ts +26 -0
- package/dist/log-stream.d.ts.map +1 -0
- package/dist/log-stream.js +111 -0
- package/dist/log-stream.js.map +1 -0
- package/dist/logger.d.ts +9 -2
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +36 -5
- package/dist/logger.js.map +1 -1
- package/dist/mdns.d.ts +15 -1
- package/dist/mdns.d.ts.map +1 -1
- package/dist/mdns.js +143 -28
- package/dist/mdns.js.map +1 -1
- package/dist/mlx-probe.d.ts +14 -0
- package/dist/mlx-probe.d.ts.map +1 -0
- package/dist/mlx-probe.js +44 -0
- package/dist/mlx-probe.js.map +1 -0
- package/dist/model-catalog.d.ts +5 -3
- package/dist/model-catalog.d.ts.map +1 -1
- package/dist/model-catalog.js +45 -29
- package/dist/model-catalog.js.map +1 -1
- package/dist/modules/adb-module.d.ts +14 -0
- package/dist/modules/adb-module.d.ts.map +1 -0
- package/dist/modules/adb-module.js +36 -0
- package/dist/modules/adb-module.js.map +1 -0
- package/dist/modules/d200h-module.d.ts +43 -0
- package/dist/modules/d200h-module.d.ts.map +1 -0
- package/dist/modules/d200h-module.js +360 -0
- package/dist/modules/d200h-module.js.map +1 -0
- package/dist/modules/d200h-renderer.d.ts +7 -0
- package/dist/modules/d200h-renderer.d.ts.map +1 -0
- package/dist/modules/d200h-renderer.js +321 -0
- package/dist/modules/d200h-renderer.js.map +1 -0
- package/dist/modules/index.d.ts +23 -0
- package/dist/modules/index.d.ts.map +1 -0
- package/dist/modules/index.js +65 -0
- package/dist/modules/index.js.map +1 -0
- package/dist/modules/mdns-module.d.ts +12 -0
- package/dist/modules/mdns-module.d.ts.map +1 -0
- package/dist/modules/mdns-module.js +21 -0
- package/dist/modules/mdns-module.js.map +1 -0
- package/dist/modules/pixoo-module.d.ts +8 -0
- package/dist/modules/pixoo-module.d.ts.map +1 -0
- package/dist/modules/pixoo-module.js +24 -0
- package/dist/modules/pixoo-module.js.map +1 -0
- package/dist/modules/serial-module.d.ts +18 -0
- package/dist/modules/serial-module.d.ts.map +1 -0
- package/dist/modules/serial-module.js +47 -0
- package/dist/modules/serial-module.js.map +1 -0
- package/dist/modules/types.d.ts +39 -0
- package/dist/modules/types.d.ts.map +1 -0
- package/dist/modules/types.js +2 -0
- package/dist/modules/types.js.map +1 -0
- package/dist/ollama-probe.d.ts +13 -0
- package/dist/ollama-probe.d.ts.map +1 -0
- package/dist/ollama-probe.js +46 -0
- package/dist/ollama-probe.js.map +1 -0
- package/dist/opencode-client.d.ts +126 -0
- package/dist/opencode-client.d.ts.map +1 -0
- package/dist/opencode-client.js +181 -0
- package/dist/opencode-client.js.map +1 -0
- package/dist/output-parser.d.ts +3 -0
- package/dist/output-parser.d.ts.map +1 -1
- package/dist/output-parser.js +29 -3
- package/dist/output-parser.js.map +1 -1
- package/dist/passive-observer.d.ts +38 -0
- package/dist/passive-observer.d.ts.map +1 -0
- package/dist/passive-observer.js +632 -0
- package/dist/passive-observer.js.map +1 -0
- package/dist/pixoo/pixoo-bridge.d.ts +50 -0
- package/dist/pixoo/pixoo-bridge.d.ts.map +1 -0
- package/dist/pixoo/pixoo-bridge.js +280 -0
- package/dist/pixoo/pixoo-bridge.js.map +1 -0
- package/dist/pixoo/pixoo-camera.d.ts +80 -0
- package/dist/pixoo/pixoo-camera.d.ts.map +1 -0
- package/dist/pixoo/pixoo-camera.js +271 -0
- package/dist/pixoo/pixoo-camera.js.map +1 -0
- package/dist/pixoo/pixoo-client.d.ts +67 -0
- package/dist/pixoo/pixoo-client.d.ts.map +1 -0
- package/dist/pixoo/pixoo-client.js +335 -0
- package/dist/pixoo/pixoo-client.js.map +1 -0
- package/dist/pixoo/pixoo-font.d.ts +24 -0
- package/dist/pixoo/pixoo-font.d.ts.map +1 -0
- package/dist/pixoo/pixoo-font.js +121 -0
- package/dist/pixoo/pixoo-font.js.map +1 -0
- package/dist/pixoo/pixoo-renderer.d.ts +32 -0
- package/dist/pixoo/pixoo-renderer.d.ts.map +1 -0
- package/dist/pixoo/pixoo-renderer.js +696 -0
- package/dist/pixoo/pixoo-renderer.js.map +1 -0
- package/dist/pixoo/pixoo-settings.d.ts +10 -0
- package/dist/pixoo/pixoo-settings.d.ts.map +1 -0
- package/dist/pixoo/pixoo-settings.js +47 -0
- package/dist/pixoo/pixoo-settings.js.map +1 -0
- package/dist/pixoo/pixoo-sprites.d.ts +187 -0
- package/dist/pixoo/pixoo-sprites.d.ts.map +1 -0
- package/dist/pixoo/pixoo-sprites.js +926 -0
- package/dist/pixoo/pixoo-sprites.js.map +1 -0
- package/dist/pty-manager.d.ts +1 -1
- package/dist/pty-manager.d.ts.map +1 -1
- package/dist/pty-manager.js +36 -15
- package/dist/pty-manager.js.map +1 -1
- package/dist/session-aggregator.d.ts +41 -0
- package/dist/session-aggregator.d.ts.map +1 -0
- package/dist/session-aggregator.js +76 -0
- package/dist/session-aggregator.js.map +1 -0
- package/dist/session-focus-relay.d.ts +35 -0
- package/dist/session-focus-relay.d.ts.map +1 -0
- package/dist/session-focus-relay.js +127 -0
- package/dist/session-focus-relay.js.map +1 -0
- package/dist/session-registry.d.ts +88 -1
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/session-registry.js +222 -15
- package/dist/session-registry.js.map +1 -1
- package/dist/session-timeline-relay.d.ts +27 -0
- package/dist/session-timeline-relay.d.ts.map +1 -0
- package/dist/session-timeline-relay.js +124 -0
- package/dist/session-timeline-relay.js.map +1 -0
- package/dist/state-machine.d.ts +1 -0
- package/dist/state-machine.d.ts.map +1 -1
- package/dist/state-machine.js +14 -0
- package/dist/state-machine.js.map +1 -1
- package/dist/terminal-status.d.ts +28 -0
- package/dist/terminal-status.d.ts.map +1 -0
- package/dist/terminal-status.js +152 -0
- package/dist/terminal-status.js.map +1 -0
- package/dist/timeline-store.d.ts +22 -0
- package/dist/timeline-store.d.ts.map +1 -0
- package/dist/timeline-store.js +79 -0
- package/dist/timeline-store.js.map +1 -0
- package/dist/timeline-summarizer.d.ts +14 -0
- package/dist/timeline-summarizer.d.ts.map +1 -0
- package/dist/timeline-summarizer.js +154 -0
- package/dist/timeline-summarizer.js.map +1 -0
- package/dist/tts.d.ts +25 -0
- package/dist/tts.d.ts.map +1 -0
- package/dist/tts.js +99 -0
- package/dist/tts.js.map +1 -0
- package/dist/tui/ansi.d.ts +92 -0
- package/dist/tui/ansi.d.ts.map +1 -0
- package/dist/tui/ansi.js +181 -0
- package/dist/tui/ansi.js.map +1 -0
- package/dist/tui/dashboard.d.ts +35 -0
- package/dist/tui/dashboard.d.ts.map +1 -0
- package/dist/tui/dashboard.js +436 -0
- package/dist/tui/dashboard.js.map +1 -0
- package/dist/tui/gauge.d.ts +15 -0
- package/dist/tui/gauge.d.ts.map +1 -0
- package/dist/tui/gauge.js +82 -0
- package/dist/tui/gauge.js.map +1 -0
- package/dist/tui/renderer.d.ts +15 -0
- package/dist/tui/renderer.d.ts.map +1 -0
- package/dist/tui/renderer.js +758 -0
- package/dist/tui/renderer.js.map +1 -0
- package/dist/tui/screen.d.ts +27 -0
- package/dist/tui/screen.d.ts.map +1 -0
- package/dist/tui/screen.js +109 -0
- package/dist/tui/screen.js.map +1 -0
- package/dist/tui/terrarium.d.ts +96 -0
- package/dist/tui/terrarium.d.ts.map +1 -0
- package/dist/tui/terrarium.js +912 -0
- package/dist/tui/terrarium.js.map +1 -0
- package/dist/types.d.ts +4 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -2
- package/dist/types.js.map +1 -1
- package/dist/usage-api.d.ts +9 -2
- package/dist/usage-api.d.ts.map +1 -1
- package/dist/usage-api.js +224 -42
- package/dist/usage-api.js.map +1 -1
- package/dist/usage-event.d.ts +12 -0
- package/dist/usage-event.d.ts.map +1 -0
- package/dist/usage-event.js +78 -0
- package/dist/usage-event.js.map +1 -0
- package/dist/usage-tracker.d.ts +4 -0
- package/dist/usage-tracker.d.ts.map +1 -1
- package/dist/usage-tracker.js +17 -0
- package/dist/usage-tracker.js.map +1 -1
- package/dist/utility-proxy.d.ts +30 -0
- package/dist/utility-proxy.d.ts.map +1 -0
- package/dist/utility-proxy.js +217 -0
- package/dist/utility-proxy.js.map +1 -0
- package/dist/utils/project-name.d.ts +22 -0
- package/dist/utils/project-name.d.ts.map +1 -0
- package/dist/utils/project-name.js +72 -0
- package/dist/utils/project-name.js.map +1 -0
- package/dist/version-check.d.ts +26 -0
- package/dist/version-check.d.ts.map +1 -0
- package/dist/version-check.js +225 -0
- package/dist/version-check.js.map +1 -0
- package/dist/voice-assistant.d.ts +63 -0
- package/dist/voice-assistant.d.ts.map +1 -0
- package/dist/voice-assistant.js +406 -0
- package/dist/voice-assistant.js.map +1 -0
- package/dist/voice.d.ts +2 -0
- package/dist/voice.d.ts.map +1 -1
- package/dist/voice.js +26 -1
- package/dist/voice.js.map +1 -1
- package/dist/wake-word.d.ts +35 -0
- package/dist/wake-word.d.ts.map +1 -0
- package/dist/wake-word.js +225 -0
- package/dist/wake-word.js.map +1 -0
- package/dist/whisper-server-manager.d.ts +1 -1
- package/dist/whisper-server-manager.js +1 -1
- package/dist/wifi-config.d.ts +24 -0
- package/dist/wifi-config.d.ts.map +1 -0
- package/dist/wifi-config.js +84 -0
- package/dist/wifi-config.js.map +1 -0
- package/dist/ws-server.d.ts +12 -0
- package/dist/ws-server.d.ts.map +1 -1
- package/dist/ws-server.js +81 -3
- package/dist/ws-server.js.map +1 -1
- package/package.json +22 -13
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* APME Evaluation Runner — two-layer pipeline executed after a run closes.
|
|
3
|
+
*
|
|
4
|
+
* Layer 1 (deterministic): detect project language from the run's projectPath,
|
|
5
|
+
* run lint/build/test in-place with a hard timeout, normalize each outcome to
|
|
6
|
+
* 0/1 in `evals` (metrics: lint_clean, build_ok, tests_pass).
|
|
7
|
+
*
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
import { spawn } from 'child_process';
|
|
14
|
+
import { existsSync, readdirSync } from 'fs';
|
|
15
|
+
import { debug } from '../logger.js';
|
|
16
|
+
import { loadApmeConfig, shouldJudge } from './settings.js';
|
|
17
|
+
import { loadMlxSettings, mlxChatUrl } from '@agentdeck/shared';
|
|
18
|
+
import { execSync } from 'child_process';
|
|
19
|
+
const DEFAULT_COMMANDS = {
|
|
20
|
+
typescript: {
|
|
21
|
+
lint: 'pnpm -w lint',
|
|
22
|
+
build: 'pnpm -r build',
|
|
23
|
+
test: 'pnpm -w test',
|
|
24
|
+
},
|
|
25
|
+
swift: {
|
|
26
|
+
test: (cwd) => {
|
|
27
|
+
// Auto-detect scheme from the project — don't hardcode "AgentDeck".
|
|
28
|
+
const scheme = detectSwiftScheme(cwd);
|
|
29
|
+
return scheme ? `xcodebuild test -scheme ${scheme} -quiet` : null;
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
kotlin: {
|
|
33
|
+
test: './gradlew testDebugUnitTest',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
// ─── Runner ────────────────────────────────────────────────────────────────────
|
|
37
|
+
export class ApmeRunner {
|
|
38
|
+
store;
|
|
39
|
+
queue = [];
|
|
40
|
+
drainPromise = null;
|
|
41
|
+
listeners = new Set();
|
|
42
|
+
configOverride = null;
|
|
43
|
+
judgeOverride = null;
|
|
44
|
+
detOverride = null;
|
|
45
|
+
constructor(store) {
|
|
46
|
+
this.store = store;
|
|
47
|
+
}
|
|
48
|
+
_setConfig(cfg) { this.configOverride = cfg; }
|
|
49
|
+
_setJudgeFn(fn) {
|
|
50
|
+
this.judgeOverride = fn;
|
|
51
|
+
}
|
|
52
|
+
_setDeterministicFn(fn) {
|
|
53
|
+
this.detOverride = fn;
|
|
54
|
+
}
|
|
55
|
+
onResult(fn) {
|
|
56
|
+
this.listeners.add(fn);
|
|
57
|
+
return () => this.listeners.delete(fn);
|
|
58
|
+
}
|
|
59
|
+
enqueue(job) {
|
|
60
|
+
if (!this.store.enabled)
|
|
61
|
+
return;
|
|
62
|
+
this.queue.push(job);
|
|
63
|
+
debug('APME', `enqueue eval runId=${job.runId} (queue=${this.queue.length})`);
|
|
64
|
+
void this.drain();
|
|
65
|
+
}
|
|
66
|
+
/** Immediately judge a single completed turn (mid-session eval).
|
|
67
|
+
* Used for non-code categories where turn prompt+response is the eval unit.
|
|
68
|
+
* Fires-and-forgets; result is stored and notified via onResult listeners. */
|
|
69
|
+
enqueueTurn(job) {
|
|
70
|
+
if (!this.store.enabled)
|
|
71
|
+
return;
|
|
72
|
+
void this.runTurnEval(job);
|
|
73
|
+
}
|
|
74
|
+
/** Judge a closed task (group of turns between boundary signals —
|
|
75
|
+
* TodoWrite all-completed, /clear, session_end). Fires-and-forgets;
|
|
76
|
+
* task-level summary and axis scores are persisted in tasks + evals. */
|
|
77
|
+
enqueueTask(job) {
|
|
78
|
+
if (!this.store.enabled)
|
|
79
|
+
return;
|
|
80
|
+
void this.runTaskEval(job);
|
|
81
|
+
}
|
|
82
|
+
async runTaskEval({ runId, taskId, category, boundarySignal }) {
|
|
83
|
+
const cfg = this.configOverride ?? loadApmeConfig();
|
|
84
|
+
if (!cfg.enabled)
|
|
85
|
+
return;
|
|
86
|
+
const task = this.store.getTask(taskId);
|
|
87
|
+
if (!task)
|
|
88
|
+
return;
|
|
89
|
+
const turns = this.store.listTurnsForTask(taskId);
|
|
90
|
+
if (turns.length === 0)
|
|
91
|
+
return;
|
|
92
|
+
// Skip tasks whose turns carry no meaningful text — all tool_only/empty.
|
|
93
|
+
const anyText = turns.some((t) => {
|
|
94
|
+
const kind = readResponseKind(t);
|
|
95
|
+
if (kind === 'text')
|
|
96
|
+
return true;
|
|
97
|
+
const prompt = typeof t.prompt === 'string' ? t.prompt.trim() : '';
|
|
98
|
+
return prompt.length > 0;
|
|
99
|
+
});
|
|
100
|
+
if (!anyText) {
|
|
101
|
+
debug('APME', `runTaskEval skip task=${taskId.slice(0, 8)} — no text`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Select rubric: task_rollup preferred, fall back to category, then general.
|
|
105
|
+
const rubric = this.store.getCurrentRubric('task_rollup')
|
|
106
|
+
?? (category ? this.store.getCurrentRubric(category) : null)
|
|
107
|
+
?? this.store.getCurrentRubric('general');
|
|
108
|
+
if (!rubric)
|
|
109
|
+
return;
|
|
110
|
+
const TURN_CAP = 10;
|
|
111
|
+
const clipped = turns.slice(0, TURN_CAP);
|
|
112
|
+
const lines = [];
|
|
113
|
+
for (const t of clipped) {
|
|
114
|
+
const idx = t.turn_index;
|
|
115
|
+
const prompt = (t.prompt ?? '').slice(0, 1500);
|
|
116
|
+
const response = (t.response ?? '').slice(0, 2500);
|
|
117
|
+
lines.push(`[Turn ${idx}] User: ${prompt || '(empty)'}`);
|
|
118
|
+
if (response)
|
|
119
|
+
lines.push(`Agent: ${response}`);
|
|
120
|
+
}
|
|
121
|
+
if (turns.length > TURN_CAP) {
|
|
122
|
+
lines.push(`… (${turns.length - TURN_CAP} more turns omitted)`);
|
|
123
|
+
}
|
|
124
|
+
const judgePrompt = [
|
|
125
|
+
rubric.prompt,
|
|
126
|
+
'',
|
|
127
|
+
'--- TASK CONTEXT ---',
|
|
128
|
+
`task_category: ${category ?? task.taskCategory ?? 'unknown'}`,
|
|
129
|
+
`turn_count: ${turns.length}`,
|
|
130
|
+
`boundary_signal: ${boundarySignal ?? task.boundarySignal}`,
|
|
131
|
+
'',
|
|
132
|
+
'--- TURNS ---',
|
|
133
|
+
...lines,
|
|
134
|
+
'',
|
|
135
|
+
'Respond with strict JSON only.',
|
|
136
|
+
].join('\n');
|
|
137
|
+
try {
|
|
138
|
+
const judgeText = this.judgeOverride
|
|
139
|
+
? await this.judgeOverride(judgePrompt, cfg.judge)
|
|
140
|
+
: await callJudge(judgePrompt, cfg.judge);
|
|
141
|
+
const parsed = parseJudgeJson(judgeText);
|
|
142
|
+
if (!parsed) {
|
|
143
|
+
debug('APME', `runTaskEval parse failed task=${taskId.slice(0, 8)}`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
const judgeModel = effectiveJudgeModelTag(cfg.judge);
|
|
148
|
+
for (const [axis, score] of Object.entries(parsed.scores)) {
|
|
149
|
+
this.store.insertEvalForTask({
|
|
150
|
+
id: 0,
|
|
151
|
+
runId, taskId,
|
|
152
|
+
layer: 'task_judge',
|
|
153
|
+
metric: axis,
|
|
154
|
+
score,
|
|
155
|
+
raw: axis === 'overall'
|
|
156
|
+
? JSON.stringify({
|
|
157
|
+
summary: parsed.summary,
|
|
158
|
+
reasoning: parsed.reasoning,
|
|
159
|
+
done: parsed.done,
|
|
160
|
+
missed: parsed.missed,
|
|
161
|
+
})
|
|
162
|
+
: null,
|
|
163
|
+
rubricVer: rubric.version,
|
|
164
|
+
judgeModel,
|
|
165
|
+
createdAt: now,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
this.store.updateTask(taskId, {
|
|
169
|
+
summary: parsed.summary ?? null,
|
|
170
|
+
compositeScore: parsed.scores.overall ?? null,
|
|
171
|
+
notesJson: JSON.stringify({
|
|
172
|
+
reasoning: parsed.reasoning,
|
|
173
|
+
done: parsed.done,
|
|
174
|
+
missed: parsed.missed,
|
|
175
|
+
}),
|
|
176
|
+
});
|
|
177
|
+
debug('APME', `task eval ${taskId.slice(0, 8)}: overall=${parsed.scores.overall} summary=${parsed.summary?.slice(0, 40) ?? '-'}`);
|
|
178
|
+
for (const fn of this.listeners) {
|
|
179
|
+
try {
|
|
180
|
+
fn({ runId, taskId, layer1Ran: false, layer2Ran: true, overall: parsed.scores.overall });
|
|
181
|
+
}
|
|
182
|
+
catch { /* ignore */ }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
debug('APME', `task eval error taskId=${taskId.slice(0, 8)}: ${String(err)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async runTurnEval({ runId, turnId, category }) {
|
|
190
|
+
const cfg = this.configOverride ?? loadApmeConfig();
|
|
191
|
+
if (!cfg.enabled)
|
|
192
|
+
return;
|
|
193
|
+
const turn = this.store.getTurn(turnId);
|
|
194
|
+
if (!turn)
|
|
195
|
+
return;
|
|
196
|
+
const prompt = turn.prompt ?? '';
|
|
197
|
+
const response = turn.response ?? '';
|
|
198
|
+
if (!prompt && !response)
|
|
199
|
+
return; // nothing to judge
|
|
200
|
+
// Skip turns the agent answered with tool calls only (or not at all) —
|
|
201
|
+
// the rubric prompt can't score "silence" meaningfully and doing so
|
|
202
|
+
// generates noise scores. `tool_only` / `empty` tags are set by the
|
|
203
|
+
// collector in turns.efficiency_json.
|
|
204
|
+
const kind = readResponseKind(turn);
|
|
205
|
+
if (kind === 'tool_only' || kind === 'empty') {
|
|
206
|
+
debug('APME', `runTurnEval skip turn=${turnId.slice(0, 8)} kind=${kind}`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// Select rubric by category (conversation/planning/research/review)
|
|
210
|
+
const rubric = (category ? this.store.getCurrentRubric(category) : null)
|
|
211
|
+
?? this.store.getCurrentRubric('conversation'); // sensible fallback
|
|
212
|
+
if (!rubric)
|
|
213
|
+
return;
|
|
214
|
+
const judgePrompt = [
|
|
215
|
+
rubric.prompt,
|
|
216
|
+
'',
|
|
217
|
+
'--- TURN CONTEXT ---',
|
|
218
|
+
`task_category: ${category ?? 'conversation'}`,
|
|
219
|
+
'',
|
|
220
|
+
'--- USER PROMPT ---',
|
|
221
|
+
prompt.slice(0, 2000) || '(not captured)',
|
|
222
|
+
'',
|
|
223
|
+
'--- AGENT RESPONSE ---',
|
|
224
|
+
response.slice(0, 4000) || '(not captured)',
|
|
225
|
+
'',
|
|
226
|
+
'Respond with strict JSON only.',
|
|
227
|
+
].join('\n');
|
|
228
|
+
try {
|
|
229
|
+
const judgeText = this.judgeOverride
|
|
230
|
+
? await this.judgeOverride(judgePrompt, cfg.judge)
|
|
231
|
+
: await callJudge(judgePrompt, cfg.judge);
|
|
232
|
+
const parsed = parseJudgeJson(judgeText);
|
|
233
|
+
if (!parsed)
|
|
234
|
+
return;
|
|
235
|
+
const now = Date.now();
|
|
236
|
+
const judgeModel = effectiveJudgeModelTag(cfg.judge);
|
|
237
|
+
for (const [axis, score] of Object.entries(parsed.scores)) {
|
|
238
|
+
this.store.insertEvalForTurn({
|
|
239
|
+
runId, turnId,
|
|
240
|
+
id: 0, // autoincrement
|
|
241
|
+
layer: 'turn_judge',
|
|
242
|
+
metric: axis,
|
|
243
|
+
score,
|
|
244
|
+
raw: axis === 'overall'
|
|
245
|
+
? JSON.stringify({ reasoning: parsed.reasoning, done: parsed.done, missed: parsed.missed })
|
|
246
|
+
: null,
|
|
247
|
+
rubricVer: rubric.version,
|
|
248
|
+
judgeModel,
|
|
249
|
+
createdAt: now,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
debug('APME', `turn eval ${turnId.slice(0, 8)}: overall=${parsed.scores.overall}`);
|
|
253
|
+
// Notify listeners with turnId so daemon can broadcast turn eval
|
|
254
|
+
for (const fn of this.listeners) {
|
|
255
|
+
fn({ runId, turnId, layer1Ran: false, layer2Ran: true, overall: parsed.scores.overall });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
debug('APME', `turn eval error turnId=${turnId.slice(0, 8)}: ${String(err)}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** Runs the queue until empty. Awaitable — callers can join the current drain. */
|
|
263
|
+
async drain() {
|
|
264
|
+
if (this.drainPromise)
|
|
265
|
+
return this.drainPromise;
|
|
266
|
+
this.drainPromise = this.doDrain();
|
|
267
|
+
try {
|
|
268
|
+
await this.drainPromise;
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
this.drainPromise = null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async doDrain() {
|
|
275
|
+
while (this.queue.length > 0) {
|
|
276
|
+
const job = this.queue.shift();
|
|
277
|
+
try {
|
|
278
|
+
const result = await this.runOne(job);
|
|
279
|
+
for (const fn of this.listeners) {
|
|
280
|
+
try {
|
|
281
|
+
fn(result);
|
|
282
|
+
}
|
|
283
|
+
catch { /* ignore */ }
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
debug('APME', `runner error runId=${job.runId}: ${String(err)}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async runOne(job) {
|
|
292
|
+
const cfg = this.configOverride ?? loadApmeConfig();
|
|
293
|
+
const run = this.store.getRun(job.runId);
|
|
294
|
+
if (!run) {
|
|
295
|
+
debug('APME', `runOne: run ${job.runId} not found`);
|
|
296
|
+
return { runId: job.runId, layer1Ran: false, layer2Ran: false };
|
|
297
|
+
}
|
|
298
|
+
// ── Layer 1 — deterministic ───────────────────────────────────────────────
|
|
299
|
+
let layer1Ran = false;
|
|
300
|
+
let layer1Passed = null;
|
|
301
|
+
if (cfg.deterministic.enabled) {
|
|
302
|
+
try {
|
|
303
|
+
const results = this.detOverride
|
|
304
|
+
? await this.detOverride(run, cfg)
|
|
305
|
+
: await runDeterministic(run, cfg);
|
|
306
|
+
for (const r of results) {
|
|
307
|
+
this.store.insertEval({
|
|
308
|
+
runId: run.id,
|
|
309
|
+
layer: 'deterministic',
|
|
310
|
+
metric: r.metric,
|
|
311
|
+
score: r.score,
|
|
312
|
+
raw: JSON.stringify({
|
|
313
|
+
command: r.command,
|
|
314
|
+
exitCode: r.exitCode,
|
|
315
|
+
durationMs: r.durationMs,
|
|
316
|
+
outputTail: r.outputTail,
|
|
317
|
+
}),
|
|
318
|
+
createdAt: Date.now(),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
if (results.length > 0) {
|
|
322
|
+
layer1Ran = true;
|
|
323
|
+
// Aggregate: pass only if every step passed.
|
|
324
|
+
layer1Passed = results.every((r) => r.score === 1);
|
|
325
|
+
}
|
|
326
|
+
debug('APME', `runOne layer1 runId=${run.id} results=${results.length} passed=${layer1Passed}`);
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
debug('APME', `layer1 error runId=${run.id}: ${String(err)}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// ── Layer 2 — llm_judge (gated) ───────────────────────────────────────────
|
|
333
|
+
let layer2Ran = false;
|
|
334
|
+
let overall;
|
|
335
|
+
if (cfg.enabled && shouldJudge(cfg.judge, layer1Passed)) {
|
|
336
|
+
// Select category-specific rubric, fall back to 'general'
|
|
337
|
+
const rubric = (run.taskCategory ? this.store.getCurrentRubric(run.taskCategory) : null)
|
|
338
|
+
?? this.store.getCurrentRubric('general');
|
|
339
|
+
if (rubric) {
|
|
340
|
+
try {
|
|
341
|
+
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);
|
|
346
|
+
if (parsed) {
|
|
347
|
+
const now = Date.now();
|
|
348
|
+
const judgeModel = effectiveJudgeModelTag(cfg.judge);
|
|
349
|
+
for (const [axis, score] of Object.entries(parsed.scores)) {
|
|
350
|
+
this.store.insertEval({
|
|
351
|
+
runId: run.id,
|
|
352
|
+
layer: 'llm_judge',
|
|
353
|
+
metric: axis,
|
|
354
|
+
score,
|
|
355
|
+
raw: axis === 'overall' ? JSON.stringify({ reasoning: parsed.reasoning, done: parsed.done, missed: parsed.missed }) : null,
|
|
356
|
+
rubricVer: rubric.version,
|
|
357
|
+
judgeModel,
|
|
358
|
+
createdAt: now,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
overall = parsed.scores.overall;
|
|
362
|
+
layer2Ran = true;
|
|
363
|
+
// Re-compute composite score with judge contribution
|
|
364
|
+
try {
|
|
365
|
+
const { recomputeComposite } = await import('./outcome.js');
|
|
366
|
+
recomputeComposite(this.store, run.id);
|
|
367
|
+
}
|
|
368
|
+
catch { /* ignore — outcome may not have run yet */ }
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
debug('APME', `judge response unparseable runId=${run.id}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
// Per cost-sensitive-defaults memory: never silently fall back from MLX to API.
|
|
376
|
+
debug('APME', `layer2 error runId=${run.id} (skipping, no fallback): ${String(err)}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return { runId: job.runId, layer1Ran, layer2Ran, overall };
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// ─── Language detection ───────────────────────────────────────────────────────
|
|
384
|
+
/** Auto-detect the first testable Xcode scheme from a project directory. */
|
|
385
|
+
function detectSwiftScheme(cwd) {
|
|
386
|
+
try {
|
|
387
|
+
const out = execSync('xcodebuild -list -json', {
|
|
388
|
+
cwd, encoding: 'utf-8', timeout: 10_000,
|
|
389
|
+
maxBuffer: 512 * 1024, stdio: ['ignore', 'pipe', 'ignore'],
|
|
390
|
+
});
|
|
391
|
+
const json = JSON.parse(out);
|
|
392
|
+
const schemes = json.project?.schemes;
|
|
393
|
+
if (!schemes || schemes.length === 0)
|
|
394
|
+
return null;
|
|
395
|
+
// Prefer schemes with "Test" or matching directory name.
|
|
396
|
+
const dirName = cwd.split('/').pop() ?? '';
|
|
397
|
+
const testScheme = schemes.find((s) => s.toLowerCase().includes('test'));
|
|
398
|
+
const matchScheme = schemes.find((s) => dirName.toLowerCase().includes(s.toLowerCase().replace(/_/g, '')));
|
|
399
|
+
return matchScheme ?? testScheme ?? schemes[0];
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
export function detectLanguage(projectPath) {
|
|
406
|
+
if (!projectPath || !existsSync(projectPath))
|
|
407
|
+
return null;
|
|
408
|
+
try {
|
|
409
|
+
const entries = readdirSync(projectPath);
|
|
410
|
+
if (entries.includes('package.json') || entries.some((e) => e.endsWith('.ts') || e.endsWith('.tsx'))) {
|
|
411
|
+
return 'typescript';
|
|
412
|
+
}
|
|
413
|
+
if (entries.some((e) => e.endsWith('.xcodeproj') || e.endsWith('.xcworkspace'))) {
|
|
414
|
+
return 'swift';
|
|
415
|
+
}
|
|
416
|
+
if (entries.includes('build.gradle') || entries.includes('build.gradle.kts') || entries.includes('settings.gradle.kts')) {
|
|
417
|
+
return 'kotlin';
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch { /* ignore */ }
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
// ─── Layer 1 execution ────────────────────────────────────────────────────────
|
|
424
|
+
export async function runDeterministic(run, cfg) {
|
|
425
|
+
const cwd = run.projectPath;
|
|
426
|
+
if (!cwd)
|
|
427
|
+
return [];
|
|
428
|
+
const lang = detectLanguage(cwd);
|
|
429
|
+
if (!lang)
|
|
430
|
+
return [];
|
|
431
|
+
// Only run deterministic checks when there are actual changes from this run.
|
|
432
|
+
// If gitBefore == gitAfter and no uncommitted diff, the agent didn't produce
|
|
433
|
+
// code — running tests gives stale baseline data, so skip.
|
|
434
|
+
if (!hasChanges(run)) {
|
|
435
|
+
debug('APME', `runDeterministic skipped runId=${run.id} — no git diff`);
|
|
436
|
+
return [];
|
|
437
|
+
}
|
|
438
|
+
const override = cfg.deterministic.commands[lang] ?? {};
|
|
439
|
+
const defaults = DEFAULT_COMMANDS[lang];
|
|
440
|
+
const rawSteps = [
|
|
441
|
+
{ metric: 'lint_clean', cmd: override.lint ?? defaults.lint },
|
|
442
|
+
{ metric: 'build_ok', cmd: override.build ?? defaults.build },
|
|
443
|
+
{ metric: 'tests_pass', cmd: override.test ?? defaults.test },
|
|
444
|
+
];
|
|
445
|
+
const results = [];
|
|
446
|
+
for (const s of rawSteps) {
|
|
447
|
+
// Resolve command: may be a string or a function(cwd) → string|null.
|
|
448
|
+
const command = typeof s.cmd === 'function' ? s.cmd(cwd) : s.cmd;
|
|
449
|
+
if (!command)
|
|
450
|
+
continue;
|
|
451
|
+
const r = await runCommand(command, cwd, cfg.deterministic.timeoutSec * 1000);
|
|
452
|
+
results.push({
|
|
453
|
+
metric: s.metric,
|
|
454
|
+
command,
|
|
455
|
+
score: r.exitCode === 0 ? 1 : 0,
|
|
456
|
+
exitCode: r.exitCode,
|
|
457
|
+
durationMs: r.durationMs,
|
|
458
|
+
outputTail: r.outputTail,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
return results;
|
|
462
|
+
}
|
|
463
|
+
function hasChanges(run) {
|
|
464
|
+
if (!run.projectPath)
|
|
465
|
+
return false;
|
|
466
|
+
if (run.gitBefore && run.gitAfter && run.gitBefore !== run.gitAfter)
|
|
467
|
+
return true;
|
|
468
|
+
// Check uncommitted diff (dirty worktree).
|
|
469
|
+
try {
|
|
470
|
+
const status = execSync('git status --porcelain', {
|
|
471
|
+
cwd: run.projectPath, encoding: 'utf-8', timeout: 3000,
|
|
472
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
473
|
+
});
|
|
474
|
+
return status.trim().length > 0;
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
// Not a git repo — assume changes exist so we don't silently skip everything.
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function runCommand(command, cwd, timeoutMs) {
|
|
482
|
+
return new Promise((resolve) => {
|
|
483
|
+
const start = Date.now();
|
|
484
|
+
const child = spawn('sh', ['-c', command], {
|
|
485
|
+
cwd,
|
|
486
|
+
env: { ...process.env, CI: '1', APME_EVAL: '1' },
|
|
487
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
488
|
+
});
|
|
489
|
+
const chunks = [];
|
|
490
|
+
let done = false;
|
|
491
|
+
const cap = 32 * 1024; // keep last 32KB
|
|
492
|
+
const onData = (buf) => {
|
|
493
|
+
chunks.push(buf);
|
|
494
|
+
let total = chunks.reduce((n, b) => n + b.length, 0);
|
|
495
|
+
while (total > cap * 2 && chunks.length > 1) {
|
|
496
|
+
const head = chunks.shift();
|
|
497
|
+
total -= head.length;
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
child.stdout?.on('data', onData);
|
|
501
|
+
child.stderr?.on('data', onData);
|
|
502
|
+
const timer = setTimeout(() => {
|
|
503
|
+
if (done)
|
|
504
|
+
return;
|
|
505
|
+
try {
|
|
506
|
+
child.kill('SIGKILL');
|
|
507
|
+
}
|
|
508
|
+
catch { /* ignore */ }
|
|
509
|
+
}, timeoutMs);
|
|
510
|
+
child.on('exit', (code, signal) => {
|
|
511
|
+
if (done)
|
|
512
|
+
return;
|
|
513
|
+
done = true;
|
|
514
|
+
clearTimeout(timer);
|
|
515
|
+
const combined = Buffer.concat(chunks).toString('utf-8');
|
|
516
|
+
const tail = combined.length > cap ? combined.slice(combined.length - cap) : combined;
|
|
517
|
+
resolve({
|
|
518
|
+
exitCode: typeof code === 'number' ? code : (signal ? 137 : 1),
|
|
519
|
+
durationMs: Date.now() - start,
|
|
520
|
+
outputTail: tail,
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
child.on('error', () => {
|
|
524
|
+
if (done)
|
|
525
|
+
return;
|
|
526
|
+
done = true;
|
|
527
|
+
clearTimeout(timer);
|
|
528
|
+
resolve({ exitCode: 127, durationMs: Date.now() - start, outputTail: `spawn failed: ${command}` });
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
// ─── Layer 2 execution ────────────────────────────────────────────────────────
|
|
533
|
+
// `ParsedJudge` is the canonical eval v1 type imported from `@agentdeck/shared`.
|
|
534
|
+
const NON_CODE_CATEGORIES = new Set(['conversation', 'planning', 'research', 'review']);
|
|
535
|
+
export function buildJudgePrompt(run, rubricPrompt, layer1Passed, store) {
|
|
536
|
+
const task = (run.taskPrompt ?? '').slice(0, 4_000);
|
|
537
|
+
const det = layer1Passed === null ? 'unknown' : layer1Passed ? 'passed' : 'failed';
|
|
538
|
+
const isNonCode = run.taskCategory && NON_CODE_CATEGORIES.has(run.taskCategory);
|
|
539
|
+
const sections = [
|
|
540
|
+
rubricPrompt,
|
|
541
|
+
'',
|
|
542
|
+
'--- RUN CONTEXT ---',
|
|
543
|
+
`agent_type: ${run.agentType}`,
|
|
544
|
+
`model: ${run.modelId ?? 'unknown'}`,
|
|
545
|
+
`project: ${run.projectName ?? 'unknown'}`,
|
|
546
|
+
`task_category: ${run.taskCategory ?? 'unknown'}`,
|
|
547
|
+
`deterministic_checks: ${det}`,
|
|
548
|
+
'',
|
|
549
|
+
'--- TASK PROMPT ---',
|
|
550
|
+
task || '(not captured)',
|
|
551
|
+
];
|
|
552
|
+
if (isNonCode && store) {
|
|
553
|
+
// Non-code categories: include turns (prompt + response) instead of diff
|
|
554
|
+
const turns = store.listTurns(run.id);
|
|
555
|
+
sections.push('', '--- CONVERSATION ---');
|
|
556
|
+
for (const t of turns.slice(0, 10)) {
|
|
557
|
+
const prompt = (t.prompt ?? '').slice(0, 2000);
|
|
558
|
+
const response = (t.response ?? '').slice(0, 3000);
|
|
559
|
+
sections.push(`[Turn ${t.turn_index}] User: ${prompt}`);
|
|
560
|
+
if (response)
|
|
561
|
+
sections.push(`Agent: ${response}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
// Code categories: include git diff
|
|
566
|
+
const diff = collectDiff(run);
|
|
567
|
+
sections.push('', '--- DIFF (truncated) ---', diff || '(no diff captured)');
|
|
568
|
+
}
|
|
569
|
+
sections.push('', 'Respond with strict JSON only.');
|
|
570
|
+
return sections.join('\n');
|
|
571
|
+
}
|
|
572
|
+
export function readResponseKind(turn) {
|
|
573
|
+
const raw = turn.efficiency_json;
|
|
574
|
+
if (typeof raw === 'string' && raw.length > 0) {
|
|
575
|
+
try {
|
|
576
|
+
const parsed = JSON.parse(raw);
|
|
577
|
+
const k = parsed.response_kind;
|
|
578
|
+
if (k === 'text' || k === 'tool_only' || k === 'empty')
|
|
579
|
+
return k;
|
|
580
|
+
}
|
|
581
|
+
catch { /* fall through */ }
|
|
582
|
+
}
|
|
583
|
+
const response = typeof turn.response === 'string' ? turn.response.trim() : '';
|
|
584
|
+
const toolCalls = typeof turn.tool_calls === 'number' ? turn.tool_calls : 0;
|
|
585
|
+
if (response.length >= 1)
|
|
586
|
+
return 'text';
|
|
587
|
+
return toolCalls > 0 ? 'tool_only' : 'empty';
|
|
588
|
+
}
|
|
589
|
+
function collectDiff(run) {
|
|
590
|
+
if (!run.projectPath)
|
|
591
|
+
return '';
|
|
592
|
+
try {
|
|
593
|
+
const args = run.gitBefore && run.gitAfter && run.gitBefore !== run.gitAfter
|
|
594
|
+
? ['diff', '--unified=2', `${run.gitBefore}..${run.gitAfter}`]
|
|
595
|
+
: ['diff', '--unified=2', 'HEAD'];
|
|
596
|
+
const out = execSync(`git ${args.join(' ')}`, {
|
|
597
|
+
cwd: run.projectPath, encoding: 'utf-8', timeout: 4000,
|
|
598
|
+
maxBuffer: 8 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'],
|
|
599
|
+
});
|
|
600
|
+
return out.length > 12_000 ? out.slice(0, 12_000) + '\n...[truncated]' : out;
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
return '';
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Build the `judgeModel` DB tag. For MLX backend the effective model is the
|
|
608
|
+
* llm.mlx pin when set — otherwise cfg.judge.model (may still be the legacy
|
|
609
|
+
* "qwen3-30b" placeholder). Non-MLX backends use cfg.judge.model verbatim.
|
|
610
|
+
*/
|
|
611
|
+
export function effectiveJudgeModelTag(cfg) {
|
|
612
|
+
if (cfg.backend === 'mlx') {
|
|
613
|
+
const pinned = loadMlxSettings().model;
|
|
614
|
+
return `mlx:${pinned ?? cfg.model}`;
|
|
615
|
+
}
|
|
616
|
+
// Must stay byte-identical with Swift's `ApmeJudgeFoundationModels.judgeModelLabel`
|
|
617
|
+
// so analytics queries aggregate FM evals across the Node and Swift stacks.
|
|
618
|
+
if (cfg.backend === 'foundationModels')
|
|
619
|
+
return 'foundationModels:apple-intelligence';
|
|
620
|
+
return `${cfg.backend}:${cfg.model}`;
|
|
621
|
+
}
|
|
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);
|
|
629
|
+
if (judgeCfg.backend === 'foundationModels') {
|
|
630
|
+
try {
|
|
631
|
+
return await callFoundationModels(prompt, judgeCfg);
|
|
632
|
+
}
|
|
633
|
+
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
|
|
636
|
+
// propagate the error so the runner's try/catch skips this eval.
|
|
637
|
+
if (judgeCfg.fallbackToMlx) {
|
|
638
|
+
debug('APME', `foundationModels unavailable, fallback to MLX: ${String(err)}`);
|
|
639
|
+
return callMlx(prompt, judgeCfg);
|
|
640
|
+
}
|
|
641
|
+
throw err;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
throw new Error(`unknown judge backend: ${String(judgeCfg.backend)}`);
|
|
645
|
+
}
|
|
646
|
+
async function callMlx(prompt, cfg) {
|
|
647
|
+
// MLX server speaks OpenAI chat-completions. The llm.mlx pin (shared with
|
|
648
|
+
// timeline/label summarizers) is the source of truth; cfg.endpoint/model
|
|
649
|
+
// override only when the user explicitly set apme.judge.* in settings.json.
|
|
650
|
+
const mlx = loadMlxSettings();
|
|
651
|
+
const url = cfg.endpoint ?? mlxChatUrl();
|
|
652
|
+
// Pin > cfg.model > probe auto-detect > cfg.model (final fallback).
|
|
653
|
+
let model = mlx.model ?? cfg.model;
|
|
654
|
+
if (!model || model === 'qwen3-30b') {
|
|
655
|
+
try {
|
|
656
|
+
const base = (cfg.endpoint ?? mlx.endpoint).replace(/\/chat\/completions$/, '').replace(/\/v1\/chat\/completions$/, '');
|
|
657
|
+
for (const path of ['/v1/models', '/models']) {
|
|
658
|
+
const mResp = await fetch(`${base}${path}`, { signal: AbortSignal.timeout(3000) }).catch(() => null);
|
|
659
|
+
if (mResp?.ok) {
|
|
660
|
+
const mJson = await mResp.json();
|
|
661
|
+
const first = mJson.data?.find(m => m.id && !m.id.toLowerCase().includes('nanollava'))?.id;
|
|
662
|
+
if (first) {
|
|
663
|
+
model = first;
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
catch { /* use configured model */ }
|
|
670
|
+
}
|
|
671
|
+
const resp = await fetch(url, {
|
|
672
|
+
method: 'POST',
|
|
673
|
+
headers: { 'Content-Type': 'application/json' },
|
|
674
|
+
body: JSON.stringify({
|
|
675
|
+
model,
|
|
676
|
+
messages: [
|
|
677
|
+
{ role: 'system', content: 'You are an exacting code evaluator. Reply with strict JSON only.' },
|
|
678
|
+
{ role: 'user', content: prompt },
|
|
679
|
+
],
|
|
680
|
+
temperature: 0.0,
|
|
681
|
+
max_tokens: 800,
|
|
682
|
+
}),
|
|
683
|
+
signal: AbortSignal.timeout(60_000),
|
|
684
|
+
});
|
|
685
|
+
if (!resp.ok)
|
|
686
|
+
throw new Error(`MLX judge HTTP ${resp.status}`);
|
|
687
|
+
const json = await resp.json();
|
|
688
|
+
const text = json.choices?.[0]?.message?.content;
|
|
689
|
+
if (typeof text !== 'string' || text.length === 0) {
|
|
690
|
+
throw new Error('MLX judge returned empty content');
|
|
691
|
+
}
|
|
692
|
+
return text;
|
|
693
|
+
}
|
|
694
|
+
async function callOpenClaw(prompt, cfg) {
|
|
695
|
+
// OpenClaw Gateway exposes the user's configured models. Route through it
|
|
696
|
+
// when the user wants to reuse their existing subscription models for judge.
|
|
697
|
+
const url = cfg.endpoint ?? 'http://127.0.0.1:18789/chat';
|
|
698
|
+
const resp = await fetch(url, {
|
|
699
|
+
method: 'POST',
|
|
700
|
+
headers: { 'Content-Type': 'application/json' },
|
|
701
|
+
body: JSON.stringify({ model: cfg.model, prompt, temperature: 0, max_tokens: 800 }),
|
|
702
|
+
signal: AbortSignal.timeout(60_000),
|
|
703
|
+
});
|
|
704
|
+
if (!resp.ok)
|
|
705
|
+
throw new Error(`OpenClaw judge HTTP ${resp.status}`);
|
|
706
|
+
const json = await resp.json();
|
|
707
|
+
if (typeof json.text !== 'string')
|
|
708
|
+
throw new Error('OpenClaw judge missing text');
|
|
709
|
+
return json.text;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Route a judge call to the Swift daemon's Foundation Models adapter.
|
|
713
|
+
*
|
|
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.
|
|
720
|
+
*
|
|
721
|
+
* Shape contract:
|
|
722
|
+
* Request : POST /apme/judge/foundation-models { prompt: string }
|
|
723
|
+
* Response : { text: string } | { error: "unavailable", reason: string }
|
|
724
|
+
*/
|
|
725
|
+
async function callFoundationModels(prompt, cfg) {
|
|
726
|
+
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'}`);
|
|
740
|
+
}
|
|
741
|
+
if (typeof json.text !== 'string' || json.text.length === 0) {
|
|
742
|
+
throw new Error('foundationModels returned empty text');
|
|
743
|
+
}
|
|
744
|
+
return json.text;
|
|
745
|
+
}
|
|
746
|
+
/** Best-effort resolver for the Swift daemon's FM endpoint. Returns null when
|
|
747
|
+
* no Swift daemon (httpPort ≠ port) is reachable. */
|
|
748
|
+
async function resolveFoundationModelsUrl() {
|
|
749
|
+
// Lazy-require to avoid pulling session-registry into every test bundle
|
|
750
|
+
// 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.');
|
|
764
|
+
}
|
|
765
|
+
export function parseJudgeJson(text) {
|
|
766
|
+
// Models often wrap JSON in prose or code fences — grab the first {...} block.
|
|
767
|
+
const match = text.match(/\{[\s\S]*\}/);
|
|
768
|
+
if (!match)
|
|
769
|
+
return null;
|
|
770
|
+
let obj;
|
|
771
|
+
try {
|
|
772
|
+
obj = JSON.parse(match[0]);
|
|
773
|
+
}
|
|
774
|
+
catch {
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
// Accept any numeric axis — category-specific rubrics define their own
|
|
778
|
+
// (conversation: accuracy/helpfulness/conciseness; research: thoroughness/…;
|
|
779
|
+
// planning: completeness/feasibility/clarity; etc.) A hardcoded whitelist
|
|
780
|
+
// silently drops those, leaving only `overall` on turn_judge rows.
|
|
781
|
+
const scores = {};
|
|
782
|
+
const RESERVED = new Set(['reasoning', 'done', 'missed', 'notes', 'summary']);
|
|
783
|
+
for (const [axis, v] of Object.entries(obj)) {
|
|
784
|
+
if (RESERVED.has(axis))
|
|
785
|
+
continue;
|
|
786
|
+
if (typeof v === 'number' && isFinite(v)) {
|
|
787
|
+
scores[axis] = clamp01(v);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
// Must at least have overall to be useful.
|
|
791
|
+
if (scores.overall === undefined)
|
|
792
|
+
return null;
|
|
793
|
+
const reasoning = typeof obj.reasoning === 'string' ? obj.reasoning : '';
|
|
794
|
+
const done = Array.isArray(obj.done) ? obj.done.filter((s) => typeof s === 'string') : undefined;
|
|
795
|
+
const missed = Array.isArray(obj.missed) ? obj.missed.filter((s) => typeof s === 'string') : undefined;
|
|
796
|
+
// `summary` is produced by the task_rollup rubric; one-line past-tense sentence.
|
|
797
|
+
// Clip defensively so a runaway model can't blow up the tasks.summary column.
|
|
798
|
+
const summary = typeof obj.summary === 'string' && obj.summary.trim().length > 0
|
|
799
|
+
? obj.summary.trim().slice(0, 280)
|
|
800
|
+
: undefined;
|
|
801
|
+
return { scores, reasoning, done, missed, summary };
|
|
802
|
+
}
|
|
803
|
+
function clamp01(n) {
|
|
804
|
+
if (n > 1 && n <= 10)
|
|
805
|
+
n = n / 10; // accept 0-10 scale and rescale
|
|
806
|
+
if (n > 1)
|
|
807
|
+
n = 1;
|
|
808
|
+
if (n < 0)
|
|
809
|
+
n = 0;
|
|
810
|
+
return n;
|
|
811
|
+
}
|
|
812
|
+
//# sourceMappingURL=runner.js.map
|