@guilz-dev/belay 0.2.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 (180) hide show
  1. package/README.md +39 -12
  2. package/dist/adapters/cursor/runtime-entry.js +1 -0
  3. package/dist/adapters/shared/gate-runtime.d.ts +11 -14
  4. package/dist/adapters/shared/gate-runtime.js +289 -23
  5. package/dist/bundle/claude-runtime.mjs +4435 -708
  6. package/dist/bundle/codex-runtime.mjs +4448 -710
  7. package/dist/bundle/cursor-runtime.mjs +4437 -710
  8. package/dist/cli.js +333 -11
  9. package/dist/commands/approve.d.ts +1 -0
  10. package/dist/commands/approve.js +56 -26
  11. package/dist/commands/config.d.ts +70 -0
  12. package/dist/commands/config.js +577 -0
  13. package/dist/commands/doctor.js +13 -2
  14. package/dist/commands/explain.js +6 -1
  15. package/dist/commands/harvest.d.ts +29 -0
  16. package/dist/commands/harvest.js +72 -0
  17. package/dist/commands/health-snapshot.d.ts +6 -0
  18. package/dist/commands/health-snapshot.js +34 -0
  19. package/dist/commands/judge.d.ts +100 -0
  20. package/dist/commands/judge.js +304 -0
  21. package/dist/commands/metrics.js +34 -2
  22. package/dist/commands/quality.d.ts +38 -0
  23. package/dist/commands/quality.js +105 -0
  24. package/dist/commands/simulate.d.ts +1 -0
  25. package/dist/commands/simulate.js +7 -2
  26. package/dist/commands/standing-allow.d.ts +10 -0
  27. package/dist/commands/standing-allow.js +26 -0
  28. package/dist/commands/status.js +2 -2
  29. package/dist/commands/stdin-key.d.ts +1 -0
  30. package/dist/commands/stdin-key.js +8 -0
  31. package/dist/commands/tui.d.ts +48 -0
  32. package/dist/commands/tui.js +150 -0
  33. package/dist/config-io.js +10 -5
  34. package/dist/conformance/guarantee-table.js +12 -0
  35. package/dist/conformance/types.d.ts +2 -0
  36. package/dist/core/approval-replay-cli.d.ts +7 -0
  37. package/dist/core/approval-replay-cli.js +36 -0
  38. package/dist/core/approval-replay.d.ts +45 -0
  39. package/dist/core/approval-replay.js +141 -0
  40. package/dist/core/approval-service.d.ts +15 -0
  41. package/dist/core/approval-service.js +47 -2
  42. package/dist/core/approval.d.ts +24 -3
  43. package/dist/core/approval.js +47 -4
  44. package/dist/core/audit-analysis.d.ts +7 -1
  45. package/dist/core/audit-analysis.js +111 -0
  46. package/dist/core/audit-io.d.ts +2 -0
  47. package/dist/core/audit-io.js +14 -0
  48. package/dist/core/audit-metrics.d.ts +7 -0
  49. package/dist/core/audit-metrics.js +24 -4
  50. package/dist/core/audit-query.d.ts +1 -0
  51. package/dist/core/audit-query.js +3 -0
  52. package/dist/core/audit-replay-context.d.ts +35 -0
  53. package/dist/core/audit-replay-context.js +88 -0
  54. package/dist/core/audit-types.d.ts +24 -1
  55. package/dist/core/audit-types.js +1 -1
  56. package/dist/core/capability/index.d.ts +4 -3
  57. package/dist/core/capability/index.js +3 -2
  58. package/dist/core/capability/paths.d.ts +2 -1
  59. package/dist/core/capability/paths.js +76 -8
  60. package/dist/core/capability/reasons.d.ts +3 -0
  61. package/dist/core/capability/reasons.js +8 -0
  62. package/dist/core/capability/trusted-workspace-roots.d.ts +25 -0
  63. package/dist/core/capability/trusted-workspace-roots.js +149 -0
  64. package/dist/core/capability/types.d.ts +11 -1
  65. package/dist/core/capability-approval.d.ts +2 -1
  66. package/dist/core/capability-approval.js +100 -2
  67. package/dist/core/classify-subagent.js +2 -20
  68. package/dist/core/classify-tool.js +129 -30
  69. package/dist/core/config-layers.js +2 -1
  70. package/dist/core/config.d.ts +40 -2
  71. package/dist/core/config.js +159 -11
  72. package/dist/core/credential-store.d.ts +11 -0
  73. package/dist/core/credential-store.js +60 -0
  74. package/dist/core/gate-engine.js +104 -13
  75. package/dist/core/harvest.d.ts +53 -0
  76. package/dist/core/harvest.js +276 -0
  77. package/dist/core/index.d.ts +9 -3
  78. package/dist/core/index.js +6 -2
  79. package/dist/core/integrity.d.ts +2 -0
  80. package/dist/core/integrity.js +13 -0
  81. package/dist/core/judge-api-key.d.ts +19 -0
  82. package/dist/core/judge-api-key.js +74 -0
  83. package/dist/core/judge-cloud-consent.d.ts +13 -0
  84. package/dist/core/judge-cloud-consent.js +38 -0
  85. package/dist/core/judge-config.d.ts +41 -4
  86. package/dist/core/judge-config.js +263 -57
  87. package/dist/core/judge-doctor.d.ts +9 -1
  88. package/dist/core/judge-doctor.js +199 -94
  89. package/dist/core/judge-model-discovery.d.ts +24 -0
  90. package/dist/core/judge-model-discovery.js +168 -0
  91. package/dist/core/judge-model-policy.d.ts +5 -0
  92. package/dist/core/judge-model-policy.js +21 -0
  93. package/dist/core/judge-runtime-detection.d.ts +9 -0
  94. package/dist/core/judge-runtime-detection.js +68 -0
  95. package/dist/core/path-utils.d.ts +11 -0
  96. package/dist/core/path-utils.js +56 -6
  97. package/dist/core/reclassify.d.ts +3 -0
  98. package/dist/core/reclassify.js +44 -14
  99. package/dist/core/replay-scrub.d.ts +9 -0
  100. package/dist/core/replay-scrub.js +43 -0
  101. package/dist/core/shell-tokenizer.d.ts +2 -0
  102. package/dist/core/shell-tokenizer.js +82 -23
  103. package/dist/core/standing-allow.d.ts +50 -0
  104. package/dist/core/standing-allow.js +175 -0
  105. package/dist/core/transactional/diff-evaluator.js +1 -19
  106. package/dist/core/types.d.ts +17 -0
  107. package/dist/core/verdict/adapter.d.ts +1 -0
  108. package/dist/core/verdict/adapter.js +17 -2
  109. package/dist/core/verdict/containment.d.ts +8 -2
  110. package/dist/core/verdict/containment.js +45 -10
  111. package/dist/core/verdict/judge-audit.d.ts +1 -0
  112. package/dist/core/verdict/judge-audit.js +32 -1
  113. package/dist/core/verdict/judge-baseline.d.ts +20 -0
  114. package/dist/core/verdict/judge-baseline.js +48 -0
  115. package/dist/core/verdict/judge-broker-service.d.ts +42 -0
  116. package/dist/core/verdict/judge-broker-service.js +279 -0
  117. package/dist/core/verdict/judge-catalog.d.ts +40 -0
  118. package/dist/core/verdict/judge-catalog.js +148 -0
  119. package/dist/core/verdict/judge-cli-fingerprint.d.ts +5 -0
  120. package/dist/core/verdict/judge-cli-fingerprint.js +61 -0
  121. package/dist/core/verdict/judge-cli.d.ts +46 -0
  122. package/dist/core/verdict/judge-cli.js +401 -0
  123. package/dist/core/verdict/judge-factory.d.ts +15 -4
  124. package/dist/core/verdict/judge-factory.js +127 -15
  125. package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
  126. package/dist/core/verdict/judge-provider-matrix.js +66 -0
  127. package/dist/core/verdict/judge-runtime-config.d.ts +42 -0
  128. package/dist/core/verdict/judge-runtime-config.js +92 -0
  129. package/dist/core/verdict/judge-session-broker.d.ts +48 -0
  130. package/dist/core/verdict/judge-session-broker.js +195 -0
  131. package/dist/core/verdict/judge-session-guard.d.ts +27 -0
  132. package/dist/core/verdict/judge-session-guard.js +91 -0
  133. package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
  134. package/dist/core/verdict/judge-session-kill-switch.js +43 -0
  135. package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
  136. package/dist/core/verdict/judge-session-mutex.js +23 -0
  137. package/dist/core/verdict/judge-shadow.d.ts +24 -0
  138. package/dist/core/verdict/judge-shadow.js +116 -0
  139. package/dist/core/verdict/judge-transport.d.ts +40 -0
  140. package/dist/core/verdict/judge-transport.js +258 -0
  141. package/dist/core/verdict/judge.d.ts +33 -1
  142. package/dist/core/verdict/judge.js +84 -14
  143. package/dist/core/verdict/parser.js +6 -1
  144. package/dist/core/verdict/persistent-paths.d.ts +8 -0
  145. package/dist/core/verdict/persistent-paths.js +52 -0
  146. package/dist/core/verdict/types.d.ts +19 -2
  147. package/dist/core/verdict/verdict.js +166 -49
  148. package/dist/corpus/adversarial-probe.d.ts +86 -0
  149. package/dist/corpus/adversarial-probe.js +220 -0
  150. package/dist/corpus/evaluate.d.ts +45 -12
  151. package/dist/corpus/evaluate.js +62 -5
  152. package/dist/corpus/gates.d.ts +35 -0
  153. package/dist/corpus/gates.js +81 -0
  154. package/dist/corpus/judge-accuracy.d.ts +8 -0
  155. package/dist/corpus/judge-accuracy.js +39 -0
  156. package/dist/corpus/must-allow-commands.d.ts +5 -0
  157. package/dist/corpus/must-allow-commands.js +13 -0
  158. package/dist/corpus/mutators.d.ts +28 -0
  159. package/dist/corpus/mutators.js +151 -0
  160. package/dist/corpus/ratchet.d.ts +42 -0
  161. package/dist/corpus/ratchet.js +116 -0
  162. package/dist/corpus/runtime-match.d.ts +19 -0
  163. package/dist/corpus/runtime-match.js +45 -0
  164. package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
  165. package/dist/corpus/standing-allow-catalog.generated.js +79 -0
  166. package/dist/corpus/types.d.ts +47 -0
  167. package/dist/corpus/types.js +157 -0
  168. package/dist/installer.js +69 -15
  169. package/dist/judge-broker-daemon.d.ts +1 -0
  170. package/dist/judge-broker-daemon.js +123 -0
  171. package/dist/services/sandbox-service.d.ts +1 -0
  172. package/dist/services/sandbox-service.js +2 -0
  173. package/dist/types.d.ts +7 -0
  174. package/dist/version.d.ts +1 -1
  175. package/dist/version.js +1 -1
  176. package/package.json +7 -3
  177. package/skills/belay/SKILL.md +15 -8
  178. package/skills/belay/belay-approve.md +17 -0
  179. package/dist/commands/init-wizard.d.ts +0 -21
  180. package/dist/commands/init-wizard.js +0 -63
