@guilz-dev/belay 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/dist/adapters/cursor/runtime-entry.js +1 -0
  2. package/dist/adapters/shared/gate-runtime.d.ts +11 -14
  3. package/dist/adapters/shared/gate-runtime.js +277 -19
  4. package/dist/bundle/claude-runtime.mjs +3138 -699
  5. package/dist/bundle/codex-runtime.mjs +3162 -711
  6. package/dist/bundle/cursor-runtime.mjs +3140 -701
  7. package/dist/cli.js +137 -5
  8. package/dist/commands/approve.d.ts +1 -0
  9. package/dist/commands/approve.js +45 -26
  10. package/dist/commands/config.js +135 -52
  11. package/dist/commands/doctor.js +11 -1
  12. package/dist/commands/explain.js +6 -1
  13. package/dist/commands/harvest.d.ts +29 -0
  14. package/dist/commands/harvest.js +72 -0
  15. package/dist/commands/judge.d.ts +11 -1
  16. package/dist/commands/judge.js +21 -2
  17. package/dist/commands/metrics.js +34 -2
  18. package/dist/commands/quality.d.ts +38 -0
  19. package/dist/commands/quality.js +105 -0
  20. package/dist/commands/simulate.d.ts +1 -0
  21. package/dist/commands/simulate.js +7 -2
  22. package/dist/commands/standing-allow.d.ts +10 -0
  23. package/dist/commands/standing-allow.js +26 -0
  24. package/dist/commands/tui.d.ts +48 -0
  25. package/dist/commands/tui.js +150 -0
  26. package/dist/config-io.js +10 -5
  27. package/dist/core/approval-replay-cli.d.ts +7 -0
  28. package/dist/core/approval-replay-cli.js +36 -0
  29. package/dist/core/approval-replay.d.ts +45 -0
  30. package/dist/core/approval-replay.js +141 -0
  31. package/dist/core/approval-service.d.ts +15 -0
  32. package/dist/core/approval-service.js +47 -2
  33. package/dist/core/approval.d.ts +24 -3
  34. package/dist/core/approval.js +47 -4
  35. package/dist/core/audit-analysis.d.ts +7 -1
  36. package/dist/core/audit-analysis.js +111 -0
  37. package/dist/core/audit-metrics.d.ts +7 -0
  38. package/dist/core/audit-metrics.js +24 -4
  39. package/dist/core/audit-query.d.ts +1 -0
  40. package/dist/core/audit-query.js +3 -0
  41. package/dist/core/audit-replay-context.d.ts +35 -0
  42. package/dist/core/audit-replay-context.js +88 -0
  43. package/dist/core/audit-types.d.ts +24 -1
  44. package/dist/core/audit-types.js +1 -1
  45. package/dist/core/capability/index.d.ts +2 -1
  46. package/dist/core/capability/index.js +1 -0
  47. package/dist/core/capability/paths.d.ts +2 -2
  48. package/dist/core/capability/paths.js +9 -9
  49. package/dist/core/capability/trusted-workspace-roots.d.ts +25 -0
  50. package/dist/core/capability/trusted-workspace-roots.js +149 -0
  51. package/dist/core/capability/types.d.ts +11 -1
  52. package/dist/core/capability-approval.d.ts +2 -1
  53. package/dist/core/capability-approval.js +100 -2
  54. package/dist/core/classify-subagent.js +2 -20
  55. package/dist/core/classify-tool.js +32 -3
  56. package/dist/core/config.d.ts +18 -0
  57. package/dist/core/config.js +49 -0
  58. package/dist/core/gate-engine.js +6 -6
  59. package/dist/core/harvest.d.ts +53 -0
  60. package/dist/core/harvest.js +276 -0
  61. package/dist/core/index.d.ts +9 -3
  62. package/dist/core/index.js +6 -2
  63. package/dist/core/judge-doctor.d.ts +3 -0
  64. package/dist/core/judge-doctor.js +54 -0
  65. package/dist/core/path-utils.d.ts +11 -0
  66. package/dist/core/path-utils.js +56 -6
  67. package/dist/core/reclassify.d.ts +3 -0
  68. package/dist/core/reclassify.js +44 -14
  69. package/dist/core/replay-scrub.d.ts +9 -0
  70. package/dist/core/replay-scrub.js +43 -0
  71. package/dist/core/shell-tokenizer.d.ts +2 -0
  72. package/dist/core/shell-tokenizer.js +82 -23
  73. package/dist/core/standing-allow.d.ts +50 -0
  74. package/dist/core/standing-allow.js +175 -0
  75. package/dist/core/types.d.ts +15 -0
  76. package/dist/core/verdict/adapter.js +10 -1
  77. package/dist/core/verdict/containment.d.ts +4 -3
  78. package/dist/core/verdict/containment.js +17 -12
  79. package/dist/core/verdict/judge-audit.d.ts +1 -0
  80. package/dist/core/verdict/judge-audit.js +32 -1
  81. package/dist/core/verdict/judge-baseline.d.ts +20 -0
  82. package/dist/core/verdict/judge-baseline.js +48 -0
  83. package/dist/core/verdict/judge-broker-service.d.ts +42 -0
  84. package/dist/core/verdict/judge-broker-service.js +279 -0
  85. package/dist/core/verdict/judge-cli-fingerprint.d.ts +5 -0
  86. package/dist/core/verdict/judge-cli-fingerprint.js +61 -0
  87. package/dist/core/verdict/judge-cli.d.ts +23 -0
  88. package/dist/core/verdict/judge-cli.js +137 -16
  89. package/dist/core/verdict/judge-factory.js +13 -4
  90. package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
  91. package/dist/core/verdict/judge-provider-matrix.js +66 -0
  92. package/dist/core/verdict/judge-runtime-config.d.ts +42 -0
  93. package/dist/core/verdict/judge-runtime-config.js +92 -0
  94. package/dist/core/verdict/judge-session-broker.d.ts +48 -0
  95. package/dist/core/verdict/judge-session-broker.js +195 -0
  96. package/dist/core/verdict/judge-session-guard.d.ts +27 -0
  97. package/dist/core/verdict/judge-session-guard.js +91 -0
  98. package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
  99. package/dist/core/verdict/judge-session-kill-switch.js +43 -0
  100. package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
  101. package/dist/core/verdict/judge-session-mutex.js +23 -0
  102. package/dist/core/verdict/judge-shadow.d.ts +24 -0
  103. package/dist/core/verdict/judge-shadow.js +116 -0
  104. package/dist/core/verdict/judge-transport.d.ts +40 -0
  105. package/dist/core/verdict/judge-transport.js +258 -0
  106. package/dist/core/verdict/judge.d.ts +14 -1
  107. package/dist/core/verdict/judge.js +6 -5
  108. package/dist/core/verdict/parser.js +6 -1
  109. package/dist/core/verdict/types.d.ts +13 -0
  110. package/dist/core/verdict/verdict.js +7 -4
  111. package/dist/corpus/adversarial-probe.d.ts +86 -0
  112. package/dist/corpus/adversarial-probe.js +220 -0
  113. package/dist/corpus/evaluate.d.ts +45 -12
  114. package/dist/corpus/evaluate.js +62 -5
  115. package/dist/corpus/gates.d.ts +35 -0
  116. package/dist/corpus/gates.js +81 -0
  117. package/dist/corpus/judge-accuracy.d.ts +8 -0
  118. package/dist/corpus/judge-accuracy.js +39 -0
  119. package/dist/corpus/must-allow-commands.d.ts +5 -0
  120. package/dist/corpus/must-allow-commands.js +13 -0
  121. package/dist/corpus/mutators.d.ts +28 -0
  122. package/dist/corpus/mutators.js +151 -0
  123. package/dist/corpus/ratchet.d.ts +42 -0
  124. package/dist/corpus/ratchet.js +116 -0
  125. package/dist/corpus/runtime-match.d.ts +19 -0
  126. package/dist/corpus/runtime-match.js +45 -0
  127. package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
  128. package/dist/corpus/standing-allow-catalog.generated.js +79 -0
  129. package/dist/corpus/types.d.ts +47 -0
  130. package/dist/corpus/types.js +157 -0
  131. package/dist/installer.js +4 -1
  132. package/dist/judge-broker-daemon.d.ts +1 -0
  133. package/dist/judge-broker-daemon.js +123 -0
  134. package/dist/services/sandbox-service.d.ts +1 -0
  135. package/dist/services/sandbox-service.js +2 -0
  136. package/dist/version.d.ts +1 -1
  137. package/dist/version.js +1 -1
  138. package/package.json +7 -3
  139. package/skills/belay/SKILL.md +7 -1
  140. package/skills/belay/belay-approve.md +17 -0
