@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,258 @@
1
+ import { hashJudgeSessionRef } from './judge-audit.js';
2
+ import { recordJudgeLatency } from './judge-baseline.js';
3
+ import { evaluateViaJudgeBroker, invalidateJudgeSession } from './judge-broker-service.js';
4
+ import { buildCliInvocation, CliRunError, parseCliJudgeOutput, runCliJsonWithTimeouts, } from './judge-cli.js';
5
+ import { resolveCliVersionFingerprint } from './judge-cli-fingerprint.js';
6
+ import { assertReadOnlyInvocationArgs, providerSupportsSession } from './judge-provider-matrix.js';
7
+ import { resolveSessionEvalTimeoutMs } from './judge-runtime-config.js';
8
+ import { buildJudgeSessionKey, exceedsPromptBudget, guardFailClosedFallbackReason, } from './judge-session-guard.js';
9
+ import { isJudgeSessionKillSwitchActive, recordShadowComparison, shouldRunShadowComparison, triggerJudgeSessionKillSwitch, verdictsEquivalent, } from './judge-shadow.js';
10
+ function isEvalTimeoutError(error) {
11
+ const message = error instanceof Error ? error.message : String(error);
12
+ return /timed out/i.test(message);
13
+ }
14
+ function classifyParseFailure(providerId, raw) {
15
+ const trimmed = raw.trim();
16
+ if (!trimmed) {
17
+ return 'non_json_response';
18
+ }
19
+ if (parseCliJudgeOutput(providerId, trimmed)) {
20
+ return 'parse_error';
21
+ }
22
+ try {
23
+ JSON.parse(trimmed);
24
+ return 'parse_error';
25
+ }
26
+ catch {
27
+ return 'non_json_response';
28
+ }
29
+ }
30
+ function spawnFallbackReason(providerId, error) {
31
+ if (error instanceof CliRunError) {
32
+ if (error.kind === 'timeout') {
33
+ return 'eval_timeout';
34
+ }
35
+ if (error.kind === 'exit_nonzero') {
36
+ return `${providerId}_cli_nonzero`;
37
+ }
38
+ if (error.kind === 'spawn_error') {
39
+ return `${providerId}_cli_spawn_error`;
40
+ }
41
+ }
42
+ if (isEvalTimeoutError(error)) {
43
+ return 'eval_timeout';
44
+ }
45
+ return `${providerId}_cli_unavailable`;
46
+ }
47
+ function parseWithBudget(providerId, raw) {
48
+ const started = Date.now();
49
+ const verdict = parseCliJudgeOutput(providerId, raw);
50
+ const parseMs = Date.now() - started;
51
+ recordJudgeLatency('tier1_parse', parseMs);
52
+ return { verdict, parseMs };
53
+ }
54
+ async function runSpawnTransport(request, runCommand) {
55
+ const { context, prompt } = request;
56
+ const sessionConfig = context.runtime.session;
57
+ const invocation = buildCliInvocation(context.providerId, prompt, context.model);
58
+ const readOnly = assertReadOnlyInvocationArgs(context.providerId, invocation.args);
59
+ if (!readOnly.ok) {
60
+ return {
61
+ raw: '',
62
+ verdict: null,
63
+ transport: 'spawn',
64
+ sessionUsed: false,
65
+ sessionReused: false,
66
+ fallbackReason: 'unsafe_option_rejected',
67
+ connectMs: 0,
68
+ evalMs: 0,
69
+ parseMs: 0,
70
+ };
71
+ }
72
+ const evalTimeoutMs = resolveSessionEvalTimeoutMs(sessionConfig, context.judgeTimeoutMs);
73
+ let raw = '';
74
+ let evalMs = 0;
75
+ const evalStarted = Date.now();
76
+ try {
77
+ raw = await runCommand(invocation, evalTimeoutMs);
78
+ evalMs = Date.now() - evalStarted;
79
+ recordJudgeLatency('tier1_spawn', evalMs);
80
+ }
81
+ catch (error) {
82
+ evalMs = Date.now() - evalStarted;
83
+ return {
84
+ raw: '',
85
+ verdict: null,
86
+ transport: 'spawn',
87
+ sessionUsed: false,
88
+ sessionReused: false,
89
+ fallbackReason: spawnFallbackReason(context.providerId, error),
90
+ connectMs: 0,
91
+ evalMs,
92
+ parseMs: 0,
93
+ };
94
+ }
95
+ const parsed = parseWithBudget(context.providerId, raw);
96
+ return {
97
+ raw,
98
+ verdict: parsed.verdict,
99
+ transport: 'spawn',
100
+ sessionUsed: false,
101
+ sessionReused: false,
102
+ fallbackReason: parsed.verdict ? undefined : classifyParseFailure(context.providerId, raw),
103
+ connectMs: 0,
104
+ evalMs,
105
+ parseMs: parsed.parseMs,
106
+ };
107
+ }
108
+ async function runSessionTransport(request, runCommand) {
109
+ const { context, prompt } = request;
110
+ const sessionConfig = context.runtime.session;
111
+ const connectStarted = Date.now();
112
+ if (!sessionConfig.enabled ||
113
+ !providerSupportsSession(context.providerId, sessionConfig.providerAllowlist) ||
114
+ (await isJudgeSessionKillSwitchActive(context.repoRoot, context.stateDir))) {
115
+ return runSpawnTransport(request, runCommand);
116
+ }
117
+ let cliVersion = 'unknown';
118
+ try {
119
+ cliVersion = await resolveCliVersionFingerprint(context.providerId, sessionConfig.connectTimeoutMs);
120
+ }
121
+ catch {
122
+ return {
123
+ ...(await runSpawnTransport(request, runCommand)),
124
+ fallbackReason: 'connect_timeout',
125
+ sessionUsed: false,
126
+ sessionReused: false,
127
+ };
128
+ }
129
+ const connectMs = Date.now() - connectStarted;
130
+ recordJudgeLatency('tier1_connect', connectMs);
131
+ const promptBytes = Buffer.byteLength(prompt, 'utf8');
132
+ if (exceedsPromptBudget(promptBytes, sessionConfig)) {
133
+ const spawnResult = await runSpawnTransport(request, runCommand);
134
+ return {
135
+ ...spawnResult,
136
+ sessionUsed: false,
137
+ sessionReused: false,
138
+ sessionResetReason: 'max_prompt_bytes_exceeded',
139
+ fallbackReason: 'guard_reset',
140
+ };
141
+ }
142
+ const keyParts = {
143
+ providerId: context.providerId,
144
+ model: context.model,
145
+ repoRoot: context.repoRoot,
146
+ judgeMode: context.judgeMode,
147
+ cliVersion,
148
+ };
149
+ const sessionKey = buildJudgeSessionKey(keyParts);
150
+ const sessionRefHash = hashJudgeSessionRef(sessionKey);
151
+ const invocation = buildCliInvocation(context.providerId, prompt, context.model);
152
+ const readOnly = assertReadOnlyInvocationArgs(context.providerId, invocation.args);
153
+ if (!readOnly.ok) {
154
+ return runSpawnTransport(request, runCommand);
155
+ }
156
+ const evalTimeoutMs = resolveSessionEvalTimeoutMs(sessionConfig, context.judgeTimeoutMs);
157
+ const evalStarted = Date.now();
158
+ let brokerResult;
159
+ try {
160
+ brokerResult = await evaluateViaJudgeBroker({
161
+ stateDir: context.stateDir,
162
+ repoRoot: context.repoRoot,
163
+ sessionConfig,
164
+ request: {
165
+ keyParts,
166
+ invocation,
167
+ promptBytes,
168
+ },
169
+ timeoutMs: evalTimeoutMs,
170
+ runCommand,
171
+ });
172
+ }
173
+ catch (error) {
174
+ const spawnResult = await runSpawnTransport(request, runCommand);
175
+ return {
176
+ ...spawnResult,
177
+ transport: 'session',
178
+ sessionUsed: true,
179
+ sessionReused: false,
180
+ fallbackReason: spawnFallbackReason(context.providerId, error),
181
+ sessionResetReason: 'cli_error',
182
+ sessionRefHash,
183
+ };
184
+ }
185
+ const evalMs = Date.now() - evalStarted;
186
+ recordJudgeLatency('tier1_session', evalMs);
187
+ recordJudgeLatency('tier1_eval', evalMs);
188
+ const parsed = parseWithBudget(context.providerId, brokerResult.raw);
189
+ if (!parsed.verdict) {
190
+ const parseReason = classifyParseFailure(context.providerId, brokerResult.raw);
191
+ await invalidateJudgeSession({
192
+ stateDir: context.stateDir,
193
+ repoRoot: context.repoRoot,
194
+ sessionConfig,
195
+ sessionKey,
196
+ reason: parseReason === 'non_json_response' ? 'non_json_response' : 'parse_failure',
197
+ });
198
+ const spawnResult = await runSpawnTransport(request, runCommand);
199
+ return {
200
+ ...spawnResult,
201
+ transport: 'session',
202
+ sessionUsed: true,
203
+ sessionReused: brokerResult.reused,
204
+ fallbackReason: guardFailClosedFallbackReason(parseReason === 'non_json_response' ? 'non_json_response' : 'parse_failure'),
205
+ sessionResetReason: parseReason === 'non_json_response' ? 'non_json_response' : 'parse_failure',
206
+ sessionRefHash,
207
+ };
208
+ }
209
+ return {
210
+ raw: brokerResult.raw,
211
+ verdict: parsed.verdict,
212
+ transport: 'session',
213
+ sessionUsed: true,
214
+ sessionReused: brokerResult.reused,
215
+ sessionResetReason: brokerResult.resetReason,
216
+ sessionRefHash,
217
+ connectMs,
218
+ evalMs,
219
+ parseMs: parsed.parseMs,
220
+ };
221
+ }
222
+ export async function evaluateWithJudgeTransport(request, options = {}) {
223
+ const runCommand = options.runCommand ??
224
+ ((invocation, timeoutMs) => runCliJsonWithTimeouts(invocation, { evalTimeoutMs: timeoutMs }));
225
+ const { context } = request;
226
+ const useSession = context.runtime.session.enabled &&
227
+ providerSupportsSession(context.providerId, context.runtime.session.providerAllowlist) &&
228
+ !(await isJudgeSessionKillSwitchActive(context.repoRoot, context.stateDir));
229
+ const primary = useSession
230
+ ? await runSessionTransport(request, runCommand)
231
+ : await runSpawnTransport(request, runCommand);
232
+ if (!shouldRunShadowComparison(context.repoRoot, context.providerId, context.runtime.shadow)) {
233
+ return primary;
234
+ }
235
+ const shadowSpawn = primary.transport === 'session' ? await runSpawnTransport(request, runCommand) : primary;
236
+ if (!primary.verdict || !shadowSpawn.verdict) {
237
+ return primary;
238
+ }
239
+ const shadow = recordShadowComparison(context.repoRoot, context.runtime.shadow, !verdictsEquivalent(primary.verdict, shadowSpawn.verdict));
240
+ if (shadow.killSwitchTriggered) {
241
+ await triggerJudgeSessionKillSwitch(context.repoRoot, context.stateDir);
242
+ }
243
+ return {
244
+ ...primary,
245
+ shadowCompared: shadow.compared,
246
+ shadowMismatch: shadow.mismatch,
247
+ shadowMismatchRateWindow: shadow.mismatchRateWindow,
248
+ killSwitchTriggered: shadow.killSwitchTriggered,
249
+ verdict: shadow.mismatch ? shadowSpawn.verdict : primary.verdict,
250
+ fallbackReason: shadow.mismatch ? 'shadow_forced_spawn' : primary.fallbackReason,
251
+ transport: shadow.mismatch ? 'spawn' : primary.transport,
252
+ sessionUsed: shadow.mismatch ? false : primary.sessionUsed,
253
+ sessionReused: shadow.mismatch ? false : primary.sessionReused,
254
+ };
255
+ }
256
+ export function buildTransportInvocation(providerId, prompt, model) {
257
+ return buildCliInvocation(providerId, prompt, model);
258
+ }
@@ -1,6 +1,6 @@
1
1
  import type { ScrubOptions } from '../types.js';
