@guilz-dev/belay 0.4.0 → 0.7.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 (240) hide show
  1. package/README.md +27 -2
  2. package/dist/adapters/claude/runtime-entry.js +54 -25
  3. package/dist/adapters/layouts/scope.js +3 -2
  4. package/dist/adapters/shared/gate-runtime.d.ts +8 -2
  5. package/dist/adapters/shared/gate-runtime.js +579 -181
  6. package/dist/bundle/claude-runtime.mjs +13574 -6334
  7. package/dist/bundle/codex-runtime.mjs +13108 -5903
  8. package/dist/bundle/cursor-runtime.mjs +12676 -5476
  9. package/dist/cli.js +80 -2
  10. package/dist/commands/approve.js +34 -10
  11. package/dist/commands/classify-for-report.js +1 -1
  12. package/dist/commands/config.d.ts +8 -0
  13. package/dist/commands/config.js +134 -11
  14. package/dist/commands/doctor.js +87 -1
  15. package/dist/commands/explain.js +11 -1
  16. package/dist/commands/judge.js +3 -2
  17. package/dist/commands/recovery-checkpoints.d.ts +164 -0
  18. package/dist/commands/recovery-checkpoints.js +347 -0
  19. package/dist/commands/revoke.js +19 -7
  20. package/dist/commands/session.d.ts +20 -0
  21. package/dist/commands/session.js +51 -0
  22. package/dist/commands/status.js +7 -0
  23. package/dist/config-io.js +91 -23
  24. package/dist/conformance/guarantee-posture.d.ts +21 -0
  25. package/dist/conformance/guarantee-posture.js +46 -0
  26. package/dist/conformance/guarantee-table.d.ts +1 -0
  27. package/dist/conformance/guarantee-table.js +20 -9
  28. package/dist/conformance/types.d.ts +1 -0
  29. package/dist/core/approval-replay.d.ts +1 -1
  30. package/dist/core/approval-replay.js +4 -1
  31. package/dist/core/approval-service.d.ts +6 -1
  32. package/dist/core/approval-service.js +87 -24
  33. package/dist/core/approval.d.ts +4 -0
  34. package/dist/core/approval.js +19 -5
  35. package/dist/core/audit-types.d.ts +13 -0
  36. package/dist/core/capability/approval-state-mutation.d.ts +32 -0
  37. package/dist/core/capability/approval-state-mutation.js +165 -0
  38. package/dist/core/capability/approval-v3.d.ts +32 -0
  39. package/dist/core/capability/approval-v3.js +114 -0
  40. package/dist/core/capability/attestation.d.ts +16 -0
  41. package/dist/core/capability/attestation.js +62 -0
  42. package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
  43. package/dist/core/capability/boundary-attestation-sign.js +84 -0
  44. package/dist/core/capability/boundary-driver-container.d.ts +19 -0
  45. package/dist/core/capability/boundary-driver-container.js +189 -0
  46. package/dist/core/capability/boundary-driver.d.ts +29 -0
  47. package/dist/core/capability/boundary-driver.js +44 -0
  48. package/dist/core/capability/boundary-egress.d.ts +21 -0
  49. package/dist/core/capability/boundary-egress.js +109 -0
  50. package/dist/core/capability/boundary-grant-materialize.d.ts +14 -0
  51. package/dist/core/capability/boundary-grant-materialize.js +93 -0
  52. package/dist/core/capability/boundary-profile.d.ts +15 -0
  53. package/dist/core/capability/boundary-profile.js +25 -0
  54. package/dist/core/capability/boundary-run.d.ts +48 -0
  55. package/dist/core/capability/boundary-run.js +51 -0
  56. package/dist/core/capability/boundary-session.d.ts +61 -0
  57. package/dist/core/capability/boundary-session.js +138 -0
  58. package/dist/core/capability/boundary-workspace-mount.d.ts +5 -0
  59. package/dist/core/capability/boundary-workspace-mount.js +68 -0
  60. package/dist/core/capability/capability-request-hash.d.ts +2 -0
  61. package/dist/core/capability/capability-request-hash.js +8 -0
  62. package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
  63. package/dist/core/capability/gate-latency-ratchet.js +18 -0
  64. package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
  65. package/dist/core/capability/gate-policy-shadow.js +130 -0
  66. package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
  67. package/dist/core/capability/gate-shadow-audit.js +1 -0
  68. package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
  69. package/dist/core/capability/gate-shadow-ratchet.js +81 -0
  70. package/dist/core/capability/grant-consumption.d.ts +4 -0
  71. package/dist/core/capability/grant-consumption.js +11 -0
  72. package/dist/core/capability/grant-lease.d.ts +44 -0
  73. package/dist/core/capability/grant-lease.js +243 -0
  74. package/dist/core/capability/grant-loader.d.ts +12 -0
  75. package/dist/core/capability/grant-loader.js +23 -0
  76. package/dist/core/capability/grant-match.d.ts +6 -0
  77. package/dist/core/capability/grant-match.js +96 -0
  78. package/dist/core/capability/grant.d.ts +16 -0
  79. package/dist/core/capability/grant.js +16 -0
  80. package/dist/core/capability/index.d.ts +8 -0
  81. package/dist/core/capability/index.js +7 -0
  82. package/dist/core/capability/limits.d.ts +7 -0
  83. package/dist/core/capability/limits.js +55 -0
  84. package/dist/core/capability/policy-bridge.d.ts +18 -0
  85. package/dist/core/capability/policy-bridge.js +51 -0
  86. package/dist/core/capability/policy-engine.d.ts +77 -0
  87. package/dist/core/capability/policy-engine.js +663 -0
  88. package/dist/core/capability/policy-types.d.ts +20 -0
  89. package/dist/core/capability/policy-types.js +1 -0
  90. package/dist/core/capability/request.d.ts +48 -0
  91. package/dist/core/capability/request.js +1 -0
  92. package/dist/core/capability/resolver.d.ts +4 -0
  93. package/dist/core/capability/resolver.js +4 -0
  94. package/dist/core/classify-subagent.d.ts +2 -1
  95. package/dist/core/classify-subagent.js +38 -2
  96. package/dist/core/classify-tool.js +126 -62
  97. package/dist/core/config.d.ts +34 -3
  98. package/dist/core/config.js +118 -3
  99. package/dist/core/effect-ir/audit.d.ts +9 -0
  100. package/dist/core/effect-ir/audit.js +86 -0
  101. package/dist/core/effect-ir/build.d.ts +31 -0
  102. package/dist/core/effect-ir/build.js +277 -0
  103. package/dist/core/effect-ir/index.d.ts +8 -0
  104. package/dist/core/effect-ir/index.js +7 -0
  105. package/dist/core/effect-ir/normalize.d.ts +4 -0
  106. package/dist/core/effect-ir/normalize.js +104 -0
  107. package/dist/core/effect-ir/package-exec-eval.d.ts +8 -0
  108. package/dist/core/effect-ir/package-exec-eval.js +152 -0
  109. package/dist/core/effect-ir/package-exec.d.ts +29 -0
  110. package/dist/core/effect-ir/package-exec.js +292 -0
  111. package/dist/core/effect-ir/policy.d.ts +13 -0
  112. package/dist/core/effect-ir/policy.js +77 -0
  113. package/dist/core/effect-ir/types.d.ts +55 -0
  114. package/dist/core/effect-ir/types.js +1 -0
  115. package/dist/core/egress/allowlist.d.ts +1 -0
  116. package/dist/core/egress/allowlist.js +8 -0
  117. package/dist/core/egress-approval.js +45 -51
  118. package/dist/core/gate-contract.d.ts +5 -0
  119. package/dist/core/gate-contract.js +5 -0
  120. package/dist/core/gate-engine.js +30 -8
  121. package/dist/core/index.d.ts +3 -1
  122. package/dist/core/index.js +2 -1
  123. package/dist/core/judge-cloud-consent.js +29 -15
  124. package/dist/core/judge-doctor.js +39 -14
  125. package/dist/core/judge-fallback-hints.d.ts +13 -0
  126. package/dist/core/judge-fallback-hints.js +108 -0
  127. package/dist/core/network-endpoint.d.ts +10 -0
  128. package/dist/core/network-endpoint.js +51 -0
  129. package/dist/core/path-utils.d.ts +1 -0
  130. package/dist/core/path-utils.js +5 -2
  131. package/dist/core/process-runner.d.ts +10 -0
  132. package/dist/core/process-runner.js +124 -0
  133. package/dist/core/recover-advice.js +2 -1
  134. package/dist/core/recovery/artifact-store.d.ts +29 -0
  135. package/dist/core/recovery/artifact-store.js +336 -0
  136. package/dist/core/recovery/capability.d.ts +3 -0
  137. package/dist/core/recovery/capability.js +22 -0
  138. package/dist/core/recovery/checkpoint.d.ts +25 -0
  139. package/dist/core/recovery/checkpoint.js +260 -0
  140. package/dist/core/recovery/fail-closed.d.ts +6 -0
  141. package/dist/core/recovery/fail-closed.js +25 -0
  142. package/dist/core/recovery/index.d.ts +5 -0
  143. package/dist/core/recovery/index.js +4 -0
  144. package/dist/core/recovery/operator-guidance.d.ts +7 -0
  145. package/dist/core/recovery/operator-guidance.js +39 -0
  146. package/dist/core/recovery/reconcile.d.ts +3 -0
  147. package/dist/core/recovery/reconcile.js +48 -0
  148. package/dist/core/recovery/resource-identity.d.ts +4 -0
  149. package/dist/core/recovery/resource-identity.js +34 -0
  150. package/dist/core/recovery/restore.d.ts +13 -0
  151. package/dist/core/recovery/restore.js +114 -0
  152. package/dist/core/recovery/snapshot-node.d.ts +27 -0
  153. package/dist/core/recovery/snapshot-node.js +261 -0
  154. package/dist/core/recovery/types.d.ts +104 -0
  155. package/dist/core/recovery/types.js +1 -0
  156. package/dist/core/standing-allow.js +3 -0
  157. package/dist/core/transactional/apply-observed-changes.d.ts +13 -0
  158. package/dist/core/transactional/apply-observed-changes.js +275 -0
  159. package/dist/core/transactional/backend-selector.d.ts +8 -0
  160. package/dist/core/transactional/backend-selector.js +118 -0
  161. package/dist/core/transactional/backend.d.ts +55 -0
  162. package/dist/core/transactional/backend.js +1 -0
  163. package/dist/core/transactional/diff-evaluator.js +4 -1
  164. package/dist/core/transactional/eligibility.js +4 -0
  165. package/dist/core/transactional/file-checkpoint-backend.d.ts +3 -0
  166. package/dist/core/transactional/file-checkpoint-backend.js +278 -0
  167. package/dist/core/transactional/file-checkpoint-git.d.ts +11 -0
  168. package/dist/core/transactional/file-checkpoint-git.js +217 -0
  169. package/dist/core/transactional/file-checkpoint-staging.d.ts +13 -0
  170. package/dist/core/transactional/file-checkpoint-staging.js +54 -0
  171. package/dist/core/transactional/file-clone.d.ts +15 -0
  172. package/dist/core/transactional/file-clone.js +139 -0
  173. package/dist/core/transactional/file-tree-path.d.ts +7 -0
  174. package/dist/core/transactional/file-tree-path.js +50 -0
  175. package/dist/core/transactional/file-tree.d.ts +41 -0
  176. package/dist/core/transactional/file-tree.js +186 -0
  177. package/dist/core/transactional/git-worktree-backend.d.ts +2 -0
  178. package/dist/core/transactional/git-worktree-backend.js +50 -0
  179. package/dist/core/transactional/git-worktree.d.ts +14 -9
  180. package/dist/core/transactional/git-worktree.js +62 -87
  181. package/dist/core/transactional/index.d.ts +11 -0
  182. package/dist/core/transactional/index.js +10 -0
  183. package/dist/core/transactional/reasons.d.ts +1 -0
  184. package/dist/core/transactional/reasons.js +5 -0
  185. package/dist/core/transactional/runner.js +171 -23
  186. package/dist/core/transactional/snapshot-node.d.ts +26 -0
  187. package/dist/core/transactional/snapshot-node.js +87 -0
  188. package/dist/core/transactional/types.d.ts +21 -1
  189. package/dist/core/types.d.ts +30 -3
  190. package/dist/core/verdict/adapter.d.ts +3 -4
  191. package/dist/core/verdict/adapter.js +24 -12
  192. package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
  193. package/dist/core/verdict/cursor-acp-client.js +345 -0
  194. package/dist/core/verdict/egress-classify.d.ts +2 -2
  195. package/dist/core/verdict/egress-classify.js +2 -2
  196. package/dist/core/verdict/git-classifier.d.ts +26 -0
  197. package/dist/core/verdict/git-classifier.js +533 -0
  198. package/dist/core/verdict/judge-audit.js +1 -0
  199. package/dist/core/verdict/judge-baseline.d.ts +1 -1
  200. package/dist/core/verdict/judge-baseline.js +1 -0
  201. package/dist/core/verdict/judge-broker-service.js +12 -0
  202. package/dist/core/verdict/judge-catalog.js +1 -0
  203. package/dist/core/verdict/judge-cli-fingerprint.d.ts +1 -0
  204. package/dist/core/verdict/judge-cli-fingerprint.js +6 -0
  205. package/dist/core/verdict/judge-cli.js +5 -3
  206. package/dist/core/verdict/judge-provider-matrix.js +1 -1
  207. package/dist/core/verdict/judge-runtime-config.d.ts +2 -0
  208. package/dist/core/verdict/judge-runtime-config.js +10 -6
  209. package/dist/core/verdict/judge-session-broker.d.ts +1 -1
  210. package/dist/core/verdict/judge-session-broker.js +10 -5
  211. package/dist/core/verdict/judge-transport.d.ts +5 -1
  212. package/dist/core/verdict/judge-transport.js +24 -14
  213. package/dist/core/verdict/judge.d.ts +2 -15
  214. package/dist/core/verdict/judge.js +5 -68
  215. package/dist/core/verdict/launcher-resolve.d.ts +2 -0
  216. package/dist/core/verdict/launcher-resolve.js +14 -16
  217. package/dist/core/verdict/prescan.d.ts +15 -0
  218. package/dist/core/verdict/prescan.js +68 -0
  219. package/dist/core/verdict/shell-policy.d.ts +26 -0
  220. package/dist/core/verdict/shell-policy.js +40 -0
  221. package/dist/core/verdict/shell-semantics.d.ts +24 -0
  222. package/dist/core/verdict/shell-semantics.js +225 -0
  223. package/dist/core/verdict/types.d.ts +18 -1
  224. package/dist/core/verdict/verdict.js +313 -178
  225. package/dist/corpus/evaluate.js +3 -7
  226. package/dist/corpus/gate-latency-budget.d.ts +39 -0
  227. package/dist/corpus/gate-latency-budget.js +33 -0
  228. package/dist/corpus/mutators.d.ts +1 -1
  229. package/dist/corpus/mutators.js +11 -0
  230. package/dist/corpus/runtime-match.js +1 -3
  231. package/dist/corpus/standing-allow-catalog.generated.js +29 -9
  232. package/dist/judge-broker-daemon.js +15 -1
  233. package/dist/services/sandbox-service.d.ts +4 -0
  234. package/dist/services/sandbox-service.js +28 -2
  235. package/dist/types.d.ts +13 -2
  236. package/dist/version.d.ts +1 -1
  237. package/dist/version.js +1 -1
  238. package/package.json +1 -1
  239. package/skills/belay/SKILL.md +8 -3
  240. package/skills/belay/belay-approve.md +10 -7