@@ -0,0 +1,401 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { buildTier1Prompt, parseTier1Json, prescanInterpreterCode, } from './judge.js';
3
+ import { scrubOutboundForJudge } from './judge-outbound.js';
4
+ import { DEFAULT_JUDGE_RUNTIME_CONFIG } from './judge-runtime-config.js';
5
+ import { transportFallbackToFailClosedReason } from './judge-session-guard.js';
6
+ import { evaluateWithJudgeTransport } from './judge-transport.js';
7
+ export class CliRunError extends Error {
8
+ kind;
9
+ exitCode;
10
+ stderr;
11
+ stdout;
12
+ command;
13
+ constructor(kind, message, details = {}) {
14
+ super(message);
15
+ this.name = 'CliRunError';
16
+ this.kind = kind;
17
+ this.exitCode = details.exitCode;
18
+ this.stderr = details.stderr;
19
+ this.stdout = details.stdout;
20
+ this.command = details.command;
21
+ }
22
+ }
23
+ const CLI_BINARIES = {
24
+ codex: 'codex',
25
+ cursor: 'cursor-agent',
26
+ claude: 'claude',
27
+ };
28
+ const FORCE_KILL_GRACE_MS = 250;
29
+ function failClosedVerdict(reason) {
30
+ return {
31
+ local_recoverable: false,
32
+ destroys_outside_repo: true,
33
+ destroys_history_or_secrets: true,
34
+ external_change: true,
35
+ reason,
36
+ };
37
+ }
38
+ function transportForProvider(providerId) {
39
+ return `${providerId}-cli`;
40
+ }
41
+ function traceProvider(providerId) {
42
+ if (providerId === 'claude') {
43
+ return 'anthropic';
44
+ }
45
+ return 'openai-compatible';
46
+ }
47
+ function initialTrace(options) {
48
+ return {
49
+ provider: traceProvider(options.providerId),
50
+ modelRequested: options.modelRequested,
51
+ modelResolved: options.modelResolved,
52
+ latencyMs: 0,
53
+ transport: transportForProvider(options.providerId),
54
+ };
55
+ }
56
+ export function buildCliInvocation(providerId, prompt, model) {
57
+ if (providerId === 'codex') {
58
+ return {
59
+ binary: CLI_BINARIES[providerId],
60
+ args: [
61
+ 'exec',
62
+ '--json',
63
+ '--skip-git-repo-check',
64
+ '--sandbox',
65
+ 'read-only',
66
+ '--model',
67
+ model,
68
+ '-',
69
+ ],
70
+ stdin: prompt,
71
+ };
72
+ }
73
+ if (providerId === 'cursor') {
74
+ return {
75
+ binary: CLI_BINARIES[providerId],
76
+ args: [
77
+ '--print',
78
+ '--output-format',
79
+ 'json',
80
+ '--mode',
81
+ 'ask',
82
+ '--sandbox',
83
+ 'enabled',
84
+ '--trust',
85
+ '--model',
86
+ model,
87
+ prompt,
88
+ ],
89
+ };
90
+ }
91
+ return {
92
+ binary: CLI_BINARIES[providerId],
93
+ args: [
94
+ '-p',
95
+ '--output-format',
96
+ 'json',
97
+ '--permission-mode',
98
+ 'plan',
99
+ '--tools',
100
+ '',
101
+ '--bare',
102
+ '--model',
103
+ model,
104
+ ],
105
+ stdin: prompt,
106
+ };
107
+ }
108
+ function tryParseJson(raw) {
109
+ try {
110
+ return JSON.parse(raw);
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ }
116
+ function extractVerdictFromJsonValue(value) {
117
+ if (typeof value === 'string') {
118
+ return parseTier1Json(value.trim());
119
+ }
120
+ if (Array.isArray(value)) {
121
+ for (let index = value.length - 1; index >= 0; index -= 1) {
122
+ const verdict = extractVerdictFromJsonValue(value[index]);
123
+ if (verdict) {
124
+ return verdict;
125
+ }
126
+ }
127
+ return null;
128
+ }
129
+ if (!value || typeof value !== 'object') {
130
+ return null;
131
+ }
132
+ const record = value;
133
+ for (const key of ['result', 'content', 'text', 'message', 'output_text', 'final']) {
134
+ if (!(key in record)) {
135
+ continue;
136
+ }
137
+ const verdict = extractVerdictFromJsonValue(record[key]);
138
+ if (verdict) {
139
+ return verdict;
140
+ }
141
+ }
142
+ for (const candidate of Object.values(record).reverse()) {
143
+ const verdict = extractVerdictFromJsonValue(candidate);
144
+ if (verdict) {
145
+ return verdict;
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+ function parseCliJsonLines(raw) {
151
+ const lines = raw
152
+ .split(/\r?\n/)
153
+ .map((line) => line.trim())
154
+ .filter(Boolean);
155
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
156
+ const parsed = tryParseJson(lines[index] ?? '');
157
+ if (!parsed) {
158
+ continue;
159
+ }
160
+ const verdict = extractVerdictFromJsonValue(parsed);
161
+ if (verdict) {
162
+ return verdict;
163
+ }
164
+ }
165
+ return null;
166
+ }
167
+ export function parseCliJudgeOutput(providerId, raw) {
168
+ const direct = parseTier1Json(raw);
169
+ if (direct) {
170
+ return direct;
171
+ }
172
+ const parsed = tryParseJson(raw);
173
+ if (parsed) {
174
+ const envelopeVerdict = extractVerdictFromJsonValue(parsed);
175
+ if (envelopeVerdict) {
176
+ return envelopeVerdict;
177
+ }
178
+ }
179
+ if (providerId === 'codex') {
180
+ return parseCliJsonLines(raw);
181
+ }
182
+ return parseCliJsonLines(raw);
183
+ }
184
+ async function runCliJson(invocation, timeoutMs) {
185
+ return runCliJsonWithTimeouts(invocation, { evalTimeoutMs: timeoutMs });
186
+ }
187
+ export async function runCliJsonWithTimeouts(invocation, budgets) {
188
+ const { binary, args, stdin } = invocation;
189
+ const connectTimeoutMs = budgets.connectTimeoutMs ?? 0;
190
+ if (connectTimeoutMs > 0) {
191
+ await new Promise((resolve) => setTimeout(resolve, 0));
192
+ }
193
+ return new Promise((resolve, reject) => {
194
+ const child = spawn(binary, args, {
195
+ stdio: ['pipe', 'pipe', 'pipe'],
196
+ });
197
+ const command = [binary, ...args].join(' ');
198
+ let stdout = '';
199
+ let stderr = '';
200
+ let settled = false;
201
+ let timer = null;
202
+ let forceKillTimer = null;
203
+ const cleanup = () => {
204
+ if (timer) {
205
+ clearTimeout(timer);
206
+ }
207
+ if (forceKillTimer) {
208
+ clearTimeout(forceKillTimer);
209
+ }
210
+ };
211
+ const rejectOnce = (error) => {
212
+ if (settled)
213
+ return;
214
+ settled = true;
215
+ cleanup();
216
+ reject(error);
217
+ };
218
+ const resolveOnce = (value) => {
219
+ if (settled)
220
+ return;
221
+ settled = true;
222
+ cleanup();
223
+ resolve(value);
224
+ };
225
+ timer = setTimeout(() => {
226
+ rejectOnce(new CliRunError('timeout', `${binary} timed out`, {
227
+ stderr: stderr.trim() || undefined,
228
+ stdout: stdout.trim() || undefined,
229
+ command,
230
+ }));
231
+ try {
232
+ child.kill('SIGTERM');
233
+ }
234
+ catch {
235
+ return;
236
+ }
237
+ forceKillTimer = setTimeout(() => {
238
+ try {
239
+ child.kill('SIGKILL');
240
+ }
241
+ catch {
242
+ // process already exited
243
+ }
244
+ }, FORCE_KILL_GRACE_MS);
245
+ }, budgets.evalTimeoutMs);
246
+ child.stdout.on('data', (chunk) => {
247
+ stdout += String(chunk);
248
+ });
249
+ child.stderr.on('data', (chunk) => {
250
+ stderr += String(chunk);
251
+ });
252
+ if (stdin !== undefined) {
253
+ child.stdin.write(stdin);
254
+ }
255
+ child.stdin.end();
256
+ child.on('error', (error) => {
257
+ rejectOnce(new CliRunError('spawn_error', error instanceof Error ? error.message : String(error), {
258
+ command,
259
+ }));
260
+ });
261
+ child.on('close', (code) => {
262
+ if (settled)
263
+ return;
264
+ if (code === 0 && stdout.trim()) {
265
+ resolveOnce(stdout.trim());
266
+ return;
267
+ }
268
+ rejectOnce(new CliRunError('exit_nonzero', stderr.trim() || `${binary} exited with code ${code ?? 'unknown'}`, {
269
+ exitCode: typeof code === 'number' ? code : undefined,
270
+ stderr: stderr.trim() || undefined,
271
+ stdout: stdout.trim() || undefined,
272
+ command,
273
+ }));
274
+ });
275
+ });
276
+ }
277
+ function createCliJudge(options) {
278
+ const transport = transportForProvider(options.providerId);
279
+ const timeoutMs = options.timeoutMs ?? 25000;
280
+ const providerId = options.providerId;
281
+ const runCommand = options.runCliCommand ?? runCliJson;
282
+ const runtime = options.runtime ?? DEFAULT_JUDGE_RUNTIME_CONFIG;
283
+ const repoRoot = options.repoRoot ?? process.cwd();
284
+ const stateDir = options.stateDir ?? repoRoot;
285
+ const judgeMode = options.judgeMode ?? 'enforce';
286
+ const judge = {
287
+ lastTrace: initialTrace(options),
288
+ async evaluate(input) {
289
+ const started = Date.now();
290
+ const prescan = input.innerCode ? prescanInterpreterCode(input.innerCode) : null;
291
+ if (prescan?.destroys_history_or_secrets) {
292
+ judge.lastTrace = {
293
+ provider: traceProvider(options.providerId),
294
+ modelRequested: options.modelRequested,
295
+ modelResolved: options.modelResolved,
296
+ latencyMs: Date.now() - started,
297
+ transport,
298
+ };
299
+ return prescan;
300
+ }
301
+ const scrubbed = scrubOutboundForJudge(input.text, {
302
+ sensitivePaths: options.sensitivePaths,
303
+ scrubOptions: options.scrubOptions,
304
+ });
305
+ if (!scrubbed.ok) {
306
+ judge.lastTrace = {
307
+ provider: 'fallback',
308
+ modelRequested: options.modelRequested,
309
+ modelResolved: options.modelResolved,
310
+ latencyMs: Date.now() - started,
311
+ fallbackReason: scrubbed.reason,
312
+ };
313
+ return failClosedVerdict('outbound_scrub_failed');
314
+ }
315
+ const prompt = buildTier1Prompt(scrubbed.text);
316
+ try {
317
+ const transportResult = await evaluateWithJudgeTransport({
318
+ prompt,
319
+ context: {
320
+ providerId,
321
+ model: options.modelResolved,
322
+ repoRoot: input.context.repoRoot || repoRoot,
323
+ stateDir,
324
+ judgeMode,
325
+ runtime,
326
+ judgeTimeoutMs: timeoutMs,
327
+ },
328
+ }, { runCommand });
329
+ const traceTransport = transportResult.transport === 'session'
330
+ ? `${providerId}-cli-session`
331
+ : transport;
332
+ if (!transportResult.verdict) {
333
+ const fallbackReason = transportResult.fallbackReason ??
334
+ `${providerId}_cli_parse_error`;
335
+ judge.lastTrace = {
336
+ provider: 'fallback',
337
+ modelRequested: options.modelRequested,
338
+ modelResolved: options.modelResolved,
339
+ latencyMs: Date.now() - started,
340
+ fallbackReason,
341
+ judgeFallbackReason: fallbackReason,
342
+ judgeSessionUsed: transportResult.sessionUsed,
343
+ judgeSessionReused: transportResult.sessionReused,
344
+ judgeSessionRefHash: transportResult.sessionRefHash,
345
+ judgeSessionResetReason: transportResult.sessionResetReason,
346
+ judgeConnectMs: transportResult.connectMs,
347
+ judgeEvalMs: transportResult.evalMs,
348
+ judgeParseMs: transportResult.parseMs,
349
+ judgeShadowCompared: transportResult.shadowCompared,
350
+ judgeShadowMismatch: transportResult.shadowMismatch,
351
+ judgeShadowMismatchRateWindow: transportResult.shadowMismatchRateWindow,
352
+ judgeKillSwitchTriggered: transportResult.killSwitchTriggered,
353
+ };
354
+ return failClosedVerdict(transportFallbackToFailClosedReason(providerId, fallbackReason));
355
+ }
356
+ judge.lastTrace = {
357
+ provider: traceProvider(options.providerId),
358
+ modelRequested: options.modelRequested,
359
+ modelResolved: options.modelResolved,
360
+ latencyMs: Date.now() - started,
361
+ transport: traceTransport,
362
+ outboundRedacted: true,
363
+ judgeSessionUsed: transportResult.sessionUsed,
364
+ judgeSessionReused: transportResult.sessionReused,
365
+ judgeSessionRefHash: transportResult.sessionRefHash,
366
+ judgeSessionResetReason: transportResult.sessionResetReason,
367
+ judgeFallbackReason: transportResult.fallbackReason,
368
+ judgeConnectMs: transportResult.connectMs,
369
+ judgeEvalMs: transportResult.evalMs,
370
+ judgeParseMs: transportResult.parseMs,
371
+ judgeShadowCompared: transportResult.shadowCompared,
372
+ judgeShadowMismatch: transportResult.shadowMismatch,
373
+ judgeShadowMismatchRateWindow: transportResult.shadowMismatchRateWindow,
374
+ judgeKillSwitchTriggered: transportResult.killSwitchTriggered,
375
+ };
376
+ return transportResult.verdict;
377
+ }
378
+ catch {
379
+ judge.lastTrace = {
380
+ provider: 'fallback',
381
+ modelRequested: options.modelRequested,
382
+ modelResolved: options.modelResolved,
383
+ latencyMs: Date.now() - started,
384
+ fallbackReason: `${providerId}_cli_unavailable`,
385
+ judgeFallbackReason: `${providerId}_cli_unavailable`,
386
+ };
387
+ return failClosedVerdict(`${providerId}_cli_unavailable`);
388
+ }
389
+ },
390
+ };
391
+ return judge;
392
+ }
393
+ export function createCodexCliJudge(options) {
394
+ return createCliJudge({ ...options, providerId: 'codex' });
395
+ }
396
+ export function createCursorCliJudge(options) {
397
+ return createCliJudge({ ...options, providerId: 'cursor' });
398
+ }
399
+ export function createClaudeCliJudge(options) {
400
+ return createCliJudge({ ...options, providerId: 'claude' });
401
+ }
@@ -9,17 +9,28 @@ export declare function loadPinnedJudgeModels(): Promise<{
9
9
  ciPin: string;
10
10
  };
11
11
  }>;
12
- export declare function resolveCloudModel(requested: string, pinned: {
12
+ export declare function resolveJudgeModel(judge: BelayJudgeConfig): {
13
+ requested: string;
14
+ resolved: string;
15
+ };
16
+ /** @deprecated Use resolveJudgeModel */
17
+ export declare const resolveCloudModel: (requested: string, _pinned: {
18
+ autoResolved: string;
19
+ }) => {
20
+ requested: string;
21
+ resolved: string;
22
+ };
23
+ /** @deprecated Use resolveJudgeModel */
24
+ export declare const resolveCursorModel: (requested: string, _pinned: {
13
25
  autoResolved: string;
14
- }): {
26
+ }) => {
15
27
  requested: string;
16
28
  resolved: string;
17
29
  };
18
- /** @deprecated Use resolveCloudModel */
19
- export declare const resolveCursorModel: typeof resolveCloudModel;
20
30
  export declare function createJudgeFromConfig(config: BelayConfigV4, options?: {
21
31
  pinnedModels?: {
22
32
  autoResolved: string;
23
33
  };
34
+ repoRoot?: string;
24
35
  }): TracedTier1Judge;
25
36
  export declare function judgeConfigSummary(judge: BelayJudgeConfig): string;
@@ -1,7 +1,14 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { fileURLToPath } from 'node:url';
3
- import { normalizeJudgeProvider, scrubOptionsFromConfig } from '../config.js';
3
+ import { repoLocalStateDirFor } from '../../config-io.js';
4
+ import { belayStateDir, normalizeJudgeProvider, scrubOptionsFromConfig } from '../config.js';
5
+ import { resolveJudgeCredential } from '../judge-api-key.js';
6
+ import { hasValidCloudConsent, isCloudJudgeConfig } from '../judge-config.js';
7
+ import { rejectDeprecatedJudgeModelAuto } from '../judge-model-policy.js';
8
+ import { detectJudgeRuntimeCapabilities } from '../judge-runtime-detection.js';
4
9
  import { createDeterministicJudgeStub, createFailClosedJudge, createOllamaJudge, createOpenAiCompatibleJudge, } from './judge.js';
10
+ import { getJudgeProviderSpec, inferProviderIdFromConfig, isRemovedProviderId, normalizeLegacyProviderId, } from './judge-catalog.js';
11
+ import { createClaudeCliJudge, createCodexCliJudge, createCursorCliJudge } from './judge-cli.js';
5
12
  const FIXTURE_MODELS_URL = new URL('../../../fixtures/judge-models.json', import.meta.url);
6
13
  let cachedPinnedModels = null;
7
14
  export function resetPinnedJudgeModelsCache() {
@@ -24,33 +31,104 @@ export async function loadPinnedJudgeModels() {
24
31
  return cachedPinnedModels;
25
32
  }
26
33
  }
27
- export function resolveCloudModel(requested, pinned) {
28
- if (requested === 'auto') {
29
- const envResolved = process.env.BELAY_JUDGE_MODEL_RESOLVED?.trim();
30
- return {
31
- requested,
32
- resolved: envResolved || pinned.autoResolved,
33
- };
34
+ export function resolveJudgeModel(judge) {
35
+ rejectDeprecatedJudgeModelAuto(judge.model);
36
+ const requested = judge.model;
37
+ const envOverride = testJudgeModelOverride();
38
+ return { requested, resolved: envOverride || requested };
39
+ }
40
+ function testJudgeModelOverride() {
41
+ if (!process.env.VITEST && !process.env.VITEST_WORKER_ID) {
42
+ return undefined;
34
43
  }
35
- return { requested, resolved: requested };
44
+ return process.env.BELAY_JUDGE_MODEL_RESOLVED?.trim() || undefined;
36
45
  }
37
- /** @deprecated Use resolveCloudModel */
46
+ /** @deprecated Use resolveJudgeModel */
47
+ export const resolveCloudModel = (requested, _pinned) => {
48
+ rejectDeprecatedJudgeModelAuto(requested);
49
+ const envResolved = testJudgeModelOverride();
50
+ return {
51
+ requested,
52
+ resolved: envResolved || requested,
53
+ };
54
+ };
55
+ /** @deprecated Use resolveJudgeModel */
38
56
  export const resolveCursorModel = resolveCloudModel;
57
+ function providerIdFromJudge(judge) {
58
+ if (judge.providerId) {
59
+ const normalized = normalizeLegacyProviderId(judge.providerId);
60
+ if (normalized) {
61
+ return normalized;
62
+ }
63
+ }
64
+ return inferProviderIdFromConfig(judge);
65
+ }
66
+ function createCliJudgeForProvider(providerId, judgeConfig, config, repoRoot) {
67
+ const resolvedRepoRoot = repoRoot ?? process.cwd();
68
+ const { resolved } = resolveJudgeModel(judgeConfig);
69
+ const options = {
70
+ modelRequested: judgeConfig.model,
71
+ modelResolved: resolved,
72
+ timeoutMs: judgeConfig.timeoutMs,
73
+ sensitivePaths: config.classifier.sensitivePaths,
74
+ scrubOptions: scrubOptionsFromConfig(config),
75
+ runtime: judgeConfig.runtime,
76
+ repoRoot: resolvedRepoRoot,
77
+ stateDir: belayStateDir(config, repoLocalStateDirFor(resolvedRepoRoot, config)),
78
+ judgeMode: config.mode,
79
+ };
80
+ if (providerId === 'codex') {
81
+ return createCodexCliJudge(options);
82
+ }
83
+ if (providerId === 'cursor') {
84
+ return createCursorCliJudge(options);
85
+ }
86
+ return createClaudeCliJudge(options);
87
+ }
39
88
  export function createJudgeFromConfig(config, options = {}) {
89
+ if (process.env.BELAY_DETERMINISTIC_JUDGE === '1') {
90
+ return createDeterministicJudgeStub();
91
+ }
40
92
  const judgeConfig = config.judge;
93
+ if (judgeConfig.providerId && isRemovedProviderId(String(judgeConfig.providerId))) {
94
+ const { resolved } = resolveJudgeModel(judgeConfig);
95
+ return createFailClosedJudge({
96
+ reason: 'judge_provider_removed',
97
+ fallbackReason: 'provider_migration_required',
98
+ modelRequested: judgeConfig.model,
99
+ modelResolved: resolved,
100
+ });
101
+ }
41
102
  const provider = normalizeJudgeProvider(judgeConfig.provider);
103
+ const providerId = providerIdFromJudge(judgeConfig);
104
+ const catalogSpec = getJudgeProviderSpec(providerId);
105
+ const { resolved } = resolveJudgeModel(judgeConfig);
106
+ const runtime = detectJudgeRuntimeCapabilities(providerId);
107
+ const repoRoot = options.repoRoot ?? process.cwd();
42
108
  if (provider === 'openai-compatible') {
43
109
  const endpoint = judgeConfig.endpoint?.trim();
110
+ if (!endpoint && runtime.cliTransport) {
111
+ if (providerId === 'codex' || providerId === 'cursor') {
112
+ return createCliJudgeForProvider(providerId, judgeConfig, config, repoRoot);
113
+ }
114
+ }
44
115
  if (!endpoint) {
45
116
  return createFailClosedJudge({
46
117
  reason: 'openai_compatible_endpoint_missing',
47
118
  fallbackReason: 'missing_endpoint',
48
119
  modelRequested: judgeConfig.model,
49
- modelResolved: judgeConfig.model,
120
+ modelResolved: resolved,
121
+ });
122
+ }
123
+ if (isCloudJudgeConfig(judgeConfig) && !hasValidCloudConsent(judgeConfig)) {
124
+ return createFailClosedJudge({
125
+ reason: 'openai_compatible_consent_missing',
126
+ fallbackReason: 'cloud_consent_missing',
127
+ modelRequested: judgeConfig.model,
128
+ modelResolved: resolved,
50
129
  });
51
130
  }
52
- const pinned = options.pinnedModels ?? { autoResolved: 'composer-2.5' };
53
- const { resolved } = resolveCloudModel(judgeConfig.model, pinned);
131
+ const repoLocalStateDir = repoLocalStateDirFor(repoRoot, config);
54
132
  return createOpenAiCompatibleJudge({
55
133
  endpoint,
56
134
  modelRequested: judgeConfig.model,
@@ -58,6 +136,13 @@ export function createJudgeFromConfig(config, options = {}) {
58
136
  timeoutMs: judgeConfig.timeoutMs,
59
137
  sensitivePaths: config.classifier.sensitivePaths,
60
138
  scrubOptions: scrubOptionsFromConfig(config),
139
+ resolveApiKey: async () => resolveJudgeCredential({
140
+ judge: judgeConfig,
141
+ catalogSpec: catalogSpec ?? undefined,
142
+ repoRoot,
143
+ repoLocalStateDir,
144
+ config,
145
+ }),
61
146
  });
62
147
  }
63
148
  if (provider === 'ollama') {
@@ -68,8 +153,35 @@ export function createJudgeFromConfig(config, options = {}) {
68
153
  keepAlive: judgeConfig.keepAlive,
69
154
  });
70
155
  }
71
- return createDeterministicJudgeStub();
156
+ if (provider === 'anthropic') {
157
+ if (runtime.cliTransport) {
158
+ return createClaudeCliJudge({
159
+ modelRequested: judgeConfig.model,
160
+ modelResolved: resolved,
161
+ timeoutMs: judgeConfig.timeoutMs,
162
+ sensitivePaths: config.classifier.sensitivePaths,
163
+ scrubOptions: scrubOptionsFromConfig(config),
164
+ runtime: judgeConfig.runtime,
165
+ repoRoot,
166
+ stateDir: belayStateDir(config, repoLocalStateDirFor(repoRoot, config)),
167
+ judgeMode: config.mode,
168
+ });
169
+ }
170
+ return createFailClosedJudge({
171
+ reason: 'anthropic_not_implemented',
172
+ fallbackReason: 'anthropic_runtime_unavailable',
173
+ modelRequested: judgeConfig.model,
174
+ modelResolved: resolved,
175
+ });
176
+ }
177
+ return createFailClosedJudge({
178
+ reason: 'unsupported_judge_provider',
179
+ fallbackReason: 'unsupported_provider',
180
+ modelRequested: judgeConfig.model,
181
+ modelResolved: resolved,
182
+ });
72
183
  }
73
184
  export function judgeConfigSummary(judge) {
74
- return `${judge.provider}/${judge.model}`;
185
+ const id = judge.providerId ?? judge.provider;
186
+ return `${id}/${judge.model}`;
75
187
  }
@@ -0,0 +1,20 @@
1
+ import type { JudgeProviderId } from './judge-catalog.js';
2
+ export type JudgeSessionFailureMode = 'parse_error' | 'non_json_response' | 'timeout' | 'cli_nonzero_exit' | 'io_error' | 'version_mismatch' | 'unsupported_resume' | 'unsafe_option_rejected';
3
+ export interface JudgeProviderSessionCapability {
4
+ providerId: Exclude<JudgeProviderId, 'ollama'>;
5
+ supportsSession: boolean;
6
+ resumeFlag: string | null;
7
+ createChatFlag: string | null;
8
+ readOnlyArgs: string[];
9
+ rejectedUnsafeOptions: string[];
10
+ failureModes: JudgeSessionFailureMode[];
11
+ notes: string;
12
+ }
13
+ export declare const JUDGE_PROVIDER_SESSION_MATRIX: Record<Exclude<JudgeProviderId, 'ollama'>, JudgeProviderSessionCapability>;
14
+ export declare function providerSupportsSession(providerId: Exclude<JudgeProviderId, 'ollama'>, allowlist: JudgeProviderId[]): boolean;
15
+ export declare function assertReadOnlyInvocationArgs(providerId: Exclude<JudgeProviderId, 'ollama'>, args: string[]): {
16
+ ok: true;
17
+ } | {
18
+ ok: false;
19
+ reason: JudgeSessionFailureMode;
20
+ };