@@ -0,0 +1,48 @@
1
+ import { JUDGE_LATENCY_SLO } from './judge-runtime-config.js';
2
+ const samples = [];
3
+ const MAX_SAMPLES = 2_000;
4
+ export function resetJudgeLatencySamples() {
5
+ samples.length = 0;
6
+ }
7
+ export function recordJudgeLatency(phase, latencyMs, at = Date.now()) {
8
+ if (!Number.isFinite(latencyMs) || latencyMs < 0) {
9
+ return;
10
+ }
11
+ samples.push({ phase, latencyMs, at });
12
+ if (samples.length > MAX_SAMPLES) {
13
+ samples.splice(0, samples.length - MAX_SAMPLES);
14
+ }
15
+ }
16
+ function percentile(values, p) {
17
+ if (values.length === 0) {
18
+ return 0;
19
+ }
20
+ const sorted = [...values].sort((a, b) => a - b);
21
+ const index = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1);
22
+ return sorted[Math.max(0, index)] ?? 0;
23
+ }
24
+ export function judgeLatencyPercentiles(phase) {
25
+ const values = samples
26
+ .filter((sample) => sample.phase === phase)
27
+ .map((sample) => sample.latencyMs);
28
+ return {
29
+ p50: percentile(values, 50),
30
+ p95: percentile(values, 95),
31
+ count: values.length,
32
+ };
33
+ }
34
+ export function judgeLatencySloReport() {
35
+ const measured = {
36
+ tier0: judgeLatencyPercentiles('tier0'),
37
+ tier1_spawn: judgeLatencyPercentiles('tier1_spawn'),
38
+ tier1_session: judgeLatencyPercentiles('tier1_session'),
39
+ tier1_connect: judgeLatencyPercentiles('tier1_connect'),
40
+ tier1_eval: judgeLatencyPercentiles('tier1_eval'),
41
+ tier1_parse: judgeLatencyPercentiles('tier1_parse'),
42
+ };
43
+ const spawnP95 = measured.tier1_spawn.count > 0 ? measured.tier1_spawn.p95 : JUDGE_LATENCY_SLO.spawnBaselineP95Ms;
44
+ const sessionP95 = measured.tier1_session.count > 0 ? measured.tier1_session.p95 : spawnP95;
45
+ const targetP95 = spawnP95 * (1 - JUDGE_LATENCY_SLO.tier1P95ReductionTarget);
46
+ const sessionMeetsTarget = sessionP95 <= targetP95 && sessionP95 <= JUDGE_LATENCY_SLO.sessionMaxP95Ms;
47
+ return { slo: JUDGE_LATENCY_SLO, measured, sessionMeetsTarget };
48
+ }
@@ -0,0 +1,42 @@
1
+ import type { CliJudgeRunCommand } from './judge-cli.js';
2
+ import type { BelayJudgeSessionConfig } from './judge-runtime-config.js';
3
+ import { type BrokerEvaluateRequest, type BrokerEvaluateResult, JudgeSessionBroker, stopRepoJudgeSessionBroker } from './judge-session-broker.js';
4
+ import type { JudgeSessionResetReason } from './judge-session-guard.js';
5
+ export declare const JUDGE_BROKER_SOCKET = "judge-broker.sock";
6
+ export declare const JUDGE_BROKER_STATUS = "judge-broker.json";
7
+ export declare const JUDGE_BROKER_PID = "judge-broker.pid";
8
+ export declare const JUDGE_BROKER_SESSION = "judge-broker-session.json";
9
+ export interface JudgeBrokerStatus {
10
+ pid: number;
11
+ repoRoot: string;
12
+ socketPath: string;
13
+ startedAt: string;
14
+ }
15
+ export interface JudgeBrokerPaths {
16
+ stateDir: string;
17
+ socketPath: string;
18
+ statusPath: string;
19
+ pidPath: string;
20
+ sessionConfigPath: string;
21
+ }
22
+ export declare function judgeBrokerPaths(stateDir: string): JudgeBrokerPaths;
23
+ export declare function ensureJudgeBrokerDaemon(stateDir: string, repoRoot: string, sessionConfig: BelayJudgeSessionConfig): Promise<JudgeBrokerPaths>;
24
+ export declare function cleanupJudgeBrokerArtifacts(paths: JudgeBrokerPaths): Promise<void>;
25
+ export declare function stopJudgeBrokerDaemon(stateDir: string): Promise<number>;
26
+ export declare function evaluateViaJudgeBroker(options: {
27
+ stateDir: string;
28
+ repoRoot: string;
29
+ sessionConfig: BelayJudgeSessionConfig;
30
+ request: BrokerEvaluateRequest;
31
+ timeoutMs: number;
32
+ runCommand?: CliJudgeRunCommand;
33
+ }): Promise<BrokerEvaluateResult>;
34
+ export declare function resetJudgeBrokerForTests(): void;
35
+ export declare function invalidateJudgeSession(options: {
36
+ stateDir: string;
37
+ repoRoot: string;
38
+ sessionConfig: BelayJudgeSessionConfig;
39
+ sessionKey: string;
40
+ reason: JudgeSessionResetReason;
41
+ }): Promise<void>;
42
+ export { JudgeSessionBroker, stopRepoJudgeSessionBroker };
@@ -0,0 +1,279 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { unlink } from 'node:fs/promises';
4
+ import net from 'node:net';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { runCliJsonWithTimeouts } from './judge-cli.js';
8
+ import { normalizeJudgeSessionConfig } from './judge-runtime-config.js';
9
+ import { getRepoJudgeSessionBroker, JudgeSessionBroker, resetJudgeSessionBrokersForTests, stopRepoJudgeSessionBroker, } from './judge-session-broker.js';
10
+ export const JUDGE_BROKER_SOCKET = 'judge-broker.sock';
11
+ export const JUDGE_BROKER_STATUS = 'judge-broker.json';
12
+ export const JUDGE_BROKER_PID = 'judge-broker.pid';
13
+ export const JUDGE_BROKER_SESSION = 'judge-broker-session.json';
14
+ export function judgeBrokerPaths(stateDir) {
15
+ return {
16
+ stateDir,
17
+ socketPath: path.join(stateDir, JUDGE_BROKER_SOCKET),
18
+ statusPath: path.join(stateDir, JUDGE_BROKER_STATUS),
19
+ pidPath: path.join(stateDir, JUDGE_BROKER_PID),
20
+ sessionConfigPath: path.join(stateDir, JUDGE_BROKER_SESSION),
21
+ };
22
+ }
23
+ function daemonScriptPath() {
24
+ return fileURLToPath(new URL('../../judge-broker-daemon.js', import.meta.url));
25
+ }
26
+ function isProcessAlive(pid) {
27
+ try {
28
+ process.kill(pid, 0);
29
+ return true;
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ function useInProcessBroker(env = process.env) {
36
+ return Boolean(env.VITEST || env.VITEST_WORKER_ID || env.BELAY_JUDGE_BROKER_IN_PROCESS === '1');
37
+ }
38
+ async function readBrokerStatus(statusPath) {
39
+ if (!existsSync(statusPath)) {
40
+ return null;
41
+ }
42
+ try {
43
+ const { readFile } = await import('node:fs/promises');
44
+ const raw = JSON.parse(await readFile(statusPath, 'utf8'));
45
+ if (typeof raw.pid !== 'number' || typeof raw.socketPath !== 'string') {
46
+ return null;
47
+ }
48
+ return raw;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ async function readBrokerSessionConfig(sessionConfigPath) {
55
+ if (!existsSync(sessionConfigPath)) {
56
+ return null;
57
+ }
58
+ try {
59
+ const { readFile } = await import('node:fs/promises');
60
+ const raw = JSON.parse(await readFile(sessionConfigPath, 'utf8'));
61
+ return normalizeJudgeSessionConfig({ ...raw, enabled: true });
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ function brokerSessionConfigPayload(sessionConfig) {
68
+ return JSON.stringify(normalizeJudgeSessionConfig({ ...sessionConfig, enabled: true }));
69
+ }
70
+ async function writeBrokerSessionConfig(paths, sessionConfig) {
71
+ const { writeFile } = await import('node:fs/promises');
72
+ await writeFile(paths.sessionConfigPath, brokerSessionConfigPayload(sessionConfig), {
73
+ encoding: 'utf8',
74
+ mode: 0o600,
75
+ });
76
+ }
77
+ async function connectBrokerSocket(socketPath, timeoutMs) {
78
+ return new Promise((resolve, reject) => {
79
+ const socket = net.createConnection(socketPath);
80
+ const timer = setTimeout(() => {
81
+ socket.destroy();
82
+ reject(new Error('judge broker connect timed out'));
83
+ }, timeoutMs);
84
+ socket.once('connect', () => {
85
+ clearTimeout(timer);
86
+ resolve(socket);
87
+ });
88
+ socket.once('error', (error) => {
89
+ clearTimeout(timer);
90
+ reject(error);
91
+ });
92
+ });
93
+ }
94
+ async function rpcEvaluate(socketPath, request, timeoutMs, connectTimeoutMs) {
95
+ const socket = await connectBrokerSocket(socketPath, connectTimeoutMs);
96
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
97
+ const payload = JSON.stringify({
98
+ type: 'evaluate',
99
+ id,
100
+ request,
101
+ timeoutMs,
102
+ });
103
+ return new Promise((resolve, reject) => {
104
+ let buffer = '';
105
+ const timer = setTimeout(() => {
106
+ socket.destroy();
107
+ reject(new Error('judge broker evaluate timed out'));
108
+ }, timeoutMs + connectTimeoutMs);
109
+ const onData = (chunk) => {
110
+ buffer += String(chunk);
111
+ const lines = buffer.split('\n');
112
+ buffer = lines.pop() ?? '';
113
+ for (const line of lines) {
114
+ if (!line.trim()) {
115
+ continue;
116
+ }
117
+ try {
118
+ const message = JSON.parse(line);
119
+ if (message.id !== id) {
120
+ continue;
121
+ }
122
+ clearTimeout(timer);
123
+ socket.destroy();
124
+ if (message.type === 'error') {
125
+ reject(new Error(message.error ?? 'judge broker error'));
126
+ return;
127
+ }
128
+ if (message.result) {
129
+ resolve(message.result);
130
+ }
131
+ }
132
+ catch {
133
+ // wait for more data
134
+ }
135
+ }
136
+ };
137
+ socket.on('data', onData);
138
+ socket.on('error', (error) => {
139
+ clearTimeout(timer);
140
+ reject(error);
141
+ });
142
+ socket.write(`${payload}\n`);
143
+ });
144
+ }
145
+ export async function ensureJudgeBrokerDaemon(stateDir, repoRoot, sessionConfig) {
146
+ const paths = judgeBrokerPaths(stateDir);
147
+ const brokerSessionConfig = normalizeJudgeSessionConfig({ ...sessionConfig, enabled: true });
148
+ const nextConfigPayload = brokerSessionConfigPayload(brokerSessionConfig);
149
+ const existingConfig = await readBrokerSessionConfig(paths.sessionConfigPath);
150
+ const configChanged = existingConfig !== null && brokerSessionConfigPayload(existingConfig) !== nextConfigPayload;
151
+ const status = await readBrokerStatus(paths.statusPath);
152
+ if (status && isProcessAlive(status.pid) && existsSync(paths.socketPath) && !configChanged) {
153
+ await writeBrokerSessionConfig(paths, brokerSessionConfig);
154
+ return paths;
155
+ }
156
+ if (status?.pid && isProcessAlive(status.pid)) {
157
+ await stopJudgeBrokerDaemon(stateDir);
158
+ }
159
+ else {
160
+ await cleanupJudgeBrokerArtifacts(paths);
161
+ }
162
+ await writeBrokerSessionConfig(paths, brokerSessionConfig);
163
+ const child = spawn(process.execPath, [daemonScriptPath()], {
164
+ detached: true,
165
+ stdio: 'ignore',
166
+ env: {
167
+ ...process.env,
168
+ BELAY_JUDGE_BROKER_REPO_ROOT: repoRoot,
169
+ BELAY_JUDGE_BROKER_STATE_DIR: stateDir,
170
+ },
171
+ });
172
+ child.unref();
173
+ const deadline = Date.now() + sessionConfig.connectTimeoutMs;
174
+ while (Date.now() < deadline) {
175
+ if (existsSync(paths.socketPath)) {
176
+ const live = await readBrokerStatus(paths.statusPath);
177
+ if (live && isProcessAlive(live.pid)) {
178
+ return paths;
179
+ }
180
+ }
181
+ await new Promise((resolve) => setTimeout(resolve, 25));
182
+ }
183
+ throw new Error('judge broker daemon failed to start');
184
+ }
185
+ export async function cleanupJudgeBrokerArtifacts(paths) {
186
+ for (const artifact of [
187
+ paths.socketPath,
188
+ paths.statusPath,
189
+ paths.pidPath,
190
+ paths.sessionConfigPath,
191
+ ]) {
192
+ if (existsSync(artifact)) {
193
+ await unlink(artifact).catch(() => undefined);
194
+ }
195
+ }
196
+ }
197
+ export async function stopJudgeBrokerDaemon(stateDir) {
198
+ const paths = judgeBrokerPaths(stateDir);
199
+ const status = await readBrokerStatus(paths.statusPath);
200
+ if (status?.pid && isProcessAlive(status.pid)) {
201
+ try {
202
+ process.kill(status.pid, 'SIGTERM');
203
+ }
204
+ catch {
205
+ // already stopped
206
+ }
207
+ }
208
+ await cleanupJudgeBrokerArtifacts(paths);
209
+ return status?.pid ? 1 : 0;
210
+ }
211
+ export async function evaluateViaJudgeBroker(options) {
212
+ const runCommand = options.runCommand ??
213
+ ((invocation, timeout) => runCliJsonWithTimeouts(invocation, { evalTimeoutMs: timeout }));
214
+ if (useInProcessBroker()) {
215
+ const broker = getRepoJudgeSessionBroker(options.repoRoot, {
216
+ config: options.sessionConfig,
217
+ runCommand,
218
+ });
219
+ return broker.evaluate(options.request, options.timeoutMs);
220
+ }
221
+ const paths = await ensureJudgeBrokerDaemon(options.stateDir, options.repoRoot, options.sessionConfig);
222
+ return rpcEvaluate(paths.socketPath, options.request, options.timeoutMs, options.sessionConfig.connectTimeoutMs);
223
+ }
224
+ export function resetJudgeBrokerForTests() {
225
+ resetJudgeSessionBrokersForTests();
226
+ }
227
+ async function rpcInvalidate(socketPath, sessionKey, reason, connectTimeoutMs) {
228
+ const socket = await connectBrokerSocket(socketPath, connectTimeoutMs);
229
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
230
+ const payload = JSON.stringify({ type: 'invalidate', id, sessionKey, reason });
231
+ await new Promise((resolve, reject) => {
232
+ let buffer = '';
233
+ const timer = setTimeout(() => {
234
+ socket.destroy();
235
+ reject(new Error('judge broker invalidate timed out'));
236
+ }, connectTimeoutMs);
237
+ const onData = (chunk) => {
238
+ buffer += String(chunk);
239
+ const lines = buffer.split('\n');
240
+ buffer = lines.pop() ?? '';
241
+ for (const line of lines) {
242
+ if (!line.trim()) {
243
+ continue;
244
+ }
245
+ try {
246
+ const message = JSON.parse(line);
247
+ if (message.id !== id) {
248
+ continue;
249
+ }
250
+ clearTimeout(timer);
251
+ socket.destroy();
252
+ resolve();
253
+ }
254
+ catch {
255
+ // wait for more data
256
+ }
257
+ }
258
+ };
259
+ socket.on('data', onData);
260
+ socket.on('error', (error) => {
261
+ clearTimeout(timer);
262
+ reject(error);
263
+ });
264
+ socket.write(`${payload}\n`);
265
+ });
266
+ }
267
+ export async function invalidateJudgeSession(options) {
268
+ if (useInProcessBroker()) {
269
+ const broker = getRepoJudgeSessionBroker(options.repoRoot, {
270
+ config: { ...options.sessionConfig, enabled: true },
271
+ runCommand: (invocation, timeout) => runCliJsonWithTimeouts(invocation, { evalTimeoutMs: timeout }),
272
+ });
273
+ broker.invalidateSession(options.sessionKey, options.reason);
274
+ return;
275
+ }
276
+ const paths = await ensureJudgeBrokerDaemon(options.stateDir, options.repoRoot, options.sessionConfig);
277
+ await rpcInvalidate(paths.socketPath, options.sessionKey, options.reason, options.sessionConfig.connectTimeoutMs);
278
+ }
279
+ export { JudgeSessionBroker, stopRepoJudgeSessionBroker };
@@ -0,0 +1,5 @@
1
+ import type { JudgeProviderId } from './judge-catalog.js';
2
+ export type CliFingerprintResolver = (providerId: Exclude<JudgeProviderId, 'ollama'>) => Promise<string>;
3
+ export declare function setCliFingerprintResolverForTests(resolver: CliFingerprintResolver | null): void;
4
+ export declare function resolveCliVersionFingerprint(providerId: Exclude<JudgeProviderId, 'ollama'>, timeoutMs?: number): Promise<string>;
5
+ export declare function resetCliFingerprintCache(): void;
@@ -0,0 +1,61 @@
1
+ import { spawn } from 'node:child_process';
2
+ const CLI_BINARIES = {
3
+ codex: 'codex',
4
+ cursor: 'cursor-agent',
5
+ claude: 'claude',
6
+ };
7
+ let fingerprintResolver = null;
8
+ const fingerprintCache = new Map();
9
+ export function setCliFingerprintResolverForTests(resolver) {
10
+ fingerprintResolver = resolver;
11
+ fingerprintCache.clear();
12
+ }
13
+ async function runVersionProbe(binary, timeoutMs) {
14
+ return new Promise((resolve, reject) => {
15
+ const child = spawn(binary, ['--version'], { stdio: ['ignore', 'pipe', 'pipe'] });
16
+ let stdout = '';
17
+ let stderr = '';
18
+ const timer = setTimeout(() => {
19
+ child.kill('SIGTERM');
20
+ reject(new Error(`${binary} --version timed out`));
21
+ }, timeoutMs);
22
+ child.stdout.on('data', (chunk) => {
23
+ stdout += String(chunk);
24
+ });
25
+ child.stderr.on('data', (chunk) => {
26
+ stderr += String(chunk);
27
+ });
28
+ child.on('error', (error) => {
29
+ clearTimeout(timer);
30
+ reject(error);
31
+ });
32
+ child.on('close', (code) => {
33
+ clearTimeout(timer);
34
+ const output = (stdout || stderr).trim();
35
+ if (code === 0 && output) {
36
+ resolve(output.split(/\r?\n/)[0]?.trim() || output);
37
+ return;
38
+ }
39
+ reject(new Error(stderr.trim() || `${binary} --version exited with code ${code ?? 'unknown'}`));
40
+ });
41
+ });
42
+ }
43
+ export async function resolveCliVersionFingerprint(providerId, timeoutMs = 3_000) {
44
+ const cacheKey = providerId;
45
+ const cached = fingerprintCache.get(cacheKey);
46
+ if (cached) {
47
+ return cached;
48
+ }
49
+ if (fingerprintResolver) {
50
+ const resolved = await fingerprintResolver(providerId);
51
+ fingerprintCache.set(cacheKey, resolved);
52
+ return resolved;
53
+ }
54
+ const binary = CLI_BINARIES[providerId];
55
+ const version = await runVersionProbe(binary, timeoutMs);
56
+ fingerprintCache.set(cacheKey, version);
57
+ return version;
58
+ }
59
+ export function resetCliFingerprintCache() {
60
+ fingerprintCache.clear();
61
+ }
@@ -1,6 +1,7 @@
1
1
  import type { ScrubOptions } from '../types.js';
2
2
  import { parseTier1Json, type TracedTier1Judge } from './judge.js';
3
3
  import type { JudgeProviderId } from './judge-catalog.js';
4
+ import type { BelayJudgeRuntimeConfig } from './judge-runtime-config.js';
4
5
  export interface CliJudgeOptions {
5
6
  providerId: JudgeProviderId;
6
7
  modelRequested: string;
@@ -9,6 +10,10 @@ export interface CliJudgeOptions {
9
10
  sensitivePaths: string[];
10
11
  scrubOptions: ScrubOptions;
11
12
  runCliCommand?: CliJudgeRunCommand;
13
+ runtime?: BelayJudgeRuntimeConfig;
14
+ repoRoot?: string;
15
+ stateDir?: string;
16
+ judgeMode?: string;
12
17
  }
13
18
  export interface CliInvocation {
14
19
  binary: string;
@@ -16,8 +21,26 @@ export interface CliInvocation {
16
21
  stdin?: string;
17
22
  }
18
23
  export type CliJudgeRunCommand = (invocation: CliInvocation, timeoutMs: number) => Promise<string>;
24
+ export type CliRunErrorKind = 'timeout' | 'spawn_error' | 'exit_nonzero';
25
+ export declare class CliRunError extends Error {
26
+ kind: CliRunErrorKind;
27
+ exitCode?: number;
28
+ stderr?: string;
29
+ stdout?: string;
30
+ command?: string;
31
+ constructor(kind: CliRunErrorKind, message: string, details?: {
32
+ exitCode?: number;
33
+ stderr?: string;
34
+ stdout?: string;
35
+ command?: string;
36
+ });
37
+ }
19
38
  export declare function buildCliInvocation(providerId: Exclude<JudgeProviderId, 'ollama'>, prompt: string, model: string): CliInvocation;
20
39
  export declare function parseCliJudgeOutput(providerId: Exclude<JudgeProviderId, 'ollama'>, raw: string): ReturnType<typeof parseTier1Json>;
40
+ export declare function runCliJsonWithTimeouts(invocation: CliInvocation, budgets: {
41
+ connectTimeoutMs?: number;
42
+ evalTimeoutMs: number;
43
+ }): Promise<string>;
21
44
  export declare function createCodexCliJudge(options: Omit<CliJudgeOptions, 'providerId'>): TracedTier1Judge;
22
45
  export declare function createCursorCliJudge(options: Omit<CliJudgeOptions, 'providerId'>): TracedTier1Judge;
23
46
  export declare function createClaudeCliJudge(options: Omit<CliJudgeOptions, 'providerId'>): TracedTier1Judge;