@@ -0,0 +1,55 @@
1
+ import type { CapabilityAction, CapabilityEvidenceLevel, CapabilityResource } from '../capability/request.js';
2
+ import type { VerdictOpacity } from '../verdict/types.js';
3
+ export declare const EFFECT_PLAN_VERSION: 1;
4
+ export type EffectTag = 'fs.read' | 'fs.write' | 'process.exec' | 'network.connect' | 'network.acquire' | 'git.ref.write' | 'secret.read' | 'control_plane.write' | 'read_only' | 'indeterminate';
5
+ export type LauncherPhase = 'invoke' | 'resolve' | 'acquire' | 'cache_read' | 'cache_write' | 'delegate';
6
+ export type PackageExecLauncher = 'npx' | 'npm' | 'pnpm';
7
+ export interface EffectEvidence {
8
+ level: CapabilityEvidenceLevel;
9
+ signals: readonly string[];
10
+ basis: readonly string[];
11
+ }
12
+ export interface EffectProvenance {
13
+ segment?: string;
14
+ launcher?: PackageExecLauncher;
15
+ phase?: LauncherPhase;
16
+ innerCommand?: string;
17
+ innerArgv?: readonly string[];
18
+ }
19
+ export interface EffectRequirement {
20
+ tag: EffectTag;
21
+ action: CapabilityAction;
22
+ resource: CapabilityResource;
23
+ evidence: EffectEvidence;
24
+ provenance: EffectProvenance;
25
+ /** All contributing sources after requirement de-duplication. */
26
+ provenances?: readonly EffectProvenance[];
27
+ }
28
+ export interface ExecEffectNode {
29
+ kind: 'exec';
30
+ commandRedacted: string;
31
+ segmentHead: string;
32
+ requirements: readonly EffectRequirement[];
33
+ }
34
+ export interface LauncherEffectNode {
35
+ kind: 'launcher';
36
+ launcher: PackageExecLauncher | 'npm' | 'pnpm' | 'make';
37
+ phase: LauncherPhase;
38
+ opaque: boolean;
39
+ reason: string;
40
+ children: readonly EffectNode[];
41
+ }
42
+ export interface MergeEffectNode {
43
+ kind: 'merge';
44
+ children: readonly EffectNode[];
45
+ }
46
+ export type EffectNode = ExecEffectNode | LauncherEffectNode | MergeEffectNode;
47
+ export interface EffectPlan {
48
+ version: typeof EFFECT_PLAN_VERSION;
49
+ root: EffectNode;
50
+ inputFingerprint: string;
51
+ opacity: VerdictOpacity;
52
+ disposition: 'effects' | 'effect_free';
53
+ completeness: 'complete' | 'partial';
54
+ signals: readonly string[];
55
+ }
@@ -0,0 +1 @@
1
+ export const EFFECT_PLAN_VERSION = 1;
@@ -3,5 +3,6 @@ import type { EgressAllowlistEntry, EgressAllowlistFile } from './types.js';
3
3
  export declare function egressAllowlistPath(config: BelayConfigV3, repoLocalStateDir: string): string;
4
4
  export declare function loadEgressAllowlist(filePath: string): Promise<EgressAllowlistFile>;
5
5
  export declare function saveEgressAllowlist(filePath: string, state: EgressAllowlistFile): Promise<void>;
6
+ export declare function mutateEgressAllowlist(filePath: string, mutate: (state: EgressAllowlistFile) => EgressAllowlistFile): Promise<EgressAllowlistFile>;
6
7
  export declare function isHostAllowlisted(host: string, allowlist: EgressAllowlistFile): boolean;
7
8
  export declare function addDomainToAllowlist(allowlist: EgressAllowlistFile, entry: EgressAllowlistEntry): EgressAllowlistFile;
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
+ import { withStateFileLock } from '../capability/approval-state-mutation.js';
4
5
  import { belayStateDir } from '../config.js';
5
6
  export function egressAllowlistPath(config, repoLocalStateDir) {
6
7
  return path.join(belayStateDir(config, repoLocalStateDir), 'egress-allowlist.json');
@@ -19,6 +20,13 @@ export async function saveEgressAllowlist(filePath, state) {
19
20
  await mkdir(path.dirname(filePath), { recursive: true });
20
21
  await writeFile(filePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
21
22
  }
23
+ export async function mutateEgressAllowlist(filePath, mutate) {
24
+ return withStateFileLock(filePath, async () => {
25
+ const updated = mutate(await loadEgressAllowlist(filePath));
26
+ await saveEgressAllowlist(filePath, updated);
27
+ return updated;
28
+ });
29
+ }
22
30
  export function isHostAllowlisted(host, allowlist) {
23
31
  const normalized = host.toLowerCase();
24
32
  return allowlist.domains.some((entry) => entry.host.toLowerCase() === normalized);
@@ -1,23 +1,15 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { compactApprovals, createApprovalRecord } from './approval.js';
3
3
  import { issueApprovalToken } from './approval-token.js';
4
+ import { mutateApprovalStateWithRetry } from './capability/approval-state-mutation.js';
4
5
  import { configuredControlPlaneDir } from './config.js';
5
- import { addDomainToAllowlist, loadEgressAllowlist, saveEgressAllowlist, } from './egress/allowlist.js';
6
+ import { addDomainToAllowlist, mutateEgressAllowlist } from './egress/allowlist.js';
6
7
  import { parseHostFromSummary } from './egress/fingerprint.js';
7
8
  import { notifyDeny } from './notify.js';
8
9
  export async function ensurePendingEgressApproval(params) {
9
10
  const { config, repoRoot, policyResult, store } = params;
10
- const pending = await store.loadPending();
11
- pending.state = compactApprovals(pending.state);
12
- const existing = pending.state.approvals.find((approval) => approval.kind === 'egress' &&
13
- approval.fingerprint === policyResult.fingerprint &&
14
- approval.repoRoot === repoRoot);
15
- if (existing) {
16
- await store.writePending(pending.filePath, pending.state);
17
- return { approvalId: existing.approvalId, approval: existing, created: false };
18
- }
19
11
  const approvalId = `belay_${randomUUID().replaceAll('-', '').slice(0, 12)}`;
20
- const approval = createApprovalRecord({
12
+ const candidate = createApprovalRecord({
21
13
  kind: 'egress',
22
14
  fingerprint: policyResult.fingerprint,
23
15
  repoRoot,
@@ -26,44 +18,47 @@ export async function ensurePendingEgressApproval(params) {
26
18
  approvalTtlMinutes: config.approvalTtlMinutes,
27
19
  approvalId,
28
20
  });
29
- pending.state.approvals.push(approval);
30
- await store.writePending(pending.filePath, pending.state);
31
- return { approvalId, approval, created: true };
32
- }
33
- const consumeLocks = new Map();
34
- async function withConsumeLock(key, fn) {
35
- const previous = consumeLocks.get(key) ?? Promise.resolve();
36
- let release;
37
- const gate = new Promise((resolve) => {
38
- release = resolve;
21
+ const outcome = await mutateApprovalStateWithRetry({
22
+ load: store.loadPending,
23
+ write: store.writePending,
24
+ mutate: (state) => {
25
+ const compacted = compactApprovals(state);
26
+ const existing = compacted.approvals.find((approval) => approval.kind === 'egress' &&
27
+ approval.fingerprint === policyResult.fingerprint &&
28
+ approval.repoRoot === repoRoot);
29
+ if (existing) {
30
+ return {
31
+ state: compacted,
32
+ result: { approvalId: existing.approvalId, approval: existing, created: false },
33
+ };
34
+ }
35
+ compacted.approvals.push(candidate);
36
+ return {
37
+ state: compacted,
38
+ result: { approvalId, approval: candidate, created: true },
39
+ };
40
+ },
39
41
  });
40
- consumeLocks.set(key, previous.then(() => gate));
41
- await previous;
42
- try {
43
- return await fn();
44
- }
45
- finally {
46
- release();
47
- if (consumeLocks.get(key) === gate) {
48
- consumeLocks.delete(key);
49
- }
42
+ if (!outcome) {
43
+ throw new Error('Failed to persist pending egress approval');
50
44
  }
45
+ return outcome;
51
46
  }
52
47
  export async function consumeApprovedEgress(params) {
53
- const lockKey = `${params.repoRoot}:${params.fingerprint}`;
54
- return withConsumeLock(lockKey, async () => {
55
- const approved = await params.store.loadApproved();
56
- approved.state = compactApprovals(approved.state);
57
- const index = approved.state.approvals.findIndex((approval) => approval.kind === 'egress' &&
58
- approval.fingerprint === params.fingerprint &&
59
- approval.repoRoot === params.repoRoot);
60
- if (index === -1) {
61
- await params.store.writeApproved(approved.filePath, approved.state);
62
- return null;
63
- }
64
- const [approval] = approved.state.approvals.splice(index, 1);
65
- await params.store.writeApproved(approved.filePath, approved.state);
66
- return approval;
48
+ return mutateApprovalStateWithRetry({
49
+ load: params.store.loadApproved,
50
+ write: params.store.writeApproved,
51
+ mutate: (state) => {
52
+ const compacted = compactApprovals(state);
53
+ const index = compacted.approvals.findIndex((approval) => approval.kind === 'egress' &&
54
+ approval.fingerprint === params.fingerprint &&
55
+ approval.repoRoot === params.repoRoot);
56
+ if (index === -1) {
57
+ return null;
58
+ }
59
+ const [approval] = compacted.approvals.splice(index, 1);
60
+ return { state: compacted, result: approval ?? null };
61
+ },
67
62
  });
68
63
  }
69
64
  export async function notifyEgressDeny(params) {
@@ -98,8 +93,8 @@ export async function recordEgressApproval(params) {
98
93
  const { recordApproval } = await import('./approval-service.js');
99
94
  const pending = await params.store.loadPending();
100
95
  const match = pending.state.approvals.find((approval) => approval.approvalId === params.approvalId);
101
- const host = match ? parseHostFromSummary(match.summary) : null;
102
- if (params.scope === 'domain' && match && !host) {
96
+ const pendingHost = match ? parseHostFromSummary(match.summary) : null;
97
+ if (params.scope === 'domain' && match && !pendingHost) {
103
98
  return {
104
99
  ok: false,
105
100
  message: `Cannot add domain to egress allowlist: could not parse host from summary "${match.summary}".`,
@@ -112,16 +107,15 @@ export async function recordEgressApproval(params) {
112
107
  requireSignedToken: params.requireSignedToken ?? false,
113
108
  store: params.store,
114
109
  });
110
+ const host = result.approval ? parseHostFromSummary(result.approval.summary) : pendingHost;
115
111
  if (!result.ok || params.scope !== 'domain' || !host) {
116
112
  return { ok: result.ok, message: result.message };
117
113
  }
118
- const allowlist = await loadEgressAllowlist(params.store.allowlistPath);
119
- const updated = addDomainToAllowlist(allowlist, {
114
+ await mutateEgressAllowlist(params.store.allowlistPath, (allowlist) => addDomainToAllowlist(allowlist, {
120
115
  host,
121
116
  approvedAt: new Date().toISOString(),
122
117
  approvalId: params.approvalId,
123
- });
124
- await saveEgressAllowlist(params.store.allowlistPath, updated);
118
+ }));
125
119
  return {
126
120
  ok: true,
127
121
  message: `${result.message} Domain ${host} added to egress allowlist.`,
@@ -29,6 +29,11 @@ export interface GateVerdict extends GatePermissionResponse {
29
29
  wouldBlock: boolean;
30
30
  mode: 'enforce' | 'audit';
31
31
  axes?: ClassifyResult['axes'];
32
+ capabilityRequests?: ClassifyResult['capabilityRequests'];
33
+ authorizationDecision?: ClassifyResult['authorizationDecision'];
34
+ effectPlan?: ClassifyResult['effectPlan'];
35
+ effectPlanPolicyDecisions?: ClassifyResult['effectPlanPolicyDecisions'];
36
+ boundaryProfile?: string;
32
37
  }
33
38
  export declare function isGatedAction(value: unknown): value is GatedAction;
34
39
  export declare function classifyResultToGateVerdict(params: {
@@ -26,6 +26,11 @@ export function classifyResultToGateVerdict(params) {
26
26
  user_message,
27
27
  agent_message,
28
28
  axes: result.axes,
29
+ capabilityRequests: result.capabilityRequests,
30
+ authorizationDecision: result.authorizationDecision,
31
+ effectPlan: result.effectPlan,
32
+ effectPlanPolicyDecisions: result.effectPlanPolicyDecisions,
33
+ boundaryProfile: result.boundaryProfile,
29
34
  };
30
35
  }
31
36
  export function unnormalizedGateVerdict(params) {
@@ -1,8 +1,10 @@
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';
5
6
  import { classifierOptionsFromConfig } from './config.js';
7
+ import { buildCapabilityEffectPlan } from './effect-ir/build.js';
6
8
  import { GATE_CONTRACT_VERSION } from './gate-contract.js';
7
9
  import { mergeAgentAssessment } from './judgment.js';
8
10
  import { classifyShell } from './verdict/adapter.js';
@@ -197,6 +199,10 @@ function applyToolSandboxPolicies(action, result, options) {
197
199
  return next;
198
200
  }
199
201
  export async function classifyGatedAction(action, config, extraOptions = {}) {
202
+ const limitResult = checkGatedActionLimits(action);
203
+ if (limitResult) {
204
+ return attachEffectPlan(action, limitResult);
205
+ }
200
206
  const options = { ...classifierOptionsFromConfig(config), ...extraOptions };
201
207
  if (action.kind === 'shell') {
202
208
  const command = action.command ?? shellCommandFromPayload(action.payload ?? {});
@@ -207,36 +213,52 @@ export async function classifyGatedAction(action, config, extraOptions = {}) {
207
213
  result = applySandboxOutsideBoundary(command, action, result, options);
208
214
  result = applyShellPeripheralPolicy(command, action, result, options);
209
215
  if (!action.agentAssessment) {
210
- return result;
216
+ return attachEffectPlan(action, result);
211
217
  }
212
218
  const merged = mergeAgentAssessment(result.assessment, action.agentAssessment);
213
219
  if (!merged.mismatch) {
214
- return { ...result, assessment: merged.assessment };
220
+ return attachEffectPlan(action, { ...result, assessment: merged.assessment });
215
221
  }
216
- return {
222
+ return attachEffectPlan(action, {
217
223
  ...result,
218
224
  verdict: 'deny_pending_approval',
219
225
  reason: 'agent_assessment_mismatch',
220
226
  assessment: merged.assessment,
221
- };
227
+ });
222
228
  }
223
229
  if (action.kind === 'subagent') {
224
- return classifySubagent(action.payload ?? {}, action.repoRoot, options);
230
+ return attachEffectPlan(action, classifySubagent(action.payload ?? {}, action.repoRoot, options, config));
225
231
  }
226
232
  let result = await classifyToolUse(action.payload ?? {}, action.repoRoot, action.cwd, config, options);
227
233
  result = applyToolSandboxPolicies(action, result, options);
228
234
  if (!action.agentAssessment) {
229
- return result;
235
+ return attachEffectPlan(action, result);
230
236
  }
231
237
  const merged = mergeAgentAssessment(result.assessment, action.agentAssessment);
232
238
  if (!merged.mismatch) {
233
- return { ...result, assessment: merged.assessment };
239
+ return attachEffectPlan(action, { ...result, assessment: merged.assessment });
234
240
  }
235
- return {
241
+ return attachEffectPlan(action, {
236
242
  ...result,
237
243
  verdict: 'deny_pending_approval',
238
244
  reason: 'agent_assessment_mismatch',
239
245
  assessment: merged.assessment,
246
+ });
247
+ }
248
+ function attachEffectPlan(action, result) {
249
+ if (result.effectPlan) {
250
+ return result;
251
+ }
252
+ const summary = result.normalizedCommand ?? result.summary ?? action.command ?? action.kind;
253
+ return {
254
+ ...result,
255
+ effectPlan: buildCapabilityEffectPlan({
256
+ actionKind: action.kind,
257
+ summary,
258
+ inputFingerprint: result.fingerprint,
259
+ requests: result.capabilityRequests ?? [],
260
+ effectFree: result.reason === 'read_only',
261
+ }),
240
262
  };
241
263
  }
242
264
  export async function classifyGatedActionAsync(action, config, extraOptions = {}) {
@@ -3,7 +3,7 @@ export type { ApprovalReplayHint, ReplayActionContext, ReplayAdapterId, } from '
3
3
  /** @deprecated Use `buildRetryInstructionForConfig` */
4
4
  export { approvalFlow, buildApprovalRecordedMessage, buildReplayEnvelopeFields, buildReplayHint, buildRetryInstructionForConfig, buildRetryInstructionForConfig as buildRetryInstruction, canAutoReplay, getExecutionLeaseMs, replayShellCommand, validateReplayEnvelope, } from './approval-replay.js';
5
5
  export type { ApprovalStore } from './approval-service.js';
6
- export { consumeApprovedAfterCliReplay, createGateApprovalStore, gateApprovalStoreFromDeps, recordApproval, } from './approval-service.js';
6
+ export { claimApprovedForReplay, consumeApprovedAfterCliReplay, createGateApprovalStore, gateApprovalStoreFromDeps, recordApproval, } from './approval-service.js';
7
7
  export type { AuditMetricsReport } from './audit-metrics.js';
8
8
  export { computeAuditMetrics, parseAuditNdjson } from './audit-metrics.js';
9
9
  export { classifySubagent } from './classify-subagent.js';
@@ -16,6 +16,8 @@ export { classifyResultToGateVerdict, GATE_CONTRACT_VERSION, isGatedAction, unno
16
16
  export { classifyGatedAction, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from './gate-engine.js';
17
17
  export { matchesSensitivePath } from './glob.js';
18
18
  export { canonicalPath, hasOutsideRepoPath, normalizeToken, pathWithinRoot, relativeWithinRepo, resolveMutationTarget, } from './path-utils.js';
19
+ export type { RecoveryBackend, RecoveryCheckpointEntry, RecoveryCheckpointEntryV1, RecoveryCheckpointEntryV2, RecoveryCheckpointManifest, RecoveryCheckpointManifestV1, RecoveryCheckpointManifestV2, RecoveryCheckpointState, RecoveryCheckpointSummary, RecoveryFileSnapshotV1, RecoveryFileSnapshotV2, RecoveryProofV1, RecoveryReceiptV1, } from './recovery/index.js';
20
+ export { listRecoveryCheckpoints, RECOVERY_CHECKPOINT_CORRUPT, RECOVERY_CHECKPOINT_QUOTA, RECOVERY_RESTORE_CONFLICT, recoveryCheckpointStorageBytes, recoveryRestoreBinding, restoreRecoveryCheckpoint, showRecoveryCheckpoint, } from './recovery/index.js';
19
21
  export { fingerprintReplayPayload, subagentFingerprintSource } from './replay-scrub.js';
20
22
  export { scrubString, scrubValue } from './scrub.js';
21
23
  export { findCommandSubstitutions, MAX_SUBSTITUTION_DEPTH } from './shell-substitution.js';
@@ -1,7 +1,7 @@
1
1
  export { APPROVAL_EXECUTION_LEASE_MS, approvalCommandMatch, compactApprovals, createApprovalRecord, createApprovalRecordWithEnvelope, isExecutionLeaseExpired, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
2
2
  /** @deprecated Use `buildRetryInstructionForConfig` */
3
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';
4
+ export { claimApprovedForReplay, consumeApprovedAfterCliReplay, createGateApprovalStore, gateApprovalStoreFromDeps, recordApproval, } from './approval-service.js';
5
5
  export { computeAuditMetrics, parseAuditNdjson } from './audit-metrics.js';
6
6
  export { classifySubagent } from './classify-subagent.js';
7
7
  export { classifyToolUse } from './classify-tool.js';
@@ -12,6 +12,7 @@ export { classifyResultToGateVerdict, GATE_CONTRACT_VERSION, isGatedAction, unno
12
12
  export { classifyGatedAction, GateNormalizationError, gateEnabledForAction, normalizeGatedAction, } from './gate-engine.js';
13
13
  export { matchesSensitivePath } from './glob.js';
14
14
  export { canonicalPath, hasOutsideRepoPath, normalizeToken, pathWithinRoot, relativeWithinRepo, resolveMutationTarget, } from './path-utils.js';
15
+ export { listRecoveryCheckpoints, RECOVERY_CHECKPOINT_CORRUPT, RECOVERY_CHECKPOINT_QUOTA, RECOVERY_RESTORE_CONFLICT, recoveryCheckpointStorageBytes, recoveryRestoreBinding, restoreRecoveryCheckpoint, showRecoveryCheckpoint, } from './recovery/index.js';
15
16
  export { fingerprintReplayPayload, subagentFingerprintSource } from './replay-scrub.js';
16
17
  export { scrubString, scrubValue } from './scrub.js';
17
18
  export { findCommandSubstitutions, MAX_SUBSTITUTION_DEPTH } from './shell-substitution.js';
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { loadApprovalState, saveApprovalState } from '../config-io.js';
2
+ import { loadApprovalState, pendingApprovalsPath, saveApprovalState } from '../config-io.js';
3
3
  import { compactApprovals, createApprovalRecord } from './approval.js';
4
+ import { mutateApprovalStateWithRetry } from './capability/approval-state-mutation.js';
4
5
  import { JUDGE_CLOUD_CONSENT_REASON } from './capability/reasons.js';
5
6
  export { JUDGE_CLOUD_CONSENT_REASON };
6
7
  export function judgeCloudConsentFingerprint(providerId, endpoint) {
@@ -10,18 +11,8 @@ export async function ensurePendingJudgeCloudConsentApproval(params) {
10
11
  const { repoRoot, config, providerId, endpoint } = params;
11
12
  const normalizedEndpoint = endpoint.trim();
12
13
  const fingerprint = judgeCloudConsentFingerprint(providerId, normalizedEndpoint);
13
- const pending = await loadApprovalState(repoRoot, 'pending-approvals.json', config);
14
- const compacted = compactApprovals(pending);
15
- const existing = compacted.approvals.find((approval) => approval.kind === 'capability' &&
16
- approval.reason === JUDGE_CLOUD_CONSENT_REASON &&
17
- approval.fingerprint === fingerprint &&
18
- approval.repoRoot === repoRoot);
19
- if (existing) {
20
- await saveApprovalState(repoRoot, 'pending-approvals.json', compacted, config);
21
- return { approvalId: existing.approvalId, created: false };
22
- }
23
14
  const approvalId = `belay_${randomUUID().replaceAll('-', '').slice(0, 12)}`;
24
- const approval = createApprovalRecord({
15
+ const candidate = createApprovalRecord({
25
16
  kind: 'capability',
26
17
  fingerprint,
27
18
  repoRoot,
@@ -32,7 +23,30 @@ export async function ensurePendingJudgeCloudConsentApproval(params) {
32
23
  input: `belay judge use ${providerId} --endpoint ${normalizedEndpoint}`,
33
24
  inputKind: 'shell',
34
25
  });
35
- compacted.approvals.push(approval);
36
- await saveApprovalState(repoRoot, 'pending-approvals.json', compacted, config);
37
- return { approvalId, created: true };
26
+ const outcome = await mutateApprovalStateWithRetry({
27
+ load: async () => ({
28
+ filePath: pendingApprovalsPath(repoRoot, config),
29
+ state: await loadApprovalState(repoRoot, 'pending-approvals.json', config),
30
+ }),
31
+ write: async (_filePath, state) => saveApprovalState(repoRoot, 'pending-approvals.json', state, config),
32
+ mutate: (state) => {
33
+ const compacted = compactApprovals(state);
34
+ const existing = compacted.approvals.find((approval) => approval.kind === 'capability' &&
35
+ approval.reason === JUDGE_CLOUD_CONSENT_REASON &&
36
+ approval.fingerprint === fingerprint &&
37
+ approval.repoRoot === repoRoot);
38
+ if (existing) {
39
+ return {
40
+ state: compacted,
41
+ result: { approvalId: existing.approvalId, created: false },
42
+ };
43
+ }
44
+ compacted.approvals.push(candidate);
45
+ return { state: compacted, result: { approvalId, created: true } };
46
+ },
47
+ });
48
+ if (!outcome) {
49
+ throw new Error('Failed to persist cloud judge consent approval');
50
+ }
51
+ return outcome;
38
52
  }
@@ -2,20 +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';
8
9
  import { stopJudgeBrokerDaemon } from './verdict/judge-broker-service.js';
9
10
  import { getJudgeProviderCapabilities, getJudgeProviderSpec, isRemovedProviderId, normalizeLegacyProviderId, } from './verdict/judge-catalog.js';
10
11
  import { createJudgeFromConfig, resolveJudgeModel } from './verdict/judge-factory.js';
12
+ import { resolveJudgeSmokeProbeTimeoutMs } from './verdict/judge-runtime-config.js';
11
13
  import { listRepoJudgeSessionBrokers, stopRepoJudgeSessionBroker, } from './verdict/judge-session-broker.js';
12
14
  import { clearJudgeSessionKillSwitch } from './verdict/judge-session-kill-switch.js';
15
+ function judgeShadowAdvisory(warnings, message) {
16
+ warnings.push(`[judge shadow advisory] ${message}`);
17
+ }
13
18
  export async function diagnoseJudge(config, repoRoot = process.cwd(), options = {}) {
14
19
  const shouldRunLiveProbe = options.liveProbe === true && !process.env.VITEST && !process.env.VITEST_WORKER_ID;
15
20
  const issues = [];
16
21
  const warnings = [];
17
22
  const notes = [];
18
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
+ }
19
29
  const provider = normalizeJudgeProvider(judge.provider);
20
30
  const rawProviderId = judge.providerId ? String(judge.providerId) : undefined;
21
31
  if (rawProviderId && isRemovedProviderId(rawProviderId)) {
@@ -36,15 +46,19 @@ export async function diagnoseJudge(config, repoRoot = process.cwd(), options =
36
46
  const capabilities = getJudgeProviderCapabilities(providerId);
37
47
  const transport = resolveJudgeTransport(judge);
38
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;
39
53
  notes.push(`Judge providerId: ${providerId}`);
40
54
  notes.push(`Judge driver: ${provider}`);
41
55
  notes.push(`Judge model requested: ${judge.model}`);
42
- notes.push(`Judge transport: ${transport}`);
56
+ notes.push(`Judge transport: ${effectiveTransport}`);
43
57
  if (config.policy.modelAssist.enabled) {
44
58
  warnings.push('policy.modelAssist is enabled but is not wired to v2 Tier1. Use top-level judge instead.');
45
59
  }
46
60
  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.');
61
+ judgeShadowAdvisory(warnings, 'Cloud judge consent is not recorded. Shadow cloud judge will fail until consent is granted.');
48
62
  }
49
63
  else if (judge.cloudConsent?.accepted) {
50
64
  notes.push(`Cloud consent: accepted ${judge.cloudConsent.at} by ${judge.cloudConsent.by}`);
@@ -74,12 +88,12 @@ export async function diagnoseJudge(config, repoRoot = process.cwd(), options =
74
88
  if (providerId === 'ollama') {
75
89
  notes.push(`Ollama endpoint: ${endpoint}`);
76
90
  if (discovery.modelIds.length === 0) {
77
- 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.');
78
92
  }
79
93
  else {
80
94
  const hasModel = modelCheck.status === 'found';
81
95
  if (!hasModel) {
82
- 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.`);
83
97
  }
84
98
  else {
85
99
  notes.push(`Ollama model "${judge.model}" is available.`);
@@ -103,7 +117,7 @@ export async function diagnoseJudge(config, repoRoot = process.cwd(), options =
103
117
  context: { cwd: process.cwd(), repoRoot: process.cwd() },
104
118
  });
105
119
  if (warmResult.reason === 'ollama_unavailable' || warmResult.reason === 'ollama_parse_error') {
106
- issues.push(`Ollama warm call failed: ${warmResult.reason}`);
120
+ judgeShadowAdvisory(warnings, `Ollama warm call failed: ${warmResult.reason}`);
107
121
  }
108
122
  else {
109
123
  notes.push('Ollama warm call succeeded.');
@@ -111,24 +125,29 @@ export async function diagnoseJudge(config, repoRoot = process.cwd(), options =
111
125
  return { issues, warnings, notes, modelCheck };
112
126
  }
113
127
  if (transport === 'unavailable') {
114
- 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.');
115
129
  return { issues, warnings, notes, modelCheck };
116
130
  }
117
131
  if (transport.endsWith('-cli')) {
118
132
  if (!runtime.cliTransport) {
119
- 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.`);
120
134
  }
121
135
  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.');
136
+ judgeShadowAdvisory(warnings, 'Judge API key is not set for the configured credential mode. Shadow cloud judge will not run.');
123
137
  }
124
138
  else {
125
- 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
+ }
126
145
  if (shouldRunLiveProbe) {
127
146
  const smokeConfig = {
128
147
  ...config,
129
148
  judge: {
130
149
  ...judge,
131
- timeoutMs: Math.min(judge.timeoutMs, 5000),
150
+ timeoutMs: resolveJudgeSmokeProbeTimeoutMs(judge.timeoutMs),
132
151
  },
133
152
  };
134
153
  const smokeJudge = createJudgeFromConfig(smokeConfig, { repoRoot });
@@ -140,10 +159,16 @@ export async function diagnoseJudge(config, repoRoot = process.cwd(), options =
140
159
  smokeJudge.lastTrace?.fallbackReason ??
141
160
  smokeResult.reason;
142
161
  if (smokeJudge.lastTrace?.provider === 'fallback') {
143
- issues.push(`Native CLI transport smoke probe failed (${smokeFallback}). Tier1 will fail closed to ask.`);
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}).`);
144
167
  }
145
168
  else {
146
- notes.push('Native CLI transport smoke probe succeeded.');
169
+ notes.push(cursorAcpEnabled
170
+ ? 'Cursor ACP smoke probe succeeded.'
171
+ : 'Native CLI transport smoke probe succeeded.');
147
172
  }
148
173
  }
149
174
  }
@@ -153,7 +178,7 @@ export async function diagnoseJudge(config, repoRoot = process.cwd(), options =
153
178
  notes.push(`HTTP endpoint: ${judge.endpoint}`);
154
179
  }
155
180
  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.');
181
+ judgeShadowAdvisory(warnings, 'Judge API key is not set for the configured credential mode. Shadow cloud judge will not run.');
157
182
  }
158
183
  else {
159
184
  notes.push(`Credential source: ${keyInfo.source ?? keyInfo.sourceKind}`);
@@ -188,7 +213,7 @@ export async function diagnoseJudge(config, repoRoot = process.cwd(), options =
188
213
  });
189
214
  if (dryRun.reason.startsWith('openai_compatible_') ||
190
215
  dryRun.reason === 'outbound_scrub_failed') {
191
- issues.push(`HTTP judge dry-run failed: ${dryRun.reason}`);
216
+ judgeShadowAdvisory(warnings, `HTTP judge dry-run failed: ${dryRun.reason}`);
192
217
  }
193
218
  else {
194
219
  notes.push('HTTP judge dry-run succeeded.');
@@ -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
+ };