@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
@@ -1,124 +1,229 @@
1
+ import { repoLocalStateDirFor } from '../config-io.js';
1
2
  import { normalizeJudgeProvider, scrubOptionsFromConfig } from './config.js';
2
- import { resolveJudgeApiKey } from './judge-api-key.js';
3
- import { assertJudgeEndpoint } from './judge-config.js';
3
+ import { resolveJudgeCredential } from './judge-api-key.js';
4
+ import { hasValidCloudConsent } from './judge-config.js';
5
+ import { discoverJudgeModels, modelPresenceFromDiscovery, } from './judge-model-discovery.js';
6
+ import { detectJudgeRuntimeCapabilities, resolveJudgeTransport } from './judge-runtime-detection.js';
4
7
  import { createOllamaJudge, createOpenAiCompatibleJudge } from './verdict/judge.js';
5
- import { loadPinnedJudgeModels, resolveCloudModel } from './verdict/judge-factory.js';
6
- export async function diagnoseJudge(config) {
8
+ import { stopJudgeBrokerDaemon } from './verdict/judge-broker-service.js';
9
+ import { getJudgeProviderCapabilities, getJudgeProviderSpec, isRemovedProviderId, normalizeLegacyProviderId, } from './verdict/judge-catalog.js';
10
+ import { createJudgeFromConfig, resolveJudgeModel } from './verdict/judge-factory.js';
11
+ import { listRepoJudgeSessionBrokers, stopRepoJudgeSessionBroker, } from './verdict/judge-session-broker.js';
12
+ import { clearJudgeSessionKillSwitch } from './verdict/judge-session-kill-switch.js';
13
+ export async function diagnoseJudge(config, repoRoot = process.cwd(), options = {}) {
14
+ const shouldRunLiveProbe = options.liveProbe === true && !process.env.VITEST && !process.env.VITEST_WORKER_ID;
7
15
  const issues = [];
8
16
  const warnings = [];
9
17
  const notes = [];
10
18
  const judge = config.judge;
11
19
  const provider = normalizeJudgeProvider(judge.provider);
12
- notes.push(`Judge provider: ${provider}`);
20
+ const rawProviderId = judge.providerId ? String(judge.providerId) : undefined;
21
+ if (rawProviderId && isRemovedProviderId(rawProviderId)) {
22
+ notes.push(`Judge providerId: ${rawProviderId}`);
23
+ notes.push(`Judge driver: ${provider}`);
24
+ notes.push(`Judge model requested: ${judge.model}`);
25
+ issues.push(`judge.providerId "${rawProviderId}" was removed; run belay config set judge.providerId <ollama|codex|claude|cursor> to migrate.`);
26
+ return { issues, warnings, notes };
27
+ }
28
+ const providerId = judge.providerId && normalizeLegacyProviderId(judge.providerId)
29
+ ? normalizeLegacyProviderId(judge.providerId)
30
+ : provider === 'ollama'
31
+ ? 'ollama'
32
+ : provider === 'anthropic'
33
+ ? 'claude'
34
+ : 'codex';
35
+ const catalogSpec = getJudgeProviderSpec(providerId);
36
+ const capabilities = getJudgeProviderCapabilities(providerId);
37
+ const transport = resolveJudgeTransport(judge);
38
+ const runtime = detectJudgeRuntimeCapabilities(providerId);
39
+ notes.push(`Judge providerId: ${providerId}`);
40
+ notes.push(`Judge driver: ${provider}`);
13
41
  notes.push(`Judge model requested: ${judge.model}`);
42
+ notes.push(`Judge transport: ${transport}`);
14
43
  if (config.policy.modelAssist.enabled) {
15
44
  warnings.push('policy.modelAssist is enabled but is not wired to v2 Tier1. Use top-level judge instead.');
16
45
  }
17
- if (provider === 'openai-compatible') {
46
+ if (capabilities?.requiresConsent && !hasValidCloudConsent(judge) && transport === 'http') {
47
+ issues.push('Cloud judge consent is not recorded. Tier1 cloud judge will fail closed until consent is granted.');
48
+ }
49
+ else if (judge.cloudConsent?.accepted) {
50
+ notes.push(`Cloud consent: accepted ${judge.cloudConsent.at} by ${judge.cloudConsent.by}`);
51
+ }
52
+ if (providerId !== 'ollama') {
18
53
  warnings.push('Cloud judge egress is enabled. Commands are redacted (R23) before send, but path structure and intent may still leave the repo.');
19
- try {
20
- assertJudgeEndpoint(judge);
21
- notes.push(`OpenAI-compatible endpoint: ${judge.endpoint}`);
54
+ }
55
+ const repoLocalDir = repoLocalStateDirFor(repoRoot, config);
56
+ const keyInfo = await resolveJudgeCredential({
57
+ judge,
58
+ catalogSpec: catalogSpec ?? undefined,
59
+ repoRoot,
60
+ repoLocalStateDir: repoLocalDir,
61
+ config,
62
+ });
63
+ notes.push(`Credential: ${keyInfo.mode} (${keyInfo.sourceKind})`);
64
+ const resolved = resolveJudgeModel(judge);
65
+ notes.push(`Resolved model: ${resolved.resolved}`);
66
+ const endpoint = judge.endpoint ?? (providerId === 'ollama' ? 'http://127.0.0.1:11434' : null);
67
+ const discovery = await discoverJudgeModels({
68
+ providerId,
69
+ model: judge.model,
70
+ endpoint,
71
+ }, options.discoveryDeps);
72
+ const modelCheck = modelPresenceFromDiscovery(discovery, judge.model);
73
+ notes.push(`Model check: ${modelCheck.status} (source: ${modelCheck.source})`);
74
+ if (providerId === 'ollama') {
75
+ notes.push(`Ollama endpoint: ${endpoint}`);
76
+ if (discovery.modelIds.length === 0) {
77
+ issues.push(`Ollama endpoint unreachable or returned no models. Tier1 will fail closed.`);
22
78
  }
23
- catch {
24
- issues.push('openai-compatible judge requires judge.endpoint. No default cloud base URL is applied.');
25
- return { issues, warnings, notes };
79
+ else {
80
+ const hasModel = modelCheck.status === 'found';
81
+ if (!hasModel) {
82
+ issues.push(`Ollama model "${judge.model}" is not present. Pull it before enforce mode.`);
83
+ }
84
+ else {
85
+ notes.push(`Ollama model "${judge.model}" is available.`);
86
+ }
26
87
  }
27
- const keyInfo = resolveJudgeApiKey();
28
- if (!keyInfo.key) {
29
- issues.push('BELAY_JUDGE_API_KEY / OPENAI_API_KEY is not set. Tier1 cloud judge will fail closed to ask.');
88
+ const warm = createOllamaJudge({
89
+ model: judge.model,
90
+ baseUrl: endpoint ?? 'http://127.0.0.1:11434',
91
+ timeoutMs: Math.min(judge.timeoutMs, 5000),
92
+ fetchImpl: async () => new Response(JSON.stringify({
93
+ response: JSON.stringify({
94
+ local_recoverable: true,
95
+ destroys_outside_repo: false,
96
+ destroys_history_or_secrets: false,
97
+ reason: 'doctor_warm',
98
+ }),
99
+ }), { status: 200 }),
100
+ });
101
+ const warmResult = await warm.evaluate({
102
+ text: 'git status',
103
+ context: { cwd: process.cwd(), repoRoot: process.cwd() },
104
+ });
105
+ if (warmResult.reason === 'ollama_unavailable' || warmResult.reason === 'ollama_parse_error') {
106
+ issues.push(`Ollama warm call failed: ${warmResult.reason}`);
30
107
  }
31
108
  else {
32
- notes.push(`API key source: ${keyInfo.source}`);
109
+ notes.push('Ollama warm call succeeded.');
33
110
  }
34
- const pinnedModels = await loadPinnedJudgeModels();
35
- const resolved = resolveCloudModel(judge.model, pinnedModels['openai-compatible']);
36
- notes.push(`Resolved model: ${resolved.resolved}`);
37
- if (keyInfo.key && judge.endpoint?.trim()) {
38
- const traced = createOpenAiCompatibleJudge({
39
- endpoint: judge.endpoint.trim(),
40
- modelRequested: judge.model,
41
- modelResolved: resolved.resolved,
42
- timeoutMs: Math.min(judge.timeoutMs, 5000),
43
- apiKey: keyInfo.key,
44
- sensitivePaths: config.classifier.sensitivePaths,
45
- scrubOptions: scrubOptionsFromConfig(config),
46
- fetchImpl: async () => new Response(JSON.stringify({
47
- choices: [
48
- {
49
- message: {
50
- content: JSON.stringify({
51
- external_change: false,
52
- destroys_outside_repo: false,
53
- destroys_history_or_secrets: false,
54
- reason: 'doctor_dry_run',
55
- }),
56
- },
57
- },
58
- ],
59
- }), { status: 200 }),
60
- });
61
- const dryRun = await traced.evaluate({
62
- text: 'git status',
63
- context: { cwd: process.cwd(), repoRoot: process.cwd() },
64
- });
65
- if (dryRun.reason.startsWith('openai_compatible_') ||
66
- dryRun.reason === 'outbound_scrub_failed') {
67
- issues.push(`OpenAI-compatible judge dry-run failed: ${dryRun.reason}`);
68
- }
69
- else {
70
- notes.push('OpenAI-compatible judge dry-run succeeded.');
111
+ return { issues, warnings, notes, modelCheck };
112
+ }
113
+ if (transport === 'unavailable') {
114
+ issues.push('No judge transport is available (configure endpoint or install native CLI). Tier1 will fail closed to ask.');
115
+ return { issues, warnings, notes, modelCheck };
116
+ }
117
+ if (transport.endsWith('-cli')) {
118
+ if (!runtime.cliTransport) {
119
+ issues.push(`Native CLI transport (${transport}) is not available. Tier1 judge will fail closed to ask.`);
120
+ }
121
+ else if (!keyInfo.key && keyInfo.sourceKind !== 'host-session') {
122
+ issues.push('Judge API key is not set for the configured credential mode. Tier1 cloud judge will fail closed to ask.');
123
+ }
124
+ else {
125
+ notes.push(`Native CLI transport available: ${transport}`);
126
+ if (shouldRunLiveProbe) {
127
+ const smokeConfig = {
128
+ ...config,
129
+ judge: {
130
+ ...judge,
131
+ timeoutMs: Math.min(judge.timeoutMs, 5000),
132
+ },
133
+ };
134
+ const smokeJudge = createJudgeFromConfig(smokeConfig, { repoRoot });
135
+ const smokeResult = await smokeJudge.evaluate({
136
+ text: 'git status',
137
+ context: { cwd: repoRoot, repoRoot },
138
+ });
139
+ const smokeFallback = smokeJudge.lastTrace?.judgeFallbackReason ??
140
+ smokeJudge.lastTrace?.fallbackReason ??
141
+ smokeResult.reason;
142
+ if (smokeJudge.lastTrace?.provider === 'fallback') {
143
+ issues.push(`Native CLI transport smoke probe failed (${smokeFallback}). Tier1 will fail closed to ask.`);
144
+ }
145
+ else {
146
+ notes.push('Native CLI transport smoke probe succeeded.');
147
+ }
71
148
  }
72
149
  }
73
- return { issues, warnings, notes };
150
+ return { issues, warnings, notes, modelCheck };
151
+ }
152
+ if (judge.endpoint?.trim()) {
153
+ notes.push(`HTTP endpoint: ${judge.endpoint}`);
154
+ }
155
+ if (!keyInfo.key) {
156
+ issues.push('Judge API key is not set for the configured credential mode. Tier1 cloud judge will fail closed to ask.');
157
+ }
158
+ else {
159
+ notes.push(`Credential source: ${keyInfo.source ?? keyInfo.sourceKind}`);
74
160
  }
75
- const endpoint = judge.endpoint ?? 'http://127.0.0.1:11434';
76
- notes.push(`Ollama endpoint: ${endpoint}`);
77
- try {
78
- const response = await fetch(`${endpoint.replace(/\/$/, '')}/api/tags`, {
79
- signal: AbortSignal.timeout(3000),
161
+ if (keyInfo.key && judge.endpoint?.trim() && hasValidCloudConsent(judge)) {
162
+ const traced = createOpenAiCompatibleJudge({
163
+ endpoint: judge.endpoint.trim(),
164
+ modelRequested: judge.model,
165
+ modelResolved: resolved.resolved,
166
+ timeoutMs: Math.min(judge.timeoutMs, 5000),
167
+ apiKey: keyInfo.key,
168
+ sensitivePaths: config.classifier.sensitivePaths,
169
+ scrubOptions: scrubOptionsFromConfig(config),
170
+ fetchImpl: async () => new Response(JSON.stringify({
171
+ choices: [
172
+ {
173
+ message: {
174
+ content: JSON.stringify({
175
+ local_recoverable: true,
176
+ destroys_outside_repo: false,
177
+ destroys_history_or_secrets: false,
178
+ reason: 'doctor_dry_run',
179
+ }),
180
+ },
181
+ },
182
+ ],
183
+ }), { status: 200 }),
184
+ });
185
+ const dryRun = await traced.evaluate({
186
+ text: 'git status',
187
+ context: { cwd: process.cwd(), repoRoot: process.cwd() },
80
188
  });
81
- if (!response.ok) {
82
- issues.push(`Ollama endpoint unreachable (HTTP ${response.status}). Tier1 will fail closed.`);
189
+ if (dryRun.reason.startsWith('openai_compatible_') ||
190
+ dryRun.reason === 'outbound_scrub_failed') {
191
+ issues.push(`HTTP judge dry-run failed: ${dryRun.reason}`);
83
192
  }
84
193
  else {
85
- const tags = (await response.json());
86
- const names = (tags.models ?? []).map((entry) => entry.name ?? '');
87
- const hasModel = names.some((name) => name === judge.model || name.startsWith(`${judge.model}:`));
88
- if (!hasModel) {
89
- issues.push(`Ollama model "${judge.model}" is not present. Pull it before enforce mode.`);
90
- }
91
- else {
92
- notes.push(`Ollama model "${judge.model}" is available.`);
93
- }
194
+ notes.push('HTTP judge dry-run succeeded.');
94
195
  }
95
196
  }
96
- catch (error) {
97
- const detail = error instanceof Error ? error.message : 'connection failed';
98
- issues.push(`Ollama endpoint unreachable (${detail}). Tier1 will fail closed.`);
197
+ const factoryJudge = createJudgeFromConfig(config, { repoRoot });
198
+ if (factoryJudge.lastTrace?.transport) {
199
+ notes.push(`Factory transport: ${factoryJudge.lastTrace.transport}`);
99
200
  }
100
- const warm = createOllamaJudge({
101
- model: judge.model,
102
- baseUrl: endpoint,
103
- timeoutMs: Math.min(judge.timeoutMs, 5000),
104
- fetchImpl: async () => new Response(JSON.stringify({
105
- response: JSON.stringify({
106
- external_change: false,
107
- destroys_outside_repo: false,
108
- destroys_history_or_secrets: false,
109
- reason: 'doctor_warm',
110
- }),
111
- }), { status: 200 }),
112
- });
113
- const warmResult = await warm.evaluate({
114
- text: 'git status',
115
- context: { cwd: process.cwd(), repoRoot: process.cwd() },
116
- });
117
- if (warmResult.reason === 'ollama_unavailable' || warmResult.reason === 'ollama_parse_error') {
118
- issues.push(`Ollama warm call failed: ${warmResult.reason}`);
201
+ if (config.judge.runtime?.session.enabled) {
202
+ notes.push('Judge session transport: enabled');
203
+ const activeBrokers = listRepoJudgeSessionBrokers();
204
+ if (activeBrokers.includes(repoRoot)) {
205
+ notes.push(`Active session broker for repo: ${repoRoot}`);
206
+ }
119
207
  }
120
208
  else {
121
- notes.push('Ollama warm call succeeded.');
209
+ notes.push('Judge session transport: disabled (default)');
210
+ }
211
+ return { issues, warnings, notes, modelCheck };
212
+ }
213
+ export async function stopJudgeSessionBrokers(repoRoot, stateDir) {
214
+ let stopped = 0;
215
+ stopped += stopRepoJudgeSessionBroker(repoRoot, 'manual_stop');
216
+ stopped += await stopJudgeBrokerDaemon(stateDir);
217
+ await clearJudgeSessionKillSwitch(stateDir);
218
+ return stopped;
219
+ }
220
+ export function stopInProcessJudgeSessionBrokers(repoRoot) {
221
+ if (repoRoot) {
222
+ return stopRepoJudgeSessionBroker(repoRoot, 'manual_stop');
223
+ }
224
+ let stopped = 0;
225
+ for (const root of listRepoJudgeSessionBrokers()) {
226
+ stopped += stopRepoJudgeSessionBroker(root, 'manual_stop');
122
227
  }
123
- return { issues, warnings, notes };
228
+ return stopped;
124
229
  }
@@ -0,0 +1,24 @@
1
+ export interface DiscoverJudgeModelsInput {
2
+ providerId: string;
3
+ model: string;
4
+ endpoint: string | null;
5
+ }
6
+ export interface DiscoverJudgeModelsResult {
7
+ source: string;
8
+ modelIds: string[];
9
+ }
10
+ export interface CheckJudgeModelPresenceResult {
11
+ status: 'found' | 'missing' | 'unverified';
12
+ source: string;
13
+ }
14
+ export type JudgeModelDiscoveryRunCommand = (command: string, args: string[], timeoutMs: number) => Promise<string>;
15
+ export interface JudgeModelDiscoveryDeps {
16
+ runCommand?: JudgeModelDiscoveryRunCommand;
17
+ fetch?: typeof fetch;
18
+ allowCliDiscovery?: boolean;
19
+ }
20
+ export declare function parseLineModelIds(raw: string): string[];
21
+ export declare function parseJsonModelIds(raw: string): string[];
22
+ export declare function discoverJudgeModels(input: DiscoverJudgeModelsInput, deps?: JudgeModelDiscoveryDeps): Promise<DiscoverJudgeModelsResult>;
23
+ export declare function checkJudgeModelPresence(input: DiscoverJudgeModelsInput, deps?: JudgeModelDiscoveryDeps): Promise<CheckJudgeModelPresenceResult>;
24
+ export declare function modelPresenceFromDiscovery(discovery: DiscoverJudgeModelsResult, model: string): CheckJudgeModelPresenceResult;
@@ -0,0 +1,168 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { getJudgeProviderSpec, inferProviderIdFromConfig, normalizeLegacyProviderId, } from './verdict/judge-catalog.js';
3
+ const MODEL_DISCOVERY_SOURCES = {
4
+ ollama: 'ollama-tags',
5
+ codex: 'codex-cli',
6
+ claude: 'anthropic-models',
7
+ cursor: 'cursor-agent',
8
+ };
9
+ function providerIdFromInput(providerId) {
10
+ const normalized = normalizeLegacyProviderId(providerId);
11
+ if (normalized) {
12
+ return normalized;
13
+ }
14
+ return inferProviderIdFromConfig({ providerId: providerId });
15
+ }
16
+ function defaultAllowCliDiscovery() {
17
+ if (process.env.BELAY_JUDGE_DISABLE_CLI_TRANSPORT === '1') {
18
+ return false;
19
+ }
20
+ return !process.env.VITEST && !process.env.VITEST_WORKER_ID;
21
+ }
22
+ function resolveDeps(deps) {
23
+ return {
24
+ runCommand: deps?.runCommand ?? runCommandCapture,
25
+ fetchImpl: deps?.fetch ?? fetch,
26
+ allowCliDiscovery: deps?.allowCliDiscovery ?? defaultAllowCliDiscovery(),
27
+ };
28
+ }
29
+ async function runCommandCapture(command, args, timeoutMs) {
30
+ return new Promise((resolve, reject) => {
31
+ const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
32
+ let stdout = '';
33
+ let stderr = '';
34
+ const timer = setTimeout(() => {
35
+ child.kill('SIGTERM');
36
+ reject(new Error(`${command} timed out`));
37
+ }, timeoutMs);
38
+ child.stdout.on('data', (chunk) => {
39
+ stdout += String(chunk);
40
+ });
41
+ child.stderr.on('data', (chunk) => {
42
+ stderr += String(chunk);
43
+ });
44
+ child.on('error', (error) => {
45
+ clearTimeout(timer);
46
+ reject(error);
47
+ });
48
+ child.on('close', (code) => {
49
+ clearTimeout(timer);
50
+ if (code === 0 && stdout.trim()) {
51
+ resolve(stdout.trim());
52
+ return;
53
+ }
54
+ reject(new Error(stderr.trim() || `${command} exited with code ${code ?? 'unknown'}`));
55
+ });
56
+ });
57
+ }
58
+ export function parseLineModelIds(raw) {
59
+ return raw
60
+ .split(/\r?\n/)
61
+ .map((line) => line.trim())
62
+ .filter(Boolean);
63
+ }
64
+ export function parseJsonModelIds(raw) {
65
+ try {
66
+ const parsed = JSON.parse(raw);
67
+ const entries = Array.isArray(parsed) ? parsed : (parsed.models ?? []);
68
+ return entries
69
+ .map((entry) => entry.id?.trim() || entry.name?.trim() || entry.slug?.trim())
70
+ .filter((id) => Boolean(id));
71
+ }
72
+ catch {
73
+ return parseLineModelIds(raw);
74
+ }
75
+ }
76
+ async function discoverCursorModels(runCommand) {
77
+ const raw = await runCommand('cursor-agent', ['--list-models'], 2000);
78
+ const fromJson = parseJsonModelIds(raw);
79
+ return fromJson.length > 0 ? fromJson : parseLineModelIds(raw);
80
+ }
81
+ async function discoverCodexModels(runCommand) {
82
+ const raw = await runCommand('codex', ['debug', 'models'], 2000);
83
+ const parsed = parseJsonModelIds(raw);
84
+ if (parsed.length > 0) {
85
+ return parsed;
86
+ }
87
+ return parseLineModelIds(raw).filter((line) => line !== 'visibility=list');
88
+ }
89
+ async function discoverClaudeModels(endpoint, fetchImpl) {
90
+ const apiKey = process.env.ANTHROPIC_API_KEY?.trim() || process.env.BELAY_JUDGE_API_KEY?.trim();
91
+ if (!apiKey) {
92
+ return [];
93
+ }
94
+ const base = (endpoint ?? 'https://api.anthropic.com').replace(/\/$/, '');
95
+ const response = await fetchImpl(`${base}/v1/models`, {
96
+ headers: {
97
+ 'x-api-key': apiKey,
98
+ 'anthropic-version': '2023-06-01',
99
+ },
100
+ signal: AbortSignal.timeout(5000),
101
+ });
102
+ if (!response.ok) {
103
+ return [];
104
+ }
105
+ const payload = (await response.json());
106
+ return (payload.data ?? [])
107
+ .map((entry) => entry.id?.trim())
108
+ .filter((id) => Boolean(id));
109
+ }
110
+ export async function discoverJudgeModels(input, deps) {
111
+ const { runCommand, fetchImpl, allowCliDiscovery } = resolveDeps(deps);
112
+ const providerId = providerIdFromInput(input.providerId);
113
+ const source = MODEL_DISCOVERY_SOURCES[providerId];
114
+ if (providerId === 'ollama' && input.endpoint) {
115
+ try {
116
+ const response = await fetchImpl(`${input.endpoint.replace(/\/$/, '')}/api/tags`, {
117
+ signal: AbortSignal.timeout(3000),
118
+ });
119
+ if (!response.ok) {
120
+ return { source, modelIds: [] };
121
+ }
122
+ const payload = (await response.json());
123
+ const modelIds = (payload.models ?? [])
124
+ .map((entry) => entry.name?.trim())
125
+ .filter((name) => Boolean(name));
126
+ return { source, modelIds };
127
+ }
128
+ catch {
129
+ return { source, modelIds: [] };
130
+ }
131
+ }
132
+ try {
133
+ if (!allowCliDiscovery) {
134
+ return { source, modelIds: [] };
135
+ }
136
+ if (providerId === 'cursor') {
137
+ return { source, modelIds: await discoverCursorModels(runCommand) };
138
+ }
139
+ if (providerId === 'codex') {
140
+ return { source, modelIds: await discoverCodexModels(runCommand) };
141
+ }
142
+ if (providerId === 'claude') {
143
+ return { source, modelIds: await discoverClaudeModels(input.endpoint, fetchImpl) };
144
+ }
145
+ }
146
+ catch {
147
+ return { source, modelIds: [] };
148
+ }
149
+ if (getJudgeProviderSpec(providerId)) {
150
+ return { source, modelIds: [] };
151
+ }
152
+ return { source, modelIds: [] };
153
+ }
154
+ export async function checkJudgeModelPresence(input, deps) {
155
+ const discovery = await discoverJudgeModels(input, deps);
156
+ return modelPresenceFromDiscovery(discovery, input.model);
157
+ }
158
+ export function modelPresenceFromDiscovery(discovery, model) {
159
+ if (discovery.modelIds.length === 0) {
160
+ return { status: 'unverified', source: discovery.source };
161
+ }
162
+ const requested = model.trim();
163
+ const found = discovery.modelIds.some((id) => id === requested || id.startsWith(`${requested}:`));
164
+ return {
165
+ status: found ? 'found' : 'missing',
166
+ source: discovery.source,
167
+ };
168
+ }
@@ -0,0 +1,5 @@
1
+ export declare function isDeprecatedJudgeModelAuto(model: string | undefined): boolean;
2
+ export declare function rejectDeprecatedJudgeModelAuto(model: string | undefined): void;
3
+ export declare function warnDeprecatedJudgeModelAuto(): void;
4
+ /** @internal test helper */
5
+ export declare function resetDeprecatedJudgeModelAutoWarningForTests(): void;
@@ -0,0 +1,21 @@
1
+ const DEPRECATED_JUDGE_MODEL_AUTO = 'auto';
2
+ export function isDeprecatedJudgeModelAuto(model) {
3
+ return model?.trim().toLowerCase() === DEPRECATED_JUDGE_MODEL_AUTO;
4
+ }
5
+ export function rejectDeprecatedJudgeModelAuto(model) {
6
+ if (isDeprecatedJudgeModelAuto(model)) {
7
+ throw new Error('judge model "auto" is no longer accepted. Use a concrete model id from belay judge list or the provider catalog default.');
8
+ }
9
+ }
10
+ let warnedDeprecatedAuto = false;
11
+ export function warnDeprecatedJudgeModelAuto() {
12
+ if (warnedDeprecatedAuto) {
13
+ return;
14
+ }
15
+ warnedDeprecatedAuto = true;
16
+ process.stderr.write('Warning: judge.model "auto" is deprecated; normalized to the provider catalog default on load. Set a concrete model id with belay config set judge.model <id>.\n');
17
+ }
18
+ /** @internal test helper */
19
+ export function resetDeprecatedJudgeModelAutoWarningForTests() {
20
+ warnedDeprecatedAuto = false;
21
+ }
@@ -0,0 +1,9 @@
1
+ import type { BelayJudgeConfig } from './config.js';
2
+ import type { Tier1JudgeTransport } from './verdict/judge.js';
3
+ import { type JudgeProviderId } from './verdict/judge-catalog.js';
4
+ export interface JudgeRuntimeCapabilities {
5
+ http: boolean;
6
+ cliTransport: Tier1JudgeTransport | null;
7
+ }
8
+ export declare function detectJudgeRuntimeCapabilities(providerId: JudgeProviderId | string, env?: NodeJS.ProcessEnv): JudgeRuntimeCapabilities;
9
+ export declare function resolveJudgeTransport(judge: Pick<BelayJudgeConfig, 'providerId' | 'provider' | 'model' | 'endpoint'>, env?: NodeJS.ProcessEnv): Tier1JudgeTransport;
@@ -0,0 +1,68 @@
1
+ import { accessSync, constants } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { inferProviderIdFromConfig, normalizeLegacyProviderId, } from './verdict/judge-catalog.js';
4
+ const CLI_COMMANDS = {
5
+ codex: 'codex',
6
+ cursor: 'cursor-agent',
7
+ claude: 'claude',
8
+ };
9
+ function isVitestRuntime(env) {
10
+ return Boolean(env.VITEST || env.VITEST_WORKER_ID);
11
+ }
12
+ function cliTransportForProvider(providerId) {
13
+ if (providerId === 'ollama') {
14
+ return null;
15
+ }
16
+ return `${providerId}-cli`;
17
+ }
18
+ function commandOnPath(command, env) {
19
+ const pathValue = env.PATH ?? '';
20
+ for (const dir of pathValue.split(path.delimiter).filter(Boolean)) {
21
+ const candidate = path.join(dir, command);
22
+ try {
23
+ accessSync(candidate, constants.X_OK);
24
+ return true;
25
+ }
26
+ catch {
27
+ // try next PATH entry
28
+ }
29
+ }
30
+ return false;
31
+ }
32
+ function providerIdFromJudge(judge) {
33
+ if (judge.providerId && normalizeLegacyProviderId(judge.providerId)) {
34
+ return normalizeLegacyProviderId(judge.providerId);
35
+ }
36
+ return inferProviderIdFromConfig(judge);
37
+ }
38
+ export function detectJudgeRuntimeCapabilities(providerId, env = process.env) {
39
+ const normalized = normalizeLegacyProviderId(String(providerId));
40
+ if (!normalized || normalized === 'ollama') {
41
+ return { http: false, cliTransport: null };
42
+ }
43
+ if (env.BELAY_JUDGE_DISABLE_CLI_TRANSPORT === '1') {
44
+ return { http: true, cliTransport: null };
45
+ }
46
+ if (isVitestRuntime(env)) {
47
+ return { http: false, cliTransport: cliTransportForProvider(normalized) };
48
+ }
49
+ const command = CLI_COMMANDS[normalized];
50
+ if (commandOnPath(command, env)) {
51
+ return { http: false, cliTransport: cliTransportForProvider(normalized) };
52
+ }
53
+ return { http: true, cliTransport: null };
54
+ }
55
+ export function resolveJudgeTransport(judge, env = process.env) {
56
+ const providerId = providerIdFromJudge(judge);
57
+ if (providerId === 'ollama') {
58
+ return 'ollama-http';
59
+ }
60
+ if (judge.endpoint?.trim()) {
61
+ return 'http';
62
+ }
63
+ const caps = detectJudgeRuntimeCapabilities(providerId, env);
64
+ if (caps.cliTransport) {
65
+ return caps.cliTransport;
66
+ }
67
+ return 'unavailable';
68
+ }
@@ -7,6 +7,17 @@
7
7
  export declare function canonicalPath(targetPath: string): string;
8
8
  export declare function pathWithinRoot(root: string, targetPath: string): boolean;
9
9
  export declare function relativeWithinRepo(repoRoot: string, targetPath: string): string | null;
10
+ export type WorkspaceRootMatch = {
11
+ kind: 'repo';
12
+ root: string;
13
+ relativePath: string;
14
+ } | {
15
+ kind: 'trusted';
16
+ root: string;
17
+ relativePath: string;
18
+ };
19
+ export declare function resolveWorkspaceRootMatch(repoRoot: string, trustedRoots: string[] | undefined, targetPath: string): WorkspaceRootMatch | null;
10
20
  export declare function normalizeToken(token: string, repoRoot: string): string;
11
21
  export declare function resolveMutationTarget(token: string, cwd: string): string | null;
12
22
  export declare function hasOutsideRepoPath(tokens: string[], cwd: string, repoRoot: string): boolean;
23
+ export declare function containingGitRoot(targetPath: string): string | null;