2
2
  import type { Tier1Judge, Tier1Verdict } from './types.js';
3
- export type Tier1JudgeTransport = 'http' | 'ollama-http' | 'codex-cli' | 'cursor-cli' | 'claude-cli' | 'unavailable';
3
+ export type Tier1JudgeTransport = 'http' | 'ollama-http' | 'codex-cli' | 'cursor-cli' | 'claude-cli' | 'codex-cli-session' | 'cursor-cli-session' | 'claude-cli-session' | 'unavailable';
4
4
  export interface Tier1JudgeTrace {
5
5
  provider: 'openai-compatible' | 'ollama' | 'anthropic' | 'fallback';
6
6
  modelRequested: string;
@@ -9,6 +9,18 @@ export interface Tier1JudgeTrace {
9
9
  transport?: Tier1JudgeTransport;
10
10
  outboundRedacted?: boolean;
11
11
  fallbackReason?: string;
12
+ judgeSessionUsed?: boolean;
13
+ judgeSessionReused?: boolean;
14
+ judgeSessionRefHash?: string;
15
+ judgeSessionResetReason?: string;
16
+ judgeFallbackReason?: string;
17
+ judgeConnectMs?: number;
18
+ judgeEvalMs?: number;
19
+ judgeParseMs?: number;
20
+ judgeShadowCompared?: boolean;
21
+ judgeShadowMismatch?: boolean;
22
+ judgeShadowMismatchRateWindow?: number;
23
+ judgeKillSwitchTriggered?: boolean;
12
24
  }
13
25
  export interface TracedTier1Judge extends Tier1Judge {
14
26
  lastTrace?: Tier1JudgeTrace;
@@ -21,6 +33,7 @@ export interface MutationPrescanParams {
21
33
  cwd: string;
22
34
  repoRoot: string;
23
35
  trustedCwd: boolean;
36
+ trustedWorkspaceRoots?: string[];
24
37
  sensitivePaths: string[];
25
38
  }
26
39
  /** ADR-002 M3: structural prescan for sensitive / persistent mutation targets (shell redirects, etc.). */
@@ -1,5 +1,5 @@
1
1
  import { matchesSensitivePath } from '../glob.js';
2
- import { relativeWithinRepo, resolveMutationTarget } from '../path-utils.js';
2
+ import { resolveMutationTarget, resolveWorkspaceRootMatch } from '../path-utils.js';
3
3
  import { resolveTrustedPath } from './containment.js';
4
4
  import { scrubOutboundForJudge } from './judge-outbound.js';
5
5
  import { isOutsideRepoSecretCredentialPath, isPersistentAgentPath } from './persistent-paths.js';
@@ -85,9 +85,9 @@ export function prescanMutationTargets(params) {
85
85
  if (!resolved) {
86
86
  continue;
87
87
  }
88
- const relative = relativeWithinRepo(params.repoRoot, resolved);
89
- if (relative !== null &&
90
- matchesSensitivePath(relative.replaceAll('\\', '/'), params.sensitivePaths)) {
88
+ const workspaceMatch = resolveWorkspaceRootMatch(params.repoRoot, params.trustedWorkspaceRoots, resolved);
89
+ if (workspaceMatch !== null &&
90
+ matchesSensitivePath(workspaceMatch.relativePath.replaceAll('\\', '/'), params.sensitivePaths)) {
91
91
  return {
92
92
  local_recoverable: false,
93
93
  destroys_outside_repo: false,
@@ -95,7 +95,8 @@ export function prescanMutationTargets(params) {
95
95
  reason: 'sensitive_path_mutation',
96
96
  };
97
97
  }
98
- if (relative === null && isOutsideRepoSecretCredentialPath(resolved)) {
98
+ if ((workspaceMatch === null || workspaceMatch.kind === 'trusted') &&
99
+ isOutsideRepoSecretCredentialPath(resolved)) {
99
100
  return {
100
101
  local_recoverable: false,
101
102
  destroys_outside_repo: false,
@@ -160,7 +160,12 @@ export function splitTopLevelSegments(command) {
160
160
  }
161
161
  };
162
162
  for (const token of tokens) {
163
- if (token === '&&' || token === '||' || token === ';' || token === '|' || token === '&') {
163
+ if (token === '&&' ||
164
+ token === '||' ||
165
+ token === ';' ||
166
+ token === '|' ||
167
+ token === '&' ||
168
+ token === '|&') {
164
169
  flush();
165
170
  continue;
166
171
  }
@@ -43,12 +43,25 @@ export interface JudgeTrace {
43
43
  latencyMs: number;
44
44
  outboundRedacted?: boolean;
45
45
  fallbackReason?: string;
46
+ judgeSessionUsed?: boolean;
47
+ judgeSessionReused?: boolean;
48
+ judgeSessionRefHash?: string;
49
+ judgeSessionResetReason?: string;
50
+ judgeFallbackReason?: string;
51
+ judgeConnectMs?: number;
52
+ judgeEvalMs?: number;
53
+ judgeParseMs?: number;
54
+ judgeShadowCompared?: boolean;
55
+ judgeShadowMismatch?: boolean;
56
+ judgeShadowMismatchRateWindow?: number;
57
+ judgeKillSwitchTriggered?: boolean;
46
58
  }
47
59
  export type VerdictMode = 'enforce' | 'audit';
48
60
  export interface VerdictContext {
49
61
  cwd: string;
50
62
  repoRoot: string;
51
63
  trustedCwd: boolean;
64
+ trustedWorkspaceRoots?: string[];
52
65
  sensitivePaths: string[];
53
66
  protectedArtifactRoots?: string[];
54
67
  customAllowCommands?: string[];
@@ -1,5 +1,5 @@
1
1
  import { resolveMutationTarget } from '../path-utils.js';
2
- import { extractRedirectTargets, tokenizeShell } from '../shell-tokenizer.js';
2
+ import { extractRedirectTargets, isFdDuplication, isRedirectOperator, tokenizeShell, } from '../shell-tokenizer.js';
3
3
  import { analyzePathTargets, cwdRelative, isDestructiveHighStakesMutation, resolveTrustedPath, touchesProtectedRoot, } from './containment.js';
4
4
  import { classifyEgressTool } from './egress-classify.js';
5
5
  import { verdictFingerprint } from './fingerprint.js';
@@ -196,7 +196,7 @@ function extractPathArgs(tokens) {
196
196
  const args = [...redirects];
197
197
  for (let index = 1; index < tokens.length; index += 1) {
198
198
  const token = tokens[index];
199
- if (!token || token.startsWith('-') || token === '>' || token === '>>' || token === '<') {
199
+ if (!token || token.startsWith('-') || isRedirectOperator(token) || isFdDuplication(token)) {
200
200
  continue;
201
201
  }
202
202
  if (redirects.includes(token)) {
@@ -304,6 +304,7 @@ function tier0HighStakesRm(tokens, context) {
304
304
  cwd: context.cwd,
305
305
  repoRoot: context.repoRoot,
306
306
  trustedCwd: context.trustedCwd,
307
+ trustedWorkspaceRoots: context.trustedWorkspaceRoots,
307
308
  sensitivePaths: context.sensitivePaths,
308
309
  protectedArtifactRoots: context.protectedArtifactRoots,
309
310
  });
@@ -336,7 +337,7 @@ function tier0HighStakesRm(tokens, context) {
336
337
  const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ??
337
338
  resolveMutationTarget(target, context.cwd);
338
339
  if (resolved &&
339
- isDestructiveHighStakesMutation(head, resolved, context.repoRoot, context.sensitivePaths, context.protectedArtifactRoots)) {
340
+ isDestructiveHighStakesMutation(head, resolved, context.repoRoot, context.sensitivePaths, context.protectedArtifactRoots, context.trustedWorkspaceRoots)) {
340
341
  destructiveHighStakes = true;
341
342
  break;
342
343
  }
@@ -563,6 +564,7 @@ async function evaluateSegment(command, context, depth) {
563
564
  cwd: context.cwd,
564
565
  repoRoot: context.repoRoot,
565
566
  trustedCwd: context.trustedCwd,
567
+ trustedWorkspaceRoots: context.trustedWorkspaceRoots,
566
568
  sensitivePaths: context.sensitivePaths,
567
569
  protectedArtifactRoots: context.protectedArtifactRoots,
568
570
  });
@@ -612,7 +614,7 @@ async function evaluateSegment(command, context, depth) {
612
614
  const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ??
613
615
  resolveMutationTarget(target, context.cwd);
614
616
  if (resolved &&
615
- isDestructiveHighStakesMutation(segment.head, resolved, context.repoRoot, context.sensitivePaths, context.protectedArtifactRoots)) {
617
+ isDestructiveHighStakesMutation(segment.head, resolved, context.repoRoot, context.sensitivePaths, context.protectedArtifactRoots, context.trustedWorkspaceRoots)) {
616
618
  destructiveHighStakes = true;
617
619
  break;
618
620
  }
@@ -668,6 +670,7 @@ async function evaluateSegment(command, context, depth) {
668
670
  cwd: context.cwd,
669
671
  repoRoot: context.repoRoot,
670
672
  trustedCwd: context.trustedCwd,
673
+ trustedWorkspaceRoots: context.trustedWorkspaceRoots,
671
674
  sensitivePaths: context.sensitivePaths,
672
675
  })
673
676
  : null;
@@ -0,0 +1,86 @@
1
+ import { type MutatedCase } from './mutators.js';
2
+ declare const MUST_ASK_VERDICT: "deny_pending_approval";
3
+ export declare function probeCaseKey(entry: {
4
+ core: string;
5
+ mutatorId: string;
6
+ command: string;
7
+ }): string;
8
+ export interface ProbePassedCase {
9
+ core: string;
10
+ mutatorId: string;
11
+ command: string;
12
+ }
13
+ export interface AdversarialProbeFailure {
14
+ core: string;
15
+ mutatorId: string;
16
+ command: string;
17
+ expected: typeof MUST_ASK_VERDICT;
18
+ actual: string;
19
+ reason: string;
20
+ }
21
+ export interface AdversarialProbeFpFailure {
22
+ core: string;
23
+ mutatorId: string;
24
+ command: string;
25
+ actual: string;
26
+ reason: string;
27
+ }
28
+ export interface AdversarialProbeReport {
29
+ iteration: number;
30
+ generatedAt: string;
31
+ seed: number;
32
+ batchId: string;
33
+ sourceCommands: string[];
34
+ selectedMutators: string[];
35
+ maxCases?: number;
36
+ holdoutRatio: number;
37
+ totalCases: number;
38
+ fixSetSize: number;
39
+ holdoutSetSize: number;
40
+ fixSetFnRate: number;
41
+ firstPassFnRate: number;
42
+ /** null when no FP probe cases were evaluated. */
43
+ firstPassFpRate: number | null;
44
+ fpProbeSize: number;
45
+ /** null when holdout set is empty, or fix set FN rate is 0 but holdout FN > 0. */
46
+ holdoutFnRate: number | null;
47
+ /** holdoutFnRate / fixSetFnRate; 1.0 when both are 0. null when holdout empty. */
48
+ holdoutFixFnRateRatio: number | null;
49
+ passedCases: ProbePassedCase[];
50
+ failures: AdversarialProbeFailure[];
51
+ fpFailures: AdversarialProbeFpFailure[];
52
+ /** Populated by fix sessions / CI integration; empty when probe-only. */
53
+ filesChanged: string[];
54
+ }
55
+ export interface AdversarialProbeOptions {
56
+ seed?: number;
57
+ holdoutRatio?: number;
58
+ maxCases?: number;
59
+ outputDir?: string;
60
+ repoRoot?: string;
61
+ iteration?: number;
62
+ }
63
+ /** Mulberry32 PRNG for reproducible shuffles. */
64
+ export declare function createSeededRandom(seed: number): () => number;
65
+ export declare function shuffleWithSeed<T>(items: readonly T[], seed: number): T[];
66
+ export declare function splitFixHoldout<T>(items: readonly T[], seed: number, holdoutRatio?: number): {
67
+ fix: T[];
68
+ holdout: T[];
69
+ };
70
+ export declare function generateProbeCases(seed: number, maxCases?: number): MutatedCase[];
71
+ /** Benign must-allow cores × AUTO_LABEL mutators — FP over-stop probe (not for corpus auto-add). */
72
+ export declare function generateFpProbeCases(seed: number): MutatedCase[];
73
+ /** Ratio for overfitting detection; null when holdout unavailable or fix-perfect but holdout not. */
74
+ export declare function holdoutFixFnRateRatio(fixSetFnRate: number, holdoutFnRate: number | null): number | null;
75
+ export declare function runAdversarialProbe(options?: AdversarialProbeOptions): Promise<AdversarialProbeReport>;
76
+ export declare function defaultProbeOutputDir(): string;
77
+ export declare function formatProbeSummary(report: AdversarialProbeReport): string;
78
+ export declare function parseProbeCliArgs(argv: string[]): {
79
+ seed: number;
80
+ holdoutRatio: number;
81
+ maxCases?: number;
82
+ strict: boolean;
83
+ outputDir: string;
84
+ };
85
+ export declare function main(argv?: string[]): Promise<number>;
86
+ export {};