@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
@@ -1,4 +1,5 @@
1
1
  import { allPathsAllowlisted } from './capability/allowlist.js';
2
+ import { checkGatedActionLimits } from './capability/limits.js';
2
3
  import { collectOutsideRepoPaths, collectOutsideRepoPathsFromToolPayload, } from './capability/paths.js';
3
4
  import { classifySubagent } from './classify-subagent.js';
4
5
  import { classifyToolUse } from './classify-tool.js';
@@ -167,28 +168,28 @@ function applyFsScopePeripheralPolicy(outsideRepoPaths, result, options) {
167
168
  return result;
168
169
  }
169
170
  function applySandboxOutsideBoundary(command, action, result, options) {
170
- const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot);
171
+ const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot, options.trustedWorkspaceRoots);
171
172
  return applySandboxFsScopeBoundary(outsideRepoPaths, result, options, {
172
173
  redirect: command.includes('>'),
173
174
  });
174
175
  }
175
176
  function applyShellPeripheralPolicy(command, action, result, options) {
176
- const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot);
177
+ const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot, options.trustedWorkspaceRoots);
177
178
  return applyFsScopePeripheralPolicy(outsideRepoPaths, result, options);
178
179
  }
179
- function outsideRepoPathsForToolAction(action) {
180
+ function outsideRepoPathsForToolAction(action, options) {
180
181
  const payload = action.payload ?? {};
181
- const paths = new Set(collectOutsideRepoPathsFromToolPayload(payload, action.cwd, action.repoRoot));
182
+ const paths = new Set(collectOutsideRepoPathsFromToolPayload(payload, action.cwd, action.repoRoot, options.trustedWorkspaceRoots));
182
183
  const command = shellCommandFromPayload(payload);
183
184
  if (command) {
184
- for (const resolved of collectOutsideRepoPaths(command, action.cwd, action.repoRoot)) {
185
+ for (const resolved of collectOutsideRepoPaths(command, action.cwd, action.repoRoot, options.trustedWorkspaceRoots)) {
185
186
  paths.add(resolved);
186
187
  }
187
188
  }
188
189
  return [...paths];
189
190
  }
190
191
  function applyToolSandboxPolicies(action, result, options) {
191
- const outsideRepoPaths = outsideRepoPathsForToolAction(action);
192
+ const outsideRepoPaths = outsideRepoPathsForToolAction(action, options);
192
193
  const command = shellCommandFromPayload(action.payload ?? {});
193
194
  let next = applySandboxFsScopeBoundary(outsideRepoPaths, result, options, {
194
195
  redirect: command.includes('>'),
@@ -197,6 +198,10 @@ function applyToolSandboxPolicies(action, result, options) {
197
198
  return next;
198
199
  }
199
200
  export async function classifyGatedAction(action, config, extraOptions = {}) {
201
+ const limitResult = checkGatedActionLimits(action);
202
+ if (limitResult) {
203
+ return limitResult;
204
+ }
200
205
  const options = { ...classifierOptionsFromConfig(config), ...extraOptions };
201
206
  if (action.kind === 'shell') {
202
207
  const command = action.command ?? shellCommandFromPayload(action.payload ?? {});
@@ -221,7 +226,7 @@ export async function classifyGatedAction(action, config, extraOptions = {}) {
221
226
  };
222
227
  }
223
228
  if (action.kind === 'subagent') {
224
- return classifySubagent(action.payload ?? {}, action.repoRoot, options);
229
+ return classifySubagent(action.payload ?? {}, action.repoRoot, options, config);
225
230
  }
226
231
  let result = await classifyToolUse(action.payload ?? {}, action.repoRoot, action.cwd, config, options);
227
232
  result = applyToolSandboxPolicies(action, result, options);
@@ -0,0 +1,53 @@
1
+ import type { CorpusCase } from '../corpus/types.js';
2
+ import type { AuditRecord } from './audit-types.js';
3
+ export declare const HARVEST_REPORT_SCHEMA_VERSION = 1;
4
+ export type HarvestCandidateSource = 'deny_then_approve' | 'repeated_ask' | 'read_style_signal' | 'overrides_allow';
5
+ export type HarvestReviewOutcome = 'provably-benign' | 'accepted-benign' | 'reject';
6
+ export interface HarvestCandidate {
7
+ kind: 'shell';
8
+ command: string;
9
+ fingerprint: string;
10
+ reason: string;
11
+ sources: HarvestCandidateSource[];
12
+ askCount: number;
13
+ approvedAfterDeny: boolean;
14
+ }
15
+ export type AvailabilitySignal = 'missing_trusted_cwd' | 'judge_timeout' | 'judge_fallback';
16
+ export interface AvailabilityQueueItem {
17
+ kind: 'shell';
18
+ command: string;
19
+ fingerprint: string;
20
+ reason: string;
21
+ availabilitySignal: AvailabilitySignal;
22
+ judgeFallbackReason?: string;
23
+ askCount: number;
24
+ }
25
+ export interface HarvestReport {
26
+ schemaVersion: typeof HARVEST_REPORT_SCHEMA_VERSION;
27
+ /** Initial harvest scope — shell audit traces only. */
28
+ scope: 'shell';
29
+ candidates: HarvestCandidate[];
30
+ availabilityQueue: AvailabilityQueueItem[];
31
+ }
32
+ export declare function extractAvailabilityQueue(records: AuditRecord[]): AvailabilityQueueItem[];
33
+ /** Shell gate rows plus approval events — matches what `belay harvest list` needs. */
34
+ export declare function filterRecordsForHarvest(records: AuditRecord[], options?: {
35
+ since?: string;
36
+ until?: string;
37
+ }): AuditRecord[];
38
+ export declare function extractHarvestCandidates(records: AuditRecord[], options?: {
39
+ allowPatterns?: string[];
40
+ }): HarvestCandidate[];
41
+ export declare function buildHarvestReport(records: AuditRecord[], options?: {
42
+ allowPatterns?: string[];
43
+ }): HarvestReport;
44
+ export declare function applyHarvestReview(cases: CorpusCase[], params: {
45
+ command: string;
46
+ outcome: HarvestReviewOutcome;
47
+ reason?: string;
48
+ }): {
49
+ cases: CorpusCase[];
50
+ applied: boolean;
51
+ ok: boolean;
52
+ message: string;
53
+ };
@@ -0,0 +1,276 @@
1
+ import { computeRepeatedFingerprintAsks, isAvailabilityCausedAsk } from './audit-analysis.js';
2
+ import { buildApprovalRoundTrips, filterAuditRecords, inferWouldBlock, isApprovalRecorded, isShellGateRecord, parseTimestamp, } from './audit-query.js';
3
+ import { matchesCustomCommand } from './custom-command-match.js';
4
+ export const HARVEST_REPORT_SCHEMA_VERSION = 1;
5
+ const READ_STYLE_COMMAND_PATTERN = /^\s*(git\s+status|git\s+diff|git\s+log|git\s+show|ls\b|cat\b|head\b|tail\b|find\b|rg\b|grep\b|fd\b|bat\b|less\b|more\b|pwd\b|whoami\b|env\b|printenv\b|npm\s+(test|run\s+test)|pnpm\s+(test|run\s+test)|yarn\s+test)/;
6
+ function shellRecords(records) {
7
+ return records.filter(isShellGateRecord);
8
+ }
9
+ function availabilitySignal(record) {
10
+ if (!isAvailabilityCausedAsk(record)) {
11
+ return null;
12
+ }
13
+ if (record.reason === 'missing_trusted_cwd') {
14
+ return 'missing_trusted_cwd';
15
+ }
16
+ const fallback = typeof record.judgeFallbackReason === 'string' ? record.judgeFallbackReason : '';
17
+ if (fallback.includes('timeout')) {
18
+ return 'judge_timeout';
19
+ }
20
+ return 'judge_fallback';
21
+ }
22
+ function commandFromRecord(record) {
23
+ return (record.summary ?? '').trim();
24
+ }
25
+ function hasReadStyleSignal(summary) {
26
+ return READ_STYLE_COMMAND_PATTERN.test(summary);
27
+ }
28
+ function upsertCandidate(map, params) {
29
+ const existing = map.get(params.fingerprint);
30
+ if (existing) {
31
+ if (!existing.sources.includes(params.source)) {
32
+ existing.sources.push(params.source);
33
+ }
34
+ existing.askCount = Math.max(existing.askCount, params.askCount ?? 1);
35
+ existing.approvedAfterDeny = existing.approvedAfterDeny || Boolean(params.approvedAfterDeny);
36
+ if (params.command) {
37
+ existing.command = params.command;
38
+ }
39
+ if (params.reason) {
40
+ existing.reason = params.reason;
41
+ }
42
+ return;
43
+ }
44
+ map.set(params.fingerprint, {
45
+ kind: 'shell',
46
+ command: params.command,
47
+ fingerprint: params.fingerprint,
48
+ reason: params.reason,
49
+ sources: [params.source],
50
+ askCount: params.askCount ?? 1,
51
+ approvedAfterDeny: Boolean(params.approvedAfterDeny),
52
+ });
53
+ }
54
+ export function extractAvailabilityQueue(records) {
55
+ const grouped = new Map();
56
+ for (const record of shellRecords(records)) {
57
+ if (!inferWouldBlock(record) || !record.fingerprint) {
58
+ continue;
59
+ }
60
+ const signal = availabilitySignal(record);
61
+ if (!signal) {
62
+ continue;
63
+ }
64
+ const fingerprint = record.fingerprint;
65
+ const existing = grouped.get(fingerprint);
66
+ if (existing) {
67
+ existing.askCount += 1;
68
+ existing.command = commandFromRecord(record) || existing.command;
69
+ existing.reason = record.reason ?? existing.reason;
70
+ continue;
71
+ }
72
+ grouped.set(fingerprint, {
73
+ kind: 'shell',
74
+ command: commandFromRecord(record),
75
+ fingerprint,
76
+ reason: record.reason ?? 'unknown',
77
+ availabilitySignal: signal,
78
+ ...(typeof record.judgeFallbackReason === 'string'
79
+ ? { judgeFallbackReason: record.judgeFallbackReason }
80
+ : {}),
81
+ askCount: 1,
82
+ });
83
+ }
84
+ return [...grouped.values()].sort((left, right) => right.askCount - left.askCount);
85
+ }
86
+ function recordsForShellRoundTrips(records) {
87
+ return records.filter((record) => isApprovalRecorded(record) ||
88
+ (isShellGateRecord(record) && inferWouldBlock(record) && !isAvailabilityCausedAsk(record)));
89
+ }
90
+ /** Pair deny/approval rows split by a time window so round-trip harvest still works. */
91
+ function augmentHarvestRoundTripPairs(scoped, timeFiltered) {
92
+ if (timeFiltered.length === 0) {
93
+ return timeFiltered;
94
+ }
95
+ const included = new Set(timeFiltered);
96
+ const augmented = [...timeFiltered];
97
+ const approvalById = new Map();
98
+ const denyByApprovalId = new Map();
99
+ for (const record of scoped) {
100
+ if (!record.approvalId) {
101
+ continue;
102
+ }
103
+ if (isApprovalRecorded(record)) {
104
+ approvalById.set(record.approvalId, record);
105
+ }
106
+ else if (isShellGateRecord(record) && inferWouldBlock(record)) {
107
+ denyByApprovalId.set(record.approvalId, record);
108
+ }
109
+ }
110
+ for (const record of timeFiltered) {
111
+ if (!record.approvalId) {
112
+ continue;
113
+ }
114
+ if (isApprovalRecorded(record)) {
115
+ const deny = denyByApprovalId.get(record.approvalId);
116
+ if (deny && !included.has(deny)) {
117
+ included.add(deny);
118
+ augmented.push(deny);
119
+ }
120
+ continue;
121
+ }
122
+ if (isShellGateRecord(record)) {
123
+ const approval = approvalById.get(record.approvalId);
124
+ if (approval && !included.has(approval)) {
125
+ included.add(approval);
126
+ augmented.push(approval);
127
+ }
128
+ }
129
+ }
130
+ return augmented.sort((left, right) => {
131
+ const leftMs = parseTimestamp(left.timestamp) ?? 0;
132
+ const rightMs = parseTimestamp(right.timestamp) ?? 0;
133
+ return leftMs - rightMs;
134
+ });
135
+ }
136
+ /** Shell gate rows plus approval events — matches what `belay harvest list` needs. */
137
+ export function filterRecordsForHarvest(records, options = {}) {
138
+ const scoped = records.filter((record) => isShellGateRecord(record) || isApprovalRecorded(record));
139
+ if (!options.since && !options.until) {
140
+ return scoped;
141
+ }
142
+ const timeFiltered = filterAuditRecords(scoped, {
143
+ since: options.since,
144
+ until: options.until,
145
+ });
146
+ return augmentHarvestRoundTripPairs(scoped, timeFiltered);
147
+ }
148
+ export function extractHarvestCandidates(records, options = {}) {
149
+ const shellOnly = shellRecords(records);
150
+ const classifierAsks = shellOnly.filter((record) => inferWouldBlock(record) && !isAvailabilityCausedAsk(record));
151
+ const map = new Map();
152
+ // `unknown` covers legacy shell rows missing `kind`.
153
+ const roundTrips = buildApprovalRoundTrips(recordsForShellRoundTrips(records)).filter((trip) => trip.kind === 'shell' || trip.kind === 'unknown');
154
+ for (const trip of roundTrips) {
155
+ if (!trip.fingerprint || !trip.approvalTimestamp) {
156
+ continue;
157
+ }
158
+ upsertCandidate(map, {
159
+ fingerprint: trip.fingerprint,
160
+ command: trip.summary,
161
+ reason: trip.reason,
162
+ source: 'deny_then_approve',
163
+ approvedAfterDeny: true,
164
+ });
165
+ }
166
+ for (const entry of computeRepeatedFingerprintAsks(classifierAsks, 2, 50)) {
167
+ upsertCandidate(map, {
168
+ fingerprint: entry.fingerprint,
169
+ command: entry.summary,
170
+ reason: entry.reason,
171
+ source: 'repeated_ask',
172
+ askCount: entry.askCount,
173
+ });
174
+ }
175
+ for (const record of classifierAsks) {
176
+ if (!record.fingerprint) {
177
+ continue;
178
+ }
179
+ const command = commandFromRecord(record);
180
+ if (!hasReadStyleSignal(command)) {
181
+ continue;
182
+ }
183
+ upsertCandidate(map, {
184
+ fingerprint: record.fingerprint,
185
+ command,
186
+ reason: record.reason ?? 'unknown',
187
+ source: 'read_style_signal',
188
+ });
189
+ }
190
+ // Current overrides.allow match on past would-blocks — not a changelog of new additions.
191
+ const allowPatterns = options.allowPatterns ?? [];
192
+ for (const record of classifierAsks) {
193
+ if (!record.fingerprint) {
194
+ continue;
195
+ }
196
+ const command = commandFromRecord(record);
197
+ if (!command) {
198
+ continue;
199
+ }
200
+ for (const pattern of allowPatterns) {
201
+ if (matchesCustomCommand(command, command, pattern)) {
202
+ upsertCandidate(map, {
203
+ fingerprint: record.fingerprint,
204
+ command,
205
+ reason: record.reason ?? 'unknown',
206
+ source: 'overrides_allow',
207
+ });
208
+ break;
209
+ }
210
+ }
211
+ }
212
+ return [...map.values()].sort((left, right) => {
213
+ if (right.askCount !== left.askCount) {
214
+ return right.askCount - left.askCount;
215
+ }
216
+ return left.command.localeCompare(right.command);
217
+ });
218
+ }
219
+ export function buildHarvestReport(records, options = {}) {
220
+ return {
221
+ schemaVersion: HARVEST_REPORT_SCHEMA_VERSION,
222
+ scope: 'shell',
223
+ candidates: extractHarvestCandidates(records, options),
224
+ availabilityQueue: extractAvailabilityQueue(records),
225
+ };
226
+ }
227
+ export function applyHarvestReview(cases, params) {
228
+ const command = params.command.trim();
229
+ if (!command) {
230
+ return { cases, applied: false, ok: false, message: 'Command must be non-empty.' };
231
+ }
232
+ if (params.outcome === 'reject') {
233
+ return {
234
+ cases,
235
+ applied: false,
236
+ ok: true,
237
+ message: `Reviewed and rejected candidate ${JSON.stringify(command)}.`,
238
+ };
239
+ }
240
+ const category = params.outcome;
241
+ const verdict = category === 'provably-benign' ? 'allow' : 'allow_flagged';
242
+ const reason = params.reason?.trim();
243
+ const duplicate = cases.find((entry) => entry.command === command);
244
+ if (duplicate) {
245
+ if (duplicate.category === category) {
246
+ return {
247
+ cases,
248
+ applied: false,
249
+ ok: false,
250
+ message: `Corpus already contains ${JSON.stringify(command)} as ${category}.`,
251
+ };
252
+ }
253
+ return {
254
+ cases,
255
+ applied: false,
256
+ ok: false,
257
+ message: `Corpus already contains ${JSON.stringify(command)} as ${duplicate.category}; resolve manually.`,
258
+ };
259
+ }
260
+ const nextCase = {
261
+ kind: 'shell',
262
+ category,
263
+ command,
264
+ verdict,
265
+ ...(reason ? { reason } : {}),
266
+ };
267
+ const followUp = category === 'provably-benign'
268
+ ? 'Next: run `pnpm corpus` and confirm hard gates pass before `pnpm build` refreshes the standing-allow catalog.'
269
+ : 'Next: run `pnpm corpus` to verify corpus evaluation (accepted-benign is soft-gated).';
270
+ return {
271
+ cases: [...cases, nextCase],
272
+ applied: true,
273
+ ok: true,
274
+ message: `Added ${JSON.stringify(command)} to corpus as ${category}. ${followUp}`,
275
+ };
276
+ }
@@ -1,9 +1,14 @@
1
- export { APPROVAL_EXECUTION_LEASE_MS, approvalCommandMatch, buildRetryInstruction, compactApprovals, createApprovalRecord, isExecutionLeaseExpired, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
1
+ export { APPROVAL_EXECUTION_LEASE_MS, approvalCommandMatch, compactApprovals, createApprovalRecord, createApprovalRecordWithEnvelope, isExecutionLeaseExpired, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
2
+ export type { ApprovalReplayHint, ReplayActionContext, ReplayAdapterId, } from './approval-replay.js';
3
+ /** @deprecated Use `buildRetryInstructionForConfig` */
4
+ export { approvalFlow, buildApprovalRecordedMessage, buildReplayEnvelopeFields, buildReplayHint, buildRetryInstructionForConfig, buildRetryInstructionForConfig as buildRetryInstruction, canAutoReplay, getExecutionLeaseMs, replayShellCommand, validateReplayEnvelope, } from './approval-replay.js';
5
+ export type { ApprovalStore } from './approval-service.js';
6
+ export { consumeApprovedAfterCliReplay, createGateApprovalStore, gateApprovalStoreFromDeps, recordApproval, } from './approval-service.js';
2
7
  export type { AuditMetricsReport } from './audit-metrics.js';
3
8
  export { computeAuditMetrics, parseAuditNdjson } from './audit-metrics.js';
4
9
  export { classifySubagent } from './classify-subagent.js';
5
10
  export { classifyToolUse } from './classify-tool.js';
6
- export { approvedApprovalsFile, type BelayConfig, type BelayConfigV1, type BelayConfigV2, type BelayConfigV3, type BelayConfigV4, type BelayControlPlaneConfig, type BelayJudgeConfig, type BelayOverridesConfig, type BelayPolicyConfig, type BelayRedactionConfig, belayStateDir, classifierOptionsFromConfig, configuredControlPlaneDir, DEFAULT_CONFIG_V2, DEFAULT_CONFIG_V3, DEFAULT_CONFIG_V4, DEFAULT_JUDGE_CURSOR_COMPOSER, DEFAULT_JUDGE_LOCAL_OLLAMA, defaultControlPlaneDir, isConfigV4, isFreshConfigInput, LEGACY_POLICY_V3, mapLegacyClassifierToOverrides, mergeConfig, migrateConfig, migrateV2ToV3, normalizeConfig, normalizeJudgeConfig, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, } from './config.js';
11
+ export { type ApprovalFlow, approvedApprovalsFile, type BelayApprovalAutoReplayScopes, type BelayApprovalConfig, type BelayConfig, type BelayConfigV1, type BelayConfigV2, type BelayConfigV3, type BelayConfigV4, type BelayControlPlaneConfig, type BelayJudgeConfig, type BelayOverridesConfig, type BelayPolicyConfig, type BelayRedactionConfig, belayStateDir, classifierOptionsFromConfig, configuredControlPlaneDir, DEFAULT_APPROVAL_CONFIG, DEFAULT_CONFIG_V2, DEFAULT_CONFIG_V3, DEFAULT_CONFIG_V4, DEFAULT_JUDGE_CURSOR_COMPOSER, DEFAULT_JUDGE_LOCAL_OLLAMA, defaultControlPlaneDir, isConfigV4, isFreshConfigInput, LEGACY_POLICY_V3, mapLegacyClassifierToOverrides, mergeConfig, migrateConfig, migrateV2ToV3, normalizeConfig, normalizeJudgeConfig, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, } from './config.js';
7
12
  export { matchesCustomCommand } from './custom-command-match.js';
8
13
  export { canonicalStringify, hashValue, shellFingerprint, subagentFingerprint, toolFingerprint, } from './fingerprint.js';
9
14
  export type { GatedAction, GatedActionKind, GatePermissionResponse, GateVerdict, } from './gate-contract.js';
@@ -11,9 +16,10 @@ export { classifyResultToGateVerdict, GATE_CONTRACT_VERSION, isGatedAction, unno
11
16
  export { classifyGatedAction, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from './gate-engine.js';
12
17
  export { matchesSensitivePath } from './glob.js';
13
18
  export { canonicalPath, hasOutsideRepoPath, normalizeToken, pathWithinRoot, relativeWithinRepo, resolveMutationTarget, } from './path-utils.js';
19
+ export { fingerprintReplayPayload, subagentFingerprintSource } from './replay-scrub.js';
14
20
  export { scrubString, scrubValue } from './scrub.js';
15
21
  export { findCommandSubstitutions, MAX_SUBSTITUTION_DEPTH } from './shell-substitution.js';
16
22
  export type { TransactionalDiffEvaluation, TransactionalExecutionResult, } from './transactional/index.js';
17
23
  export { isTransactionalEligible, runTransactionalExecution } from './transactional/index.js';
18
- export type { ApprovalRecord, ApprovalStateFile, Assessment, ClassifierOptions, ClassifyResult, HookVerdict, Reversibility, ScrubOptions, UnknownLocalEffectPolicy, } from './types.js';
24
+ export type { ApprovalRecord, ApprovalScopeHint, ApprovalStateFile, Assessment, ClassifierOptions, ClassifyResult, HookVerdict, Reversibility, ScrubOptions, UnknownLocalEffectPolicy, } from './types.js';
19
25
  export { buildVerdictContext, classifyShell, verdict, verdictToClassifyResult, } from './verdict/index.js';
@@ -1,14 +1,18 @@
1
- export { APPROVAL_EXECUTION_LEASE_MS, approvalCommandMatch, buildRetryInstruction, compactApprovals, createApprovalRecord, isExecutionLeaseExpired, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
1
+ export { APPROVAL_EXECUTION_LEASE_MS, approvalCommandMatch, compactApprovals, createApprovalRecord, createApprovalRecordWithEnvelope, isExecutionLeaseExpired, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
2
+ /** @deprecated Use `buildRetryInstructionForConfig` */
3
+ export { approvalFlow, buildApprovalRecordedMessage, buildReplayEnvelopeFields, buildReplayHint, buildRetryInstructionForConfig, buildRetryInstructionForConfig as buildRetryInstruction, canAutoReplay, getExecutionLeaseMs, replayShellCommand, validateReplayEnvelope, } from './approval-replay.js';
4
+ export { consumeApprovedAfterCliReplay, createGateApprovalStore, gateApprovalStoreFromDeps, recordApproval, } from './approval-service.js';
2
5
  export { computeAuditMetrics, parseAuditNdjson } from './audit-metrics.js';
3
6
  export { classifySubagent } from './classify-subagent.js';
4
7
  export { classifyToolUse } from './classify-tool.js';
5
- export { approvedApprovalsFile, belayStateDir, classifierOptionsFromConfig, configuredControlPlaneDir, DEFAULT_CONFIG_V2, DEFAULT_CONFIG_V3, DEFAULT_CONFIG_V4, DEFAULT_JUDGE_CURSOR_COMPOSER, DEFAULT_JUDGE_LOCAL_OLLAMA, defaultControlPlaneDir, isConfigV4, isFreshConfigInput, LEGACY_POLICY_V3, mapLegacyClassifierToOverrides, mergeConfig, migrateConfig, migrateV2ToV3, normalizeConfig, normalizeJudgeConfig, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, } from './config.js';
8
+ export { approvedApprovalsFile, belayStateDir, classifierOptionsFromConfig, configuredControlPlaneDir, DEFAULT_APPROVAL_CONFIG, DEFAULT_CONFIG_V2, DEFAULT_CONFIG_V3, DEFAULT_CONFIG_V4, DEFAULT_JUDGE_CURSOR_COMPOSER, DEFAULT_JUDGE_LOCAL_OLLAMA, defaultControlPlaneDir, isConfigV4, isFreshConfigInput, LEGACY_POLICY_V3, mapLegacyClassifierToOverrides, mergeConfig, migrateConfig, migrateV2ToV3, normalizeConfig, normalizeJudgeConfig, pendingApprovalsFile, resolveControlPlaneDir, scrubOptionsFromConfig, } from './config.js';
6
9
  export { matchesCustomCommand } from './custom-command-match.js';
7
10
  export { canonicalStringify, hashValue, shellFingerprint, subagentFingerprint, toolFingerprint, } from './fingerprint.js';
8
11
  export { classifyResultToGateVerdict, GATE_CONTRACT_VERSION, isGatedAction, unnormalizedGateVerdict, } from './gate-contract.js';
9
12
  export { classifyGatedAction, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from './gate-engine.js';
10
13
  export { matchesSensitivePath } from './glob.js';
11
14
  export { canonicalPath, hasOutsideRepoPath, normalizeToken, pathWithinRoot, relativeWithinRepo, resolveMutationTarget, } from './path-utils.js';
15
+ export { fingerprintReplayPayload, subagentFingerprintSource } from './replay-scrub.js';
12
16
  export { scrubString, scrubValue } from './scrub.js';
13
17
  export { findCommandSubstitutions, MAX_SUBSTITUTION_DEPTH } from './shell-substitution.js';
14
18
  export { isTransactionalEligible, runTransactionalExecution } from './transactional/index.js';
@@ -8,5 +8,8 @@ export interface JudgeDoctorResult {
8
8
  }
9
9
  export interface DiagnoseJudgeOptions {
10
10
  discoveryDeps?: JudgeModelDiscoveryDeps;
11
+ liveProbe?: boolean;
11
12
  }
12
13
  export declare function diagnoseJudge(config: BelayConfigV4, repoRoot?: string, options?: DiagnoseJudgeOptions): Promise<JudgeDoctorResult>;
14
+ export declare function stopJudgeSessionBrokers(repoRoot: string, stateDir: string): Promise<number>;
15
+ export declare function stopInProcessJudgeSessionBrokers(repoRoot?: string): number;
@@ -2,16 +2,30 @@ import { repoLocalStateDirFor } from '../config-io.js';
2
2
  import { normalizeJudgeProvider, scrubOptionsFromConfig } from './config.js';
3
3
  import { resolveJudgeCredential } from './judge-api-key.js';
4
4
  import { hasValidCloudConsent } from './judge-config.js';
5
+ import { formatJudgeRecoveryHint, inferProviderIdFromFallbackReason, } from './judge-fallback-hints.js';
5
6
  import { discoverJudgeModels, modelPresenceFromDiscovery, } from './judge-model-discovery.js';
6
7
  import { detectJudgeRuntimeCapabilities, resolveJudgeTransport } from './judge-runtime-detection.js';
7
8
  import { createOllamaJudge, createOpenAiCompatibleJudge } from './verdict/judge.js';
9
+ import { stopJudgeBrokerDaemon } from './verdict/judge-broker-service.js';
8
10
  import { getJudgeProviderCapabilities, getJudgeProviderSpec, isRemovedProviderId, normalizeLegacyProviderId, } from './verdict/judge-catalog.js';
9
11
  import { createJudgeFromConfig, resolveJudgeModel } from './verdict/judge-factory.js';
12
+ import { resolveJudgeSmokeProbeTimeoutMs } from './verdict/judge-runtime-config.js';
13
+ import { listRepoJudgeSessionBrokers, stopRepoJudgeSessionBroker, } from './verdict/judge-session-broker.js';
14
+ import { clearJudgeSessionKillSwitch } from './verdict/judge-session-kill-switch.js';
15
+ function judgeShadowAdvisory(warnings, message) {
16
+ warnings.push(`[judge shadow advisory] ${message}`);
17
+ }
10
18
  export async function diagnoseJudge(config, repoRoot = process.cwd(), options = {}) {
19
+ const shouldRunLiveProbe = options.liveProbe === true && !process.env.VITEST && !process.env.VITEST_WORKER_ID;
11
20
  const issues = [];
12
21
  const warnings = [];
13
22
  const notes = [];
14
23
  const judge = config.judge;
24
+ const gateMode = judge.mode ?? 'shadow';
25
+ notes.push(`Judge gate mode: ${gateMode} (sync Tier1 removed from hook gate; judge is shadow/async only)`);
26
+ if (gateMode === 'off') {
27
+ notes.push('Judge shadow is disabled (judge.mode=off).');
28
+ }
15
29
  const provider = normalizeJudgeProvider(judge.provider);
16
30
  const rawProviderId = judge.providerId ? String(judge.providerId) : undefined;
17
31
  if (rawProviderId && isRemovedProviderId(rawProviderId)) {
@@ -32,15 +46,19 @@ export async function diagnoseJudge(config, repoRoot = process.cwd(), options =
32
46
  const capabilities = getJudgeProviderCapabilities(providerId);
33
47
  const transport = resolveJudgeTransport(judge);
34
48
  const runtime = detectJudgeRuntimeCapabilities(providerId);
49
+ const cursorAcpEnabled = transport === 'cursor-cli' &&
50
+ providerId === 'cursor' &&
51
+ config.judge.runtime?.session.enabled === true;
52
+ const effectiveTransport = cursorAcpEnabled ? 'cursor-acp' : transport;
35
53
  notes.push(`Judge providerId: ${providerId}`);
36
54
  notes.push(`Judge driver: ${provider}`);
37
55
  notes.push(`Judge model requested: ${judge.model}`);
38
- notes.push(`Judge transport: ${transport}`);
56
+ notes.push(`Judge transport: ${effectiveTransport}`);
39
57
  if (config.policy.modelAssist.enabled) {
40
58
  warnings.push('policy.modelAssist is enabled but is not wired to v2 Tier1. Use top-level judge instead.');
41
59
  }
42
60
  if (capabilities?.requiresConsent && !hasValidCloudConsent(judge) && transport === 'http') {
43
- issues.push('Cloud judge consent is not recorded. Tier1 cloud judge will fail closed until consent is granted.');
61
+ judgeShadowAdvisory(warnings, 'Cloud judge consent is not recorded. Shadow cloud judge will fail until consent is granted.');
44
62
  }
45
63
  else if (judge.cloudConsent?.accepted) {
46
64
  notes.push(`Cloud consent: accepted ${judge.cloudConsent.at} by ${judge.cloudConsent.by}`);
@@ -70,12 +88,12 @@ export async function diagnoseJudge(config, repoRoot = process.cwd(), options =
70
88
  if (providerId === 'ollama') {
71
89
  notes.push(`Ollama endpoint: ${endpoint}`);
72
90
  if (discovery.modelIds.length === 0) {
73
- issues.push(`Ollama endpoint unreachable or returned no models. Tier1 will fail closed.`);
91
+ judgeShadowAdvisory(warnings, 'Ollama endpoint unreachable or returned no models. Shadow judge will not run.');
74
92
  }
75
93
  else {
76
94
  const hasModel = modelCheck.status === 'found';
77
95
  if (!hasModel) {
78
- issues.push(`Ollama model "${judge.model}" is not present. Pull it before enforce mode.`);
96
+ judgeShadowAdvisory(warnings, `Ollama model "${judge.model}" is not present. Pull it before enabling shadow comparisons.`);
79
97
  }
80
98
  else {
81
99
  notes.push(`Ollama model "${judge.model}" is available.`);
@@ -99,7 +117,7 @@ export async function diagnoseJudge(config, repoRoot = process.cwd(), options =
99
117
  context: { cwd: process.cwd(), repoRoot: process.cwd() },
100
118
  });
101
119
  if (warmResult.reason === 'ollama_unavailable' || warmResult.reason === 'ollama_parse_error') {
102
- issues.push(`Ollama warm call failed: ${warmResult.reason}`);
120
+ judgeShadowAdvisory(warnings, `Ollama warm call failed: ${warmResult.reason}`);
103
121
  }
104
122
  else {
105
123
  notes.push('Ollama warm call succeeded.');
@@ -107,18 +125,52 @@ export async function diagnoseJudge(config, repoRoot = process.cwd(), options =
107
125
  return { issues, warnings, notes, modelCheck };
108
126
  }
109
127
  if (transport === 'unavailable') {
110
- issues.push('No judge transport is available (configure endpoint or install native CLI). Tier1 will fail closed to ask.');
128
+ judgeShadowAdvisory(warnings, 'No judge transport is available (configure endpoint or install native CLI). Shadow judge will not run.');
111
129
  return { issues, warnings, notes, modelCheck };
112
130
  }
113
131
  if (transport.endsWith('-cli')) {
114
132
  if (!runtime.cliTransport) {
115
- issues.push(`Native CLI transport (${transport}) is not available. Tier1 judge will fail closed to ask.`);
133
+ judgeShadowAdvisory(warnings, `Native CLI transport (${transport}) is not available. Shadow judge will not run.`);
116
134
  }
117
135
  else if (!keyInfo.key && keyInfo.sourceKind !== 'host-session') {
118
- issues.push('Judge API key is not set for the configured credential mode. Tier1 cloud judge will fail closed to ask.');
136
+ judgeShadowAdvisory(warnings, 'Judge API key is not set for the configured credential mode. Shadow cloud judge will not run.');
119
137
  }
120
138
  else {
121
- notes.push(`Native CLI transport available: ${transport}`);
139
+ notes.push(cursorAcpEnabled
140
+ ? 'Cursor ACP transport available.'
141
+ : `Native CLI transport available: ${transport}`);
142
+ if (cursorAcpEnabled) {
143
+ notes.push(`Cursor ACP session transport enabled (max ${config.judge.runtime?.session.maxTurns ?? 'unknown'} turns).`);
144
+ }
145
+ if (shouldRunLiveProbe) {
146
+ const smokeConfig = {
147
+ ...config,
148
+ judge: {
149
+ ...judge,
150
+ timeoutMs: resolveJudgeSmokeProbeTimeoutMs(judge.timeoutMs),
151
+ },
152
+ };
153
+ const smokeJudge = createJudgeFromConfig(smokeConfig, { repoRoot });
154
+ const smokeResult = await smokeJudge.evaluate({
155
+ text: 'git status',
156
+ context: { cwd: repoRoot, repoRoot },
157
+ });
158
+ const smokeFallback = smokeJudge.lastTrace?.judgeFallbackReason ??
159
+ smokeJudge.lastTrace?.fallbackReason ??
160
+ smokeResult.reason;
161
+ if (smokeJudge.lastTrace?.provider === 'fallback') {
162
+ const recoveryHint = formatJudgeRecoveryHint(inferProviderIdFromFallbackReason(smokeFallback, providerId), smokeFallback);
163
+ const smokeLabel = cursorAcpEnabled ? 'Cursor ACP' : 'Native CLI transport';
164
+ judgeShadowAdvisory(warnings, recoveryHint
165
+ ? `${smokeLabel} smoke probe failed (${smokeFallback}). ${recoveryHint}`
166
+ : `${smokeLabel} smoke probe failed (${smokeFallback}).`);
167
+ }
168
+ else {
169
+ notes.push(cursorAcpEnabled
170
+ ? 'Cursor ACP smoke probe succeeded.'
171
+ : 'Native CLI transport smoke probe succeeded.');
172
+ }
173
+ }
122
174
  }
123
175
  return { issues, warnings, notes, modelCheck };
124
176
  }
@@ -126,7 +178,7 @@ export async function diagnoseJudge(config, repoRoot = process.cwd(), options =
126
178
  notes.push(`HTTP endpoint: ${judge.endpoint}`);
127
179
  }
128
180
  if (!keyInfo.key) {
129
- issues.push('Judge API key is not set for the configured credential mode. Tier1 cloud judge will fail closed to ask.');
181
+ judgeShadowAdvisory(warnings, 'Judge API key is not set for the configured credential mode. Shadow cloud judge will not run.');
130
182
  }
131
183
  else {
132
184
  notes.push(`Credential source: ${keyInfo.source ?? keyInfo.sourceKind}`);
@@ -161,7 +213,7 @@ export async function diagnoseJudge(config, repoRoot = process.cwd(), options =
161
213
  });
162
214
  if (dryRun.reason.startsWith('openai_compatible_') ||
163
215
  dryRun.reason === 'outbound_scrub_failed') {
164
- issues.push(`HTTP judge dry-run failed: ${dryRun.reason}`);
216
+ judgeShadowAdvisory(warnings, `HTTP judge dry-run failed: ${dryRun.reason}`);
165
217
  }
166
218
  else {
167
219
  notes.push('HTTP judge dry-run succeeded.');
@@ -171,5 +223,32 @@ export async function diagnoseJudge(config, repoRoot = process.cwd(), options =
171
223
  if (factoryJudge.lastTrace?.transport) {
172
224
  notes.push(`Factory transport: ${factoryJudge.lastTrace.transport}`);
173
225
  }
226
+ if (config.judge.runtime?.session.enabled) {
227
+ notes.push('Judge session transport: enabled');
228
+ const activeBrokers = listRepoJudgeSessionBrokers();
229
+ if (activeBrokers.includes(repoRoot)) {
230
+ notes.push(`Active session broker for repo: ${repoRoot}`);
231
+ }
232
+ }
233
+ else {
234
+ notes.push('Judge session transport: disabled (default)');
235
+ }
174
236
  return { issues, warnings, notes, modelCheck };
175
237
  }
238
+ export async function stopJudgeSessionBrokers(repoRoot, stateDir) {
239
+ let stopped = 0;
240
+ stopped += stopRepoJudgeSessionBroker(repoRoot, 'manual_stop');
241
+ stopped += await stopJudgeBrokerDaemon(stateDir);
242
+ await clearJudgeSessionKillSwitch(stateDir);
243
+ return stopped;
244
+ }
245
+ export function stopInProcessJudgeSessionBrokers(repoRoot) {
246
+ if (repoRoot) {
247
+ return stopRepoJudgeSessionBroker(repoRoot, 'manual_stop');
248
+ }
249
+ let stopped = 0;
250
+ for (const root of listRepoJudgeSessionBrokers()) {
251
+ stopped += stopRepoJudgeSessionBroker(root, 'manual_stop');
252
+ }
253
+ return stopped;
254
+ }
@@ -0,0 +1,13 @@
1
+ import type { ClassifyResult } from './types.js';
2
+ export declare function extractJudgeFallbackReason(result: Pick<ClassifyResult, 'axes' | 'assessment'>): string | undefined;
3
+ export declare function isJudgeInfrastructureFailure(result: Pick<ClassifyResult, 'axes' | 'assessment' | 'reason'>): boolean;
4
+ export declare function inferProviderIdFromFallbackReason(fallbackReason: string | undefined, fallbackProviderId?: string): string;
5
+ export declare function formatJudgeRecoveryHint(providerId: string, fallbackReason?: string): string | null;
6
+ export declare function formatJudgeInfrastructureDenyMessage(params: {
7
+ providerId: string;
8
+ fallbackReason?: string;
9
+ command?: string;
10
+ }): {
11
+ user_message: string;
12
+ agent_message: string;
13
+ };