@guilz-dev/belay 0.3.0 → 0.6.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 (242) hide show
  1. package/README.md +7 -1
  2. package/dist/adapters/cursor/runtime-entry.js +1 -0
  3. package/dist/adapters/layouts/scope.js +3 -2
  4. package/dist/adapters/shared/gate-runtime.d.ts +12 -16
  5. package/dist/adapters/shared/gate-runtime.js +499 -43
  6. package/dist/bundle/claude-runtime.mjs +8004 -2676
  7. package/dist/bundle/codex-runtime.mjs +8021 -2683
  8. package/dist/bundle/cursor-runtime.mjs +7993 -2667
  9. package/dist/cli.js +180 -6
  10. package/dist/commands/approve.d.ts +1 -0
  11. package/dist/commands/approve.js +45 -26
  12. package/dist/commands/classify-for-report.js +1 -1
  13. package/dist/commands/config.d.ts +8 -0
  14. package/dist/commands/config.js +264 -58
  15. package/dist/commands/doctor.js +47 -2
  16. package/dist/commands/explain.js +17 -2
  17. package/dist/commands/harvest.d.ts +29 -0
  18. package/dist/commands/harvest.js +72 -0
  19. package/dist/commands/judge.d.ts +11 -1
  20. package/dist/commands/judge.js +24 -4
  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/session.d.ts +20 -0
  25. package/dist/commands/session.js +51 -0
  26. package/dist/commands/simulate.d.ts +1 -0
  27. package/dist/commands/simulate.js +7 -2
  28. package/dist/commands/standing-allow.d.ts +10 -0
  29. package/dist/commands/standing-allow.js +26 -0
  30. package/dist/commands/tui.d.ts +48 -0
  31. package/dist/commands/tui.js +150 -0
  32. package/dist/config-io.js +59 -17
  33. package/dist/conformance/guarantee-posture.d.ts +21 -0
  34. package/dist/conformance/guarantee-posture.js +46 -0
  35. package/dist/conformance/guarantee-table.d.ts +1 -0
  36. package/dist/conformance/guarantee-table.js +14 -9
  37. package/dist/core/approval-replay-cli.d.ts +7 -0
  38. package/dist/core/approval-replay-cli.js +36 -0
  39. package/dist/core/approval-replay.d.ts +45 -0
  40. package/dist/core/approval-replay.js +141 -0
  41. package/dist/core/approval-service.d.ts +15 -0
  42. package/dist/core/approval-service.js +53 -4
  43. package/dist/core/approval.d.ts +26 -3
  44. package/dist/core/approval.js +61 -7
  45. package/dist/core/audit-analysis.d.ts +7 -1
  46. package/dist/core/audit-analysis.js +111 -0
  47. package/dist/core/audit-metrics.d.ts +7 -0
  48. package/dist/core/audit-metrics.js +24 -4
  49. package/dist/core/audit-query.d.ts +1 -0
  50. package/dist/core/audit-query.js +3 -0
  51. package/dist/core/audit-replay-context.d.ts +35 -0
  52. package/dist/core/audit-replay-context.js +88 -0
  53. package/dist/core/audit-types.d.ts +24 -1
  54. package/dist/core/audit-types.js +1 -1
  55. package/dist/core/capability/approval-state-mutation.d.ts +13 -0
  56. package/dist/core/capability/approval-state-mutation.js +92 -0
  57. package/dist/core/capability/approval-v3.d.ts +15 -0
  58. package/dist/core/capability/approval-v3.js +74 -0
  59. package/dist/core/capability/attestation.d.ts +13 -0
  60. package/dist/core/capability/attestation.js +55 -0
  61. package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
  62. package/dist/core/capability/boundary-attestation-sign.js +84 -0
  63. package/dist/core/capability/boundary-driver-container.d.ts +7 -0
  64. package/dist/core/capability/boundary-driver-container.js +122 -0
  65. package/dist/core/capability/boundary-driver.d.ts +29 -0
  66. package/dist/core/capability/boundary-driver.js +43 -0
  67. package/dist/core/capability/boundary-egress.d.ts +21 -0
  68. package/dist/core/capability/boundary-egress.js +109 -0
  69. package/dist/core/capability/boundary-grant-materialize.d.ts +13 -0
  70. package/dist/core/capability/boundary-grant-materialize.js +78 -0
  71. package/dist/core/capability/boundary-profile.d.ts +15 -0
  72. package/dist/core/capability/boundary-profile.js +25 -0
  73. package/dist/core/capability/boundary-run.d.ts +23 -0
  74. package/dist/core/capability/boundary-run.js +23 -0
  75. package/dist/core/capability/boundary-session.d.ts +57 -0
  76. package/dist/core/capability/boundary-session.js +129 -0
  77. package/dist/core/capability/capability-request-hash.d.ts +2 -0
  78. package/dist/core/capability/capability-request-hash.js +8 -0
  79. package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
  80. package/dist/core/capability/gate-latency-ratchet.js +18 -0
  81. package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
  82. package/dist/core/capability/gate-policy-shadow.js +108 -0
  83. package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
  84. package/dist/core/capability/gate-shadow-audit.js +1 -0
  85. package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
  86. package/dist/core/capability/gate-shadow-ratchet.js +81 -0
  87. package/dist/core/capability/grant-lease.d.ts +9 -0
  88. package/dist/core/capability/grant-lease.js +46 -0
  89. package/dist/core/capability/grant-loader.d.ts +12 -0
  90. package/dist/core/capability/grant-loader.js +23 -0
  91. package/dist/core/capability/grant-match.d.ts +6 -0
  92. package/dist/core/capability/grant-match.js +93 -0
  93. package/dist/core/capability/grant.d.ts +16 -0
  94. package/dist/core/capability/grant.js +16 -0
  95. package/dist/core/capability/index.d.ts +10 -1
  96. package/dist/core/capability/index.js +8 -0
  97. package/dist/core/capability/limits.d.ts +7 -0
  98. package/dist/core/capability/limits.js +55 -0
  99. package/dist/core/capability/paths.d.ts +2 -2
  100. package/dist/core/capability/paths.js +9 -9
  101. package/dist/core/capability/policy-bridge.d.ts +18 -0
  102. package/dist/core/capability/policy-bridge.js +51 -0
  103. package/dist/core/capability/policy-engine.d.ts +70 -0
  104. package/dist/core/capability/policy-engine.js +476 -0
  105. package/dist/core/capability/policy-types.d.ts +20 -0
  106. package/dist/core/capability/policy-types.js +1 -0
  107. package/dist/core/capability/request.d.ts +45 -0
  108. package/dist/core/capability/request.js +1 -0
  109. package/dist/core/capability/resolver.d.ts +4 -0
  110. package/dist/core/capability/resolver.js +4 -0
  111. package/dist/core/capability/trusted-workspace-roots.d.ts +25 -0
  112. package/dist/core/capability/trusted-workspace-roots.js +149 -0
  113. package/dist/core/capability/types.d.ts +11 -1
  114. package/dist/core/capability-approval.d.ts +2 -1
  115. package/dist/core/capability-approval.js +100 -2
  116. package/dist/core/classify-subagent.d.ts +2 -1
  117. package/dist/core/classify-subagent.js +40 -22
  118. package/dist/core/classify-tool.js +157 -64
  119. package/dist/core/config.d.ts +32 -3
  120. package/dist/core/config.js +105 -3
  121. package/dist/core/gate-contract.d.ts +3 -0
  122. package/dist/core/gate-contract.js +3 -0
  123. package/dist/core/gate-engine.js +12 -7
  124. package/dist/core/harvest.d.ts +53 -0
  125. package/dist/core/harvest.js +276 -0
  126. package/dist/core/index.d.ts +9 -3
  127. package/dist/core/index.js +6 -2
  128. package/dist/core/judge-doctor.d.ts +3 -0
  129. package/dist/core/judge-doctor.js +90 -11
  130. package/dist/core/judge-fallback-hints.d.ts +13 -0
  131. package/dist/core/judge-fallback-hints.js +108 -0
  132. package/dist/core/path-utils.d.ts +12 -0
  133. package/dist/core/path-utils.js +60 -7
  134. package/dist/core/reclassify.d.ts +3 -0
  135. package/dist/core/reclassify.js +44 -14
  136. package/dist/core/recovery/capability.d.ts +3 -0
  137. package/dist/core/recovery/capability.js +22 -0
  138. package/dist/core/recovery/fail-closed.d.ts +6 -0
  139. package/dist/core/recovery/fail-closed.js +25 -0
  140. package/dist/core/recovery/index.d.ts +3 -0
  141. package/dist/core/recovery/index.js +2 -0
  142. package/dist/core/recovery/types.d.ts +20 -0
  143. package/dist/core/recovery/types.js +1 -0
  144. package/dist/core/replay-scrub.d.ts +9 -0
  145. package/dist/core/replay-scrub.js +43 -0
  146. package/dist/core/shell-tokenizer.d.ts +2 -0
  147. package/dist/core/shell-tokenizer.js +82 -23
  148. package/dist/core/standing-allow.d.ts +50 -0
  149. package/dist/core/standing-allow.js +178 -0
  150. package/dist/core/transactional/diff-evaluator.js +4 -1
  151. package/dist/core/transactional/eligibility.js +4 -0
  152. package/dist/core/transactional/git-worktree.d.ts +10 -3
  153. package/dist/core/transactional/git-worktree.js +81 -11
  154. package/dist/core/transactional/reasons.d.ts +1 -0
  155. package/dist/core/transactional/reasons.js +4 -0
  156. package/dist/core/transactional/runner.js +22 -6
  157. package/dist/core/transactional/types.d.ts +4 -0
  158. package/dist/core/types.d.ts +36 -3
  159. package/dist/core/verdict/adapter.d.ts +3 -4
  160. package/dist/core/verdict/adapter.js +19 -6
  161. package/dist/core/verdict/containment.d.ts +4 -3
  162. package/dist/core/verdict/containment.js +17 -12
  163. package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
  164. package/dist/core/verdict/cursor-acp-client.js +345 -0
  165. package/dist/core/verdict/egress-classify.d.ts +2 -2
  166. package/dist/core/verdict/egress-classify.js +2 -2
  167. package/dist/core/verdict/git-classifier.d.ts +26 -0
  168. package/dist/core/verdict/git-classifier.js +533 -0
  169. package/dist/core/verdict/judge-audit.d.ts +1 -0
  170. package/dist/core/verdict/judge-audit.js +33 -1
  171. package/dist/core/verdict/judge-baseline.d.ts +20 -0
  172. package/dist/core/verdict/judge-baseline.js +49 -0
  173. package/dist/core/verdict/judge-broker-service.d.ts +42 -0
  174. package/dist/core/verdict/judge-broker-service.js +291 -0
  175. package/dist/core/verdict/judge-catalog.js +1 -0
  176. package/dist/core/verdict/judge-cli-fingerprint.d.ts +6 -0
  177. package/dist/core/verdict/judge-cli-fingerprint.js +67 -0
  178. package/dist/core/verdict/judge-cli.d.ts +23 -0
  179. package/dist/core/verdict/judge-cli.js +139 -16
  180. package/dist/core/verdict/judge-factory.js +13 -4
  181. package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
  182. package/dist/core/verdict/judge-provider-matrix.js +66 -0
  183. package/dist/core/verdict/judge-runtime-config.d.ts +44 -0
  184. package/dist/core/verdict/judge-runtime-config.js +96 -0
  185. package/dist/core/verdict/judge-session-broker.d.ts +48 -0
  186. package/dist/core/verdict/judge-session-broker.js +200 -0
  187. package/dist/core/verdict/judge-session-guard.d.ts +27 -0
  188. package/dist/core/verdict/judge-session-guard.js +91 -0
  189. package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
  190. package/dist/core/verdict/judge-session-kill-switch.js +43 -0
  191. package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
  192. package/dist/core/verdict/judge-session-mutex.js +23 -0
  193. package/dist/core/verdict/judge-shadow.d.ts +24 -0
  194. package/dist/core/verdict/judge-shadow.js +116 -0
  195. package/dist/core/verdict/judge-transport.d.ts +44 -0
  196. package/dist/core/verdict/judge-transport.js +268 -0
  197. package/dist/core/verdict/judge.d.ts +14 -14
  198. package/dist/core/verdict/judge.js +5 -67
  199. package/dist/core/verdict/launcher-resolve.d.ts +2 -0
  200. package/dist/core/verdict/launcher-resolve.js +16 -0
  201. package/dist/core/verdict/parser.js +6 -1
  202. package/dist/core/verdict/prescan.d.ts +15 -0
  203. package/dist/core/verdict/prescan.js +68 -0
  204. package/dist/core/verdict/shell-policy.d.ts +25 -0
  205. package/dist/core/verdict/shell-policy.js +40 -0
  206. package/dist/core/verdict/shell-semantics.d.ts +24 -0
  207. package/dist/core/verdict/shell-semantics.js +225 -0
  208. package/dist/core/verdict/types.d.ts +26 -1
  209. package/dist/core/verdict/verdict.js +300 -179
  210. package/dist/corpus/adversarial-probe.d.ts +86 -0
  211. package/dist/corpus/adversarial-probe.js +220 -0
  212. package/dist/corpus/evaluate.d.ts +45 -12
  213. package/dist/corpus/evaluate.js +61 -8
  214. package/dist/corpus/gate-latency-budget.d.ts +39 -0
  215. package/dist/corpus/gate-latency-budget.js +33 -0
  216. package/dist/corpus/gates.d.ts +35 -0
  217. package/dist/corpus/gates.js +81 -0
  218. package/dist/corpus/judge-accuracy.d.ts +8 -0
  219. package/dist/corpus/judge-accuracy.js +39 -0
  220. package/dist/corpus/must-allow-commands.d.ts +5 -0
  221. package/dist/corpus/must-allow-commands.js +13 -0
  222. package/dist/corpus/mutators.d.ts +28 -0
  223. package/dist/corpus/mutators.js +162 -0
  224. package/dist/corpus/ratchet.d.ts +42 -0
  225. package/dist/corpus/ratchet.js +116 -0
  226. package/dist/corpus/runtime-match.d.ts +19 -0
  227. package/dist/corpus/runtime-match.js +43 -0
  228. package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
  229. package/dist/corpus/standing-allow-catalog.generated.js +99 -0
  230. package/dist/corpus/types.d.ts +47 -0
  231. package/dist/corpus/types.js +157 -0
  232. package/dist/installer.js +4 -1
  233. package/dist/judge-broker-daemon.d.ts +1 -0
  234. package/dist/judge-broker-daemon.js +137 -0
  235. package/dist/services/sandbox-service.d.ts +5 -0
  236. package/dist/services/sandbox-service.js +30 -2
  237. package/dist/types.d.ts +2 -2
  238. package/dist/version.d.ts +1 -1
  239. package/dist/version.js +1 -1
  240. package/package.json +7 -3
  241. package/skills/belay/SKILL.md +7 -1
  242. package/skills/belay/belay-approve.md +17 -0
@@ -0,0 +1,291 @@
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
+ const deadline = Date.now() + 1_000;
208
+ while (isProcessAlive(status.pid) && Date.now() < deadline) {
209
+ await new Promise((resolve) => setTimeout(resolve, 25));
210
+ }
211
+ if (isProcessAlive(status.pid)) {
212
+ try {
213
+ process.kill(status.pid, 'SIGKILL');
214
+ }
215
+ catch {
216
+ // already stopped
217
+ }
218
+ }
219
+ }
220
+ await cleanupJudgeBrokerArtifacts(paths);
221
+ return status?.pid ? 1 : 0;
222
+ }
223
+ export async function evaluateViaJudgeBroker(options) {
224
+ const runCommand = options.runCommand ??
225
+ ((invocation, timeout) => runCliJsonWithTimeouts(invocation, { evalTimeoutMs: timeout }));
226
+ if (useInProcessBroker()) {
227
+ const broker = getRepoJudgeSessionBroker(options.repoRoot, {
228
+ config: options.sessionConfig,
229
+ runCommand,
230
+ });
231
+ return broker.evaluate(options.request, options.timeoutMs);
232
+ }
233
+ const paths = await ensureJudgeBrokerDaemon(options.stateDir, options.repoRoot, options.sessionConfig);
234
+ return rpcEvaluate(paths.socketPath, options.request, options.timeoutMs, options.sessionConfig.connectTimeoutMs);
235
+ }
236
+ export function resetJudgeBrokerForTests() {
237
+ resetJudgeSessionBrokersForTests();
238
+ }
239
+ async function rpcInvalidate(socketPath, sessionKey, reason, connectTimeoutMs) {
240
+ const socket = await connectBrokerSocket(socketPath, connectTimeoutMs);
241
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
242
+ const payload = JSON.stringify({ type: 'invalidate', id, sessionKey, reason });
243
+ await new Promise((resolve, reject) => {
244
+ let buffer = '';
245
+ const timer = setTimeout(() => {
246
+ socket.destroy();
247
+ reject(new Error('judge broker invalidate timed out'));
248
+ }, connectTimeoutMs);
249
+ const onData = (chunk) => {
250
+ buffer += String(chunk);
251
+ const lines = buffer.split('\n');
252
+ buffer = lines.pop() ?? '';
253
+ for (const line of lines) {
254
+ if (!line.trim()) {
255
+ continue;
256
+ }
257
+ try {
258
+ const message = JSON.parse(line);
259
+ if (message.id !== id) {
260
+ continue;
261
+ }
262
+ clearTimeout(timer);
263
+ socket.destroy();
264
+ resolve();
265
+ }
266
+ catch {
267
+ // wait for more data
268
+ }
269
+ }
270
+ };
271
+ socket.on('data', onData);
272
+ socket.on('error', (error) => {
273
+ clearTimeout(timer);
274
+ reject(error);
275
+ });
276
+ socket.write(`${payload}\n`);
277
+ });
278
+ }
279
+ export async function invalidateJudgeSession(options) {
280
+ if (useInProcessBroker()) {
281
+ const broker = getRepoJudgeSessionBroker(options.repoRoot, {
282
+ config: { ...options.sessionConfig, enabled: true },
283
+ runCommand: (invocation, timeout) => runCliJsonWithTimeouts(invocation, { evalTimeoutMs: timeout }),
284
+ });
285
+ broker.invalidateSession(options.sessionKey, options.reason);
286
+ return;
287
+ }
288
+ const paths = await ensureJudgeBrokerDaemon(options.stateDir, options.repoRoot, options.sessionConfig);
289
+ await rpcInvalidate(paths.socketPath, options.sessionKey, options.reason, options.sessionConfig.connectTimeoutMs);
290
+ }
291
+ export { JudgeSessionBroker, stopRepoJudgeSessionBroker };
@@ -102,6 +102,7 @@ export function resolveJudgeFromCatalog(input) {
102
102
  const model = input.model?.trim() || spec.defaultModel;
103
103
  const endpoint = input.endpoint !== undefined ? input.endpoint?.trim() || null : spec.defaultEndpoint;
104
104
  return {
105
+ mode: 'shadow',
105
106
  provider: spec.driver,
106
107
  providerId: spec.id,
107
108
  model,
@@ -0,0 +1,6 @@
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 resolveJudgeSessionFingerprint(providerId: Exclude<JudgeProviderId, 'ollama'>, timeoutMs?: number): Promise<string>;
6
+ export declare function resetCliFingerprintCache(): void;
@@ -0,0 +1,67 @@
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 async function resolveJudgeSessionFingerprint(providerId, timeoutMs = 3_000) {
60
+ if (providerId === 'cursor' && !fingerprintResolver) {
61
+ return 'cursor-acp-v1';
62
+ }
63
+ return resolveCliVersionFingerprint(providerId, timeoutMs);
64
+ }
65
+ export function resetCliFingerprintCache() {
66
+ fingerprintCache.clear();
67
+ }
@@ -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;
@@ -1,11 +1,31 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { buildTier1Prompt, parseTier1Json, prescanInterpreterCode, } from './judge.js';
3
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
+ }
4
23
  const CLI_BINARIES = {
5
24
  codex: 'codex',
6
25
  cursor: 'cursor-agent',
7
26
  claude: 'claude',
8
27
  };
28
+ const FORCE_KILL_GRACE_MS = 250;
9
29
  function failClosedVerdict(reason) {
10
30
  return {
11
31
  local_recoverable: false,
@@ -162,17 +182,67 @@ export function parseCliJudgeOutput(providerId, raw) {
162
182
  return parseCliJsonLines(raw);
163
183
  }
164
184
  async function runCliJson(invocation, timeoutMs) {
185
+ return runCliJsonWithTimeouts(invocation, { evalTimeoutMs: timeoutMs });
186
+ }
187
+ export async function runCliJsonWithTimeouts(invocation, budgets) {
165
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
+ }
166
193
  return new Promise((resolve, reject) => {
167
194
  const child = spawn(binary, args, {
168
195
  stdio: ['pipe', 'pipe', 'pipe'],
169
196
  });
197
+ const command = [binary, ...args].join(' ');
170
198
  let stdout = '';
171
199
  let stderr = '';
172
- const timer = setTimeout(() => {
173
- child.kill('SIGTERM');
174
- reject(new Error(`${binary} timed out`));
175
- }, timeoutMs);
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);
176
246
  child.stdout.on('data', (chunk) => {
177
247
  stdout += String(chunk);
178
248
  });
@@ -184,16 +254,23 @@ async function runCliJson(invocation, timeoutMs) {
184
254
  }
185
255
  child.stdin.end();
186
256
  child.on('error', (error) => {
187
- clearTimeout(timer);
188
- reject(error);
257
+ rejectOnce(new CliRunError('spawn_error', error instanceof Error ? error.message : String(error), {
258
+ command,
259
+ }));
189
260
  });
190
261
  child.on('close', (code) => {
191
- clearTimeout(timer);
262
+ if (settled)
263
+ return;
192
264
  if (code === 0 && stdout.trim()) {
193
- resolve(stdout.trim());
265
+ resolveOnce(stdout.trim());
194
266
  return;
195
267
  }
196
- reject(new Error(stderr.trim() || `${binary} exited with code ${code ?? 'unknown'}`));
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
+ }));
197
274
  });
198
275
  });
199
276
  }
@@ -202,6 +279,10 @@ function createCliJudge(options) {
202
279
  const timeoutMs = options.timeoutMs ?? 25000;
203
280
  const providerId = options.providerId;
204
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';
205
286
  const judge = {
206
287
  lastTrace: initialTrace(options),
207
288
  async evaluate(input) {
@@ -233,27 +314,68 @@ function createCliJudge(options) {
233
314
  }
234
315
  const prompt = buildTier1Prompt(scrubbed.text);
235
316
  try {
236
- const raw = await runCommand(buildCliInvocation(providerId, prompt, options.modelResolved), timeoutMs);
237
- const parsed = parseCliJudgeOutput(providerId, raw);
238
- if (!parsed) {
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 === 'acp'
330
+ ? 'cursor-acp'
331
+ : transportResult.transport === 'session'
332
+ ? `${providerId}-cli-session`
333
+ : transport;
334
+ if (!transportResult.verdict) {
335
+ const fallbackReason = transportResult.fallbackReason ??
336
+ `${providerId}_cli_parse_error`;
239
337
  judge.lastTrace = {
240
338
  provider: 'fallback',
241
339
  modelRequested: options.modelRequested,
242
340
  modelResolved: options.modelResolved,
243
341
  latencyMs: Date.now() - started,
244
- fallbackReason: `${providerId}_cli_parse_error`,
342
+ fallbackReason,
343
+ judgeFallbackReason: fallbackReason,
344
+ judgeSessionUsed: transportResult.sessionUsed,
345
+ judgeSessionReused: transportResult.sessionReused,
346
+ judgeSessionRefHash: transportResult.sessionRefHash,
347
+ judgeSessionResetReason: transportResult.sessionResetReason,
348
+ judgeConnectMs: transportResult.connectMs,
349
+ judgeEvalMs: transportResult.evalMs,
350
+ judgeParseMs: transportResult.parseMs,
351
+ judgeShadowCompared: transportResult.shadowCompared,
352
+ judgeShadowMismatch: transportResult.shadowMismatch,
353
+ judgeShadowMismatchRateWindow: transportResult.shadowMismatchRateWindow,
354
+ judgeKillSwitchTriggered: transportResult.killSwitchTriggered,
245
355
  };
246
- return failClosedVerdict(`${providerId}_cli_parse_error`);
356
+ return failClosedVerdict(transportFallbackToFailClosedReason(providerId, fallbackReason));
247
357
  }
248
358
  judge.lastTrace = {
249
359
  provider: traceProvider(options.providerId),
250
360
  modelRequested: options.modelRequested,
251
361
  modelResolved: options.modelResolved,
252
362
  latencyMs: Date.now() - started,
253
- transport,
363
+ transport: traceTransport,
254
364
  outboundRedacted: true,
365
+ judgeSessionUsed: transportResult.sessionUsed,
366
+ judgeSessionReused: transportResult.sessionReused,
367
+ judgeSessionRefHash: transportResult.sessionRefHash,
368
+ judgeSessionResetReason: transportResult.sessionResetReason,
369
+ judgeFallbackReason: transportResult.fallbackReason,
370
+ judgeConnectMs: transportResult.connectMs,
371
+ judgeEvalMs: transportResult.evalMs,
372
+ judgeParseMs: transportResult.parseMs,
373
+ judgeShadowCompared: transportResult.shadowCompared,
374
+ judgeShadowMismatch: transportResult.shadowMismatch,
375
+ judgeShadowMismatchRateWindow: transportResult.shadowMismatchRateWindow,
376
+ judgeKillSwitchTriggered: transportResult.killSwitchTriggered,
255
377
  };
256
- return parsed;
378
+ return transportResult.verdict;
257
379
  }
258
380
  catch {
259
381
  judge.lastTrace = {
@@ -262,6 +384,7 @@ function createCliJudge(options) {
262
384
  modelResolved: options.modelResolved,
263
385
  latencyMs: Date.now() - started,
264
386
  fallbackReason: `${providerId}_cli_unavailable`,
387
+ judgeFallbackReason: `${providerId}_cli_unavailable`,
265
388
  };
266
389
  return failClosedVerdict(`${providerId}_cli_unavailable`);
267
390
  }