@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
package/dist/cli.js CHANGED
@@ -10,8 +10,10 @@ import { formatHarvestReport, harvestApplyProject, harvestListProject } from './
10
10
  import { formatMetricsReport, metricsProject } from './commands/metrics.js';
11
11
  import { formatQualityReport, qualityCheck } from './commands/quality.js';
12
12
  import { formatRecoverReport, recoverProject } from './commands/recover.js';
13
+ import { formatRecoveryCheckpointResult, recoveryCheckpointCommand, } from './commands/recovery-checkpoints.js';
13
14
  import { formatReport, reportProject } from './commands/report.js';
14
15
  import { revokeApproval } from './commands/revoke.js';
16
+ import { formatSessionStatusReport, sessionStartProject, sessionStatusProject, } from './commands/session.js';
15
17
  import { formatSimulateReport, simulateProject } from './commands/simulate.js';
16
18
  import { revokeStandingAllow } from './commands/standing-allow.js';
17
19
  import { formatStatusReport, statusProject } from './commands/status.js';
@@ -381,7 +383,13 @@ function parseArgs(argv) {
381
383
  return { command: 'help', options };
382
384
  }
383
385
  if (token === '--') {
384
- options.explainCommand = rest.slice(index + 1).join(' ');
386
+ const remainder = rest.slice(index + 1).join(' ');
387
+ if (command === 'session' && options.sessionSubcommand === 'start') {
388
+ options.sessionAgentCommand = remainder;
389
+ }
390
+ else {
391
+ options.explainCommand = remainder;
392
+ }
385
393
  break;
386
394
  }
387
395
  if (command === 'audit' && !options.auditSubcommand) {
@@ -405,6 +413,13 @@ function parseArgs(argv) {
405
413
  }
406
414
  throw new Error('sandbox requires subcommand: status');
407
415
  }
416
+ if (command === 'session' && !options.sessionSubcommand) {
417
+ if (token === 'start' || token === 'status') {
418
+ options.sessionSubcommand = token;
419
+ continue;
420
+ }
421
+ throw new Error('session requires subcommand: start or status');
422
+ }
408
423
  if (command === 'standing-allow' && !options.standingAllowSubcommand) {
409
424
  if (token === 'revoke') {
410
425
  options.standingAllowSubcommand = token;
@@ -419,6 +434,23 @@ function parseArgs(argv) {
419
434
  }
420
435
  throw new Error('harvest requires subcommand: list or apply');
421
436
  }
437
+ if (command === 'recover' && !options.recoverSubcommand) {
438
+ if (token === 'advice' ||
439
+ token === 'status' ||
440
+ token === 'list' ||
441
+ token === 'show' ||
442
+ token === 'apply') {
443
+ options.recoverSubcommand = token;
444
+ continue;
445
+ }
446
+ throw new Error('recover requires subcommand: advice, status, list, show, or apply');
447
+ }
448
+ if (command === 'recover' &&
449
+ (options.recoverSubcommand === 'show' || options.recoverSubcommand === 'apply') &&
450
+ !options.recoverCheckpointId) {
451
+ options.recoverCheckpointId = token;
452
+ continue;
453
+ }
422
454
  if (command === 'judge' && !options.judgeSubcommand) {
423
455
  if (token === 'status' ||
424
456
  token === 'list' ||
@@ -513,14 +545,18 @@ Usage:
513
545
  ${c} metrics [--target <dir>] [--json]
514
546
  ${c} quality [--target <dir>] [--corpus <path>] [--json]
515
547
  ${c} report [--target <dir>] [--since <iso>] [--until <iso>] [--limit <n>] [--json]
516
- ${c} recover [--target <dir>] [--since <iso>] [--fingerprint <fp>] [--command "<text>"] [--limit <n>] [--json]
548
+ ${c} recover [advice] [--target <dir>] [--since <iso>] [--fingerprint <fp>] [--command "<text>"] [--limit <n>] [--json]
517
549
  (--limit picks the Nth recover candidate after priority ranking: local_mutation first, then recency; 1 = highest priority, default 1)
550
+ ${c} recover <status|list> [--target <dir>] [--json]
551
+ ${c} recover <show|apply> <checkpoint-id> [--target <dir>] [--json]
518
552
  ${c} audit <query|summarize|replay> [--target <dir>] [--json] [--since <iso>] [--until <iso>] [--verdict <v>] [--reason <r>] [--kind <k>] [--fingerprint <fp>] [--event <e>] [--location <v>] [--opacity <v>] [--effect <v>] [--confidence <v>] [--limit <n>] [--config <path>]
519
553
  ${c} simulate --config <path> [--target <dir>] [--json]
520
554
  ${c} status [--target <dir>] [--json]
521
555
  ${c} explain [--target <dir>] [--cwd <dir>] [--kind shell|tool|subagent] [--tool <name>] [--payload-json <json>] [--command <text>] [--json] [-- <command>]
522
556
  ${c} egress <start|stop|status|env> [--target <dir>] [--json]
523
557
  ${c} sandbox status [--target <dir>] [--json]
558
+ ${c} session <start|status> [--target <dir>] [--json]
559
+ ${c} session start [--target <dir>] [-- <agent command>]
524
560
  ${c} judge <status|list|use|test|bench|consent> [--target <dir>] [--json]
525
561
  ${c} judge test [--target <dir>] [--json] [--live-probe]
526
562
  ${c} judge use <ollama|codex|claude|cursor> [--model <id>] [--endpoint <url>] [--timeout <ms>] [--accept-cloud] [--cloud-consent-approval-id <id>] [--credential project|apiKey] [--key-stdin] [--key-env <NAME>]
@@ -809,6 +845,22 @@ async function main() {
809
845
  throw new Error('harvest requires subcommand: list or apply');
810
846
  }
811
847
  if (command === 'recover') {
848
+ if (options.recoverSubcommand && options.recoverSubcommand !== 'advice') {
849
+ const result = await recoveryCheckpointCommand({
850
+ targetDir: options.targetDir,
851
+ subcommand: options.recoverSubcommand,
852
+ checkpointId: options.recoverCheckpointId,
853
+ });
854
+ if (options.json) {
855
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
856
+ }
857
+ else {
858
+ process.stdout.write(formatRecoveryCheckpointResult(result));
859
+ }
860
+ if (!result.ok)
861
+ process.exitCode = 2;
862
+ return;
863
+ }
812
864
  const report = await recoverProject({
813
865
  targetDir: options.targetDir,
814
866
  since: options.since,
@@ -907,6 +959,32 @@ async function main() {
907
959
  process.exitCode = report.issues.length > 0 && report.sandboxEnabled ? 1 : 0;
908
960
  return;
909
961
  }
962
+ if (command === 'session') {
963
+ if (!options.sessionSubcommand) {
964
+ throw new Error('session requires subcommand: start or status');
965
+ }
966
+ if (options.sessionSubcommand === 'start') {
967
+ const result = await sessionStartProject({
968
+ targetDir: options.targetDir,
969
+ agentCommand: options.sessionAgentCommand,
970
+ });
971
+ process.stdout.write(`${result.message}\n`);
972
+ process.exitCode = result.ok ? 0 : 1;
973
+ return;
974
+ }
975
+ const report = await sessionStatusProject({
976
+ targetDir: options.targetDir,
977
+ json: options.json,
978
+ });
979
+ if (options.json) {
980
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
981
+ }
982
+ else {
983
+ process.stdout.write(`${formatSessionStatusReport(report)}\n`);
984
+ }
985
+ process.exitCode = report.ok ? 0 : 1;
986
+ return;
987
+ }
910
988
  if (command === 'approve') {
911
989
  if (!options.approvalId) {
912
990
  throw new Error('approve requires an approval ID.');
@@ -1,10 +1,12 @@
1
1
  import path from 'node:path';
2
2
  import { loadApprovalState, loadConfigFile } from '../config-io.js';
3
- import { canAutoReplay, getExecutionLeaseMs, replayShellCommand } from '../core/approval-replay.js';
4
- import { consumeApprovedAfterCliReplay, createGateApprovalStore, recordApproval, } from '../core/approval-service.js';
3
+ import { canAutoReplay, getExecutionLeaseMs } from '../core/approval-replay.js';
4
+ import { claimApprovedForReplay, createGateApprovalStore, recordApproval, } from '../core/approval-service.js';
5
+ import { runBoundaryAgentCommand } from '../core/capability/boundary-session.js';
5
6
  import { JUDGE_CLOUD_CONSENT_REASON } from '../core/capability/reasons.js';
6
7
  import { recordCapabilityApproval } from '../core/capability-approval.js';
7
8
  import { recordEgressApproval } from '../core/egress-approval.js';
9
+ import { RECOVERY_RESTORE_REASON } from '../core/recovery/checkpoint.js';
8
10
  import { createEgressApprovalStore } from '../services/egress-service.js';
9
11
  import { createCapabilityApprovalStore } from '../services/sandbox-service.js';
10
12
  export async function approvePending(options) {
@@ -12,6 +14,19 @@ export async function approvePending(options) {
12
14
  const config = await loadConfigFile(repoRoot);
13
15
  const pending = await loadApprovalState(repoRoot, 'pending-approvals.json', config);
14
16
  const match = pending.approvals.find((approval) => approval.approvalId === options.approvalId);
17
+ if (match?.reason === RECOVERY_RESTORE_REASON) {
18
+ if (options.scope && options.scope !== 'once') {
19
+ return { ok: false, message: 'Recovery restore approvals support one-shot scope only.' };
20
+ }
21
+ const result = await recordApproval({
22
+ approvalId: options.approvalId,
23
+ config,
24
+ token: options.token,
25
+ requireSignedToken: true,
26
+ store: createGateApprovalStore(repoRoot, config),
27
+ });
28
+ return { ok: result.ok, message: result.message };
29
+ }
15
30
  if (match?.reason === JUDGE_CLOUD_CONSENT_REASON) {
16
31
  const result = await recordCapabilityApproval({
17
32
  approvalId: options.approvalId,
@@ -73,22 +88,31 @@ export async function approvePending(options) {
73
88
  message: 'CLI replay is only supported for shell approvals. Retry the original tool or subagent action manually.',
74
89
  };
75
90
  }
76
- const replayResult = await replayShellCommand(approval.input, approval.cwd ?? repoRoot, getExecutionLeaseMs(config));
77
- const output = [replayResult.stdout, replayResult.stderr].filter(Boolean).join('\n').trim();
91
+ const claimed = await claimApprovedForReplay({
92
+ approvalId: options.approvalId,
93
+ store: approvalStore,
94
+ });
95
+ if (!claimed) {
96
+ return { ok: false, message: 'Approval could not be claimed for replay.' };
97
+ }
98
+ const replayResult = await runBoundaryAgentCommand({
99
+ repoRoot,
100
+ config,
101
+ command: claimed.input ?? approval.input,
102
+ cwd: claimed.cwd ?? repoRoot,
103
+ timeoutMs: getExecutionLeaseMs(config),
104
+ runOptions: { mountReadOnly: false },
105
+ });
78
106
  if (replayResult.exitCode === 0) {
79
- await consumeApprovedAfterCliReplay({
80
- approvalId: options.approvalId,
81
- store: approvalStore,
82
- });
83
107
  return {
84
108
  ok: true,
85
- message: `Belay replay succeeded for ${options.approvalId}. Do not retry via hooks; the one-shot grant was consumed.${output ? `\n${output}` : ''}`,
109
+ message: `Belay replay succeeded for ${options.approvalId}. Do not retry via hooks; the one-shot grant was consumed.`,
86
110
  };
87
111
  }
88
112
  const timeoutNote = replayResult.timedOut ? ' Replay timed out.' : '';
89
113
  return {
90
114
  ok: false,
91
- message: `Belay replay failed for ${options.approvalId} (exit ${replayResult.exitCode}).${timeoutNote} Approval remains active for one hook retry.${output ? `\n${output}` : ''}`,
115
+ message: `Belay replay failed for ${options.approvalId} (exit ${replayResult.exitCode}).${timeoutNote} The one-shot approval was consumed before replay and was not re-armed.`,
92
116
  };
93
117
  }
94
118
  return { ok: result.ok, message: result.message };
@@ -36,7 +36,7 @@ export async function classifyForReport(params) {
36
36
  if (!params.command && !params.payload) {
37
37
  throw new Error('classify-for-report requires command or payload for subagent classification.');
38
38
  }
39
- result = classifySubagent(payload, repoRoot, classifierOptions);
39
+ result = classifySubagent(payload, repoRoot, classifierOptions, config);
40
40
  input = params.command ?? JSON.stringify(payload);
41
41
  }
42
42
  else if (kind === 'tool') {
@@ -1,5 +1,6 @@
1
1
  import { type JudgeProviderId } from '../core/verdict/judge-catalog.js';
2
2
  import type { AdapterName, InitOptions } from '../types.js';
3
+ import { type SelectOptions } from './tui.js';
3
4
  export declare const BELAY_CONFIG_SUBCOMMANDS: readonly ["list", "get", "set", "unset", "credential", "judge"];
4
5
  export type BelayConfigSubcommand = (typeof BELAY_CONFIG_SUBCOMMANDS)[number];
5
6
  export interface BelayConfigOptions {
@@ -39,6 +40,13 @@ export interface BelayConfigInteractiveOptions {
39
40
  /** Suppress the interactive header when nested under runBelayConfigInteractive */
40
41
  skipBanner?: boolean;
41
42
  }
43
+ export declare const JUDGE_CREDENTIAL_MODE_PROMPT = "Judge API key source";
44
+ export declare const JUDGE_CREDENTIAL_STORE_KEY_PROMPT = "Paste API key to store locally (visible input; prefer belay config credential set --key-stdin): ";
45
+ export declare const JUDGE_TRANSPORT_MODE_PROMPT = "How should Belay reach the judge?";
46
+ export declare const JUDGE_HTTP_ENDPOINT_PROMPT = "Judge HTTP API URL:";
47
+ export type JudgeTransportWizardMode = 'cli' | 'http';
48
+ export declare function buildJudgeTransportSelectOptions(judgeProviderId: JudgeProviderId): SelectOptions<JudgeTransportWizardMode>;
49
+ export declare function buildJudgeCredentialModeSelectOptions(judgeProviderId: JudgeProviderId): SelectOptions<'project' | 'apiKey'>;
42
50
  export declare function resolveBelayConfigInteractiveMode(repoRoot: string): Promise<'full' | 'judge-only'>;
43
51
  export declare function runBelayConfigFullInteractive(options?: BelayConfigInteractiveOptions): Promise<{
44
52
  repoRoot: string;
@@ -8,8 +8,9 @@ import { clearJudgeCredentialStore, writeJudgeCredentialStore } from '../core/cr
8
8
  import { refreshIntegrityIfPinned } from '../core/integrity.js';
9
9
  import { defaultJudgeProviderForAdapter, hasValidCloudConsent, isCloudJudgeConfig, resolveJudgeUsePatch, } from '../core/judge-config.js';
10
10
  import { rejectDeprecatedJudgeModelAuto } from '../core/judge-model-policy.js';
11
- import { resolveJudgeTransport } from '../core/judge-runtime-detection.js';
12
- import { isJudgeProviderId, JUDGE_PROVIDER_IDS, normalizeLegacyProviderId, } from '../core/verdict/judge-catalog.js';
11
+ import { detectJudgeRuntimeCapabilities, resolveJudgeTransport, } from '../core/judge-runtime-detection.js';
12
+ import { getJudgeProviderSpec, isJudgeProviderId, JUDGE_PROVIDER_IDS, normalizeLegacyProviderId, } from '../core/verdict/judge-catalog.js';
13
+ import { normalizeJudgeRuntimeConfig } from '../core/verdict/judge-runtime-config.js';
13
14
  import { initProject } from '../installer.js';
14
15
  import { isBelayFloorInstalled } from './health-snapshot.js';
15
16
  import { judgeStatus } from './judge.js';
@@ -31,6 +32,8 @@ const JUDGE_CONFIG_PATHS = [
31
32
  'judge.timeoutMs',
32
33
  'judge.credential.mode',
33
34
  'judge.credential.ref',
35
+ 'judge.runtime.session.enabled',
36
+ 'judge.runtime.shadow.enabled',
34
37
  ];
35
38
  export function parseAdapter(value) {
36
39
  const normalized = (value?.trim() || 'cursor').toLowerCase();
@@ -98,6 +101,12 @@ function getJudgeField(judge, pathKey) {
98
101
  return judge.credential?.mode ?? null;
99
102
  if (pathKey === 'judge.credential.ref')
100
103
  return judge.credential?.ref ?? null;
104
+ if (pathKey === 'judge.runtime.session.enabled') {
105
+ return normalizeJudgeRuntimeConfig(judge.runtime).session.enabled;
106
+ }
107
+ if (pathKey === 'judge.runtime.shadow.enabled') {
108
+ return normalizeJudgeRuntimeConfig(judge.runtime).shadow.enabled;
109
+ }
101
110
  throw new Error(`Unknown judge config path: ${pathKey}`);
102
111
  }
103
112
  function listJudgeFields(judge) {
@@ -181,6 +190,36 @@ async function applyJudgeSet(repoRoot, config, pathKey, rawValue) {
181
190
  await persistJudge(repoRoot, config, judge, adapter);
182
191
  return judge;
183
192
  }
193
+ if (pathKey === 'judge.runtime.session.enabled') {
194
+ if (value !== 'true' && value !== 'false') {
195
+ throw new Error('judge.runtime.session.enabled must be true or false.');
196
+ }
197
+ const runtime = normalizeJudgeRuntimeConfig(config.judge.runtime);
198
+ const judge = normalizeJudgeConfig({
199
+ ...config.judge,
200
+ runtime: {
201
+ ...runtime,
202
+ session: { ...runtime.session, enabled: value === 'true' },
203
+ },
204
+ });
205
+ await persistJudge(repoRoot, config, judge, adapter);
206
+ return judge;
207
+ }
208
+ if (pathKey === 'judge.runtime.shadow.enabled') {
209
+ if (value !== 'true' && value !== 'false') {
210
+ throw new Error('judge.runtime.shadow.enabled must be true or false.');
211
+ }
212
+ const runtime = normalizeJudgeRuntimeConfig(config.judge.runtime);
213
+ const judge = normalizeJudgeConfig({
214
+ ...config.judge,
215
+ runtime: {
216
+ ...runtime,
217
+ shadow: { ...runtime.shadow, enabled: value === 'true' },
218
+ },
219
+ });
220
+ await persistJudge(repoRoot, config, judge, adapter);
221
+ return judge;
222
+ }
184
223
  throw new Error(`Unknown judge config path: ${pathKey}`);
185
224
  }
186
225
  async function applyJudgeUnset(repoRoot, config, pathKey) {
@@ -289,7 +328,75 @@ export async function runBelayConfigCredential(options) {
289
328
  }
290
329
  throw new Error('credential requires action: mode, set, or clear.');
291
330
  }
331
+ export const JUDGE_CREDENTIAL_MODE_PROMPT = 'Judge API key source';
332
+ export const JUDGE_CREDENTIAL_STORE_KEY_PROMPT = 'Paste API key to store locally (visible input; prefer belay config credential set --key-stdin): ';
333
+ export const JUDGE_TRANSPORT_MODE_PROMPT = 'How should Belay reach the judge?';
334
+ export const JUDGE_HTTP_ENDPOINT_PROMPT = 'Judge HTTP API URL:';
335
+ const CLOUD_CLI_LABELS = {
336
+ codex: 'Codex CLI',
337
+ claude: 'Claude CLI',
338
+ cursor: 'Cursor CLI',
339
+ };
340
+ export function buildJudgeTransportSelectOptions(judgeProviderId) {
341
+ const cliLabel = CLOUD_CLI_LABELS[judgeProviderId];
342
+ const runtime = detectJudgeRuntimeCapabilities(judgeProviderId);
343
+ const cliHint = runtime.cliTransport
344
+ ? 'Uses your logged-in host session; no URL needed.'
345
+ : 'Host CLI not found on PATH — install it or choose custom HTTP API.';
346
+ return {
347
+ message: JUDGE_TRANSPORT_MODE_PROMPT,
348
+ defaultValue: 'cli',
349
+ choices: [
350
+ {
351
+ value: 'cli',
352
+ label: `Use ${cliLabel} (recommended)`,
353
+ hint: cliHint,
354
+ },
355
+ {
356
+ value: 'http',
357
+ label: 'Custom HTTP API endpoint',
358
+ hint: 'Requires a provider API URL and cloud egress consent.',
359
+ },
360
+ ],
361
+ };
362
+ }
363
+ export function buildJudgeCredentialModeSelectOptions(judgeProviderId) {
364
+ const spec = getJudgeProviderSpec(judgeProviderId);
365
+ const providerEnvVars = spec?.apiKeyEnvVars.filter((name) => name !== 'BELAY_JUDGE_API_KEY') ?? [];
366
+ const envNames = providerEnvVars.length > 0
367
+ ? ['BELAY_JUDGE_API_KEY', ...providerEnvVars].join(', ')
368
+ : 'BELAY_JUDGE_API_KEY';
369
+ const projectHint = providerEnvVars.length > 0
370
+ ? `Uses ${envNames} from the shell; does not read .env files. Or host CLI login when available.`
371
+ : 'Uses BELAY_JUDGE_API_KEY from the shell; does not read .env files.';
372
+ return {
373
+ message: JUDGE_CREDENTIAL_MODE_PROMPT,
374
+ defaultValue: 'project',
375
+ choices: [
376
+ {
377
+ value: 'project',
378
+ label: 'Environment variables or host CLI',
379
+ hint: projectHint,
380
+ },
381
+ {
382
+ value: 'apiKey',
383
+ label: 'Store in Belay credential store',
384
+ hint: 'Saved to credentials.json (mode 0600) on the next step.',
385
+ },
386
+ ],
387
+ };
388
+ }
292
389
  function formatSelectPrompt(options) {
390
+ const hasDetails = options.choices.some((choice) => choice.hint || choice.label);
391
+ if (hasDetails) {
392
+ const lines = options.choices.map((choice) => {
393
+ const label = choice.label ?? choice.value;
394
+ const hint = choice.hint ? ` — ${choice.hint}` : '';
395
+ return ` ${choice.value}: ${label}${hint}`;
396
+ });
397
+ const values = options.choices.map((choice) => choice.value).join(' | ');
398
+ return `${options.message}\n${lines.join('\n')}\n[${values}] (${options.defaultValue}): `;
399
+ }
293
400
  const values = options.choices.map((choice) => choice.value).join(' | ');
294
401
  return `${options.message} [${values}] (${options.defaultValue}): `;
295
402
  }
@@ -374,20 +481,36 @@ function writeConfigWizardBanner(options, title) {
374
481
  }
375
482
  output.write(`${title}\n`);
376
483
  }
484
+ async function askJudgeHttpEndpoint(prompter) {
485
+ while (true) {
486
+ const endpoint = (await prompter.askText(JUDGE_HTTP_ENDPOINT_PROMPT)).trim();
487
+ if (endpoint) {
488
+ return endpoint;
489
+ }
490
+ process.stderr.write('Warning: HTTP endpoint URL is required when using custom HTTP API. Paste a URL or re-run belay config and choose host CLI.\n');
491
+ }
492
+ }
493
+ async function askJudgeCredentialStoreKey(prompter) {
494
+ while (true) {
495
+ const key = (await prompter.askText(JUDGE_CREDENTIAL_STORE_KEY_PROMPT)).trim();
496
+ if (key) {
497
+ return key;
498
+ }
499
+ process.stderr.write('Warning: API key cannot be empty. Paste a key or re-run belay config and choose environment variables.\n');
500
+ }
501
+ }
377
502
  async function collectCloudJudgeWizardAnswers(prompter, judgeProviderId) {
378
503
  if (judgeProviderId === 'ollama') {
379
504
  return { acceptCloud: false };
380
505
  }
381
- const judgeCredentialMode = (await prompter.askConfirm('Use project env for credentials?', true))
382
- ? 'project'
383
- : 'apiKey';
384
- const optionalEndpoint = (await prompter.askText('Judge endpoint URL (optional): ')).trim();
385
- const judgeEndpoint = optionalEndpoint || undefined;
506
+ const judgeCredentialMode = await prompter.askSelect(buildJudgeCredentialModeSelectOptions(judgeProviderId));
507
+ const transportMode = await prompter.askSelect(buildJudgeTransportSelectOptions(judgeProviderId));
508
+ let judgeEndpoint;
509
+ if (transportMode === 'http') {
510
+ judgeEndpoint = await askJudgeHttpEndpoint(prompter);
511
+ }
386
512
  if (judgeCredentialMode === 'apiKey') {
387
- const key = await prompter.askText('Paste API key (hidden input not available in all shells): ');
388
- if (key.trim()) {
389
- process.env.BELAY_CONFIG_WIZARD_JUDGE_KEY = key.trim();
390
- }
513
+ process.env.BELAY_CONFIG_WIZARD_JUDGE_KEY = await askJudgeCredentialStoreKey(prompter);
391
514
  }
392
515
  let acceptCloud = false;
393
516
  if (judgeEndpoint) {
@@ -7,10 +7,17 @@ import { getAdapterLayout } from '../adapters/layouts/index.js';
7
7
  import { resolveScopedPaths } from '../adapters/layouts/scope.js';
8
8
  import { cleanupOrphanApprovalState } from '../cleanup-orphans.js';
9
9
  import { approvedApprovalsPath, belayStateDir, detectAdapterName, loadLayeredConfig, pendingApprovalsPath, repoLocalStateDirFor, } from '../config-io.js';
10
+ import { approvalSigningKeyPath } from '../core/approval-token.js';
10
11
  import { detectFenceDrift, summarizeAuditVisibility } from '../core/audit-summary.js';
12
+ import { inspectBoundaryAttestationFile } from '../core/capability/boundary-attestation-sign.js';
13
+ import { boundaryAttestationPath, boundarySessionStatus, } from '../core/capability/boundary-session.js';
11
14
  import { defaultControlPlaneDir } from '../core/config.js';
12
15
  import { verifyIntegrityManifest } from '../core/integrity.js';
13
16
  import { diagnoseJudge, stopJudgeSessionBrokers } from '../core/judge-doctor.js';
17
+ import { resolveJudgeTransport } from '../core/judge-runtime-detection.js';
18
+ import { listRecoveryCheckpoints } from '../core/recovery/checkpoint.js';
19
+ import { recoveryApprovalSetupNotes, recoveryNotificationConfigured, recoveryNotificationSetupWarning, summarizeRecoveryCheckpointDiagnostics, } from '../core/recovery/operator-guidance.js';
20
+ import { probeFileCloneStrategy } from '../core/transactional/file-clone.js';
14
21
  import { getManagedHookEntries } from '../defaults.js';
15
22
  import { resolveNodeBinary } from '../node-resolution.js';
16
23
  import { egressStatus } from '../services/egress-service.js';
@@ -78,7 +85,9 @@ export async function doctorProject(options = {}) {
78
85
  if (loadedConfig.version !== 4) {
79
86
  warnings.push(`Config version is ${loadedConfig.version}; expected 4. Run belay upgrade to migrate.`);
80
87
  }
81
- const judgeDoctor = await diagnoseJudge(loadedConfig);
88
+ const transport = resolveJudgeTransport(loadedConfig.judge);
89
+ const shouldProbe = transport.endsWith('-cli');
90
+ const judgeDoctor = await diagnoseJudge(loadedConfig, repoRoot, { liveProbe: shouldProbe });
82
91
  issues.push(...judgeDoctor.issues);
83
92
  warnings.push(...judgeDoctor.warnings);
84
93
  notes.push(...judgeDoctor.notes);
@@ -285,6 +294,45 @@ export async function doctorProject(options = {}) {
285
294
  warnings.push('Transactional execution is enabled but this directory is not a git repository. Transactional worktrees will be skipped until git is available.');
286
295
  }
287
296
  }
297
+ const fileCheckpoint = loadedConfig.policy.transactional.fileCheckpoint;
298
+ if (fileCheckpoint.enabled) {
299
+ const copyStrategy = await probeFileCloneStrategy();
300
+ notes.push(`File checkpoint: enabled (copyStrategy=${copyStrategy}, non-Git=${fileCheckpoint.allowNonGit}, maxFiles=${fileCheckpoint.maxFiles}, maxSourceBytes=${fileCheckpoint.maxSourceBytes}, maxWorkspaceBytes=${fileCheckpoint.maxWorkspaceBytes}, prepareTimeoutMs=${fileCheckpoint.prepareTimeoutMs}, copyConcurrency=${fileCheckpoint.copyConcurrency}).`);
301
+ if (!loadedConfig.policy.transactional.enabled) {
302
+ warnings.push('File checkpoint is enabled but transactional execution is disabled; dirty Git workspaces cannot use the backend.');
303
+ }
304
+ if (!loadedConfig.policy.transactional.checkpoint?.enabled) {
305
+ warnings.push('File checkpoint is enabled but durable Recovery checkpointing is disabled; backend selection will fail closed.');
306
+ }
307
+ }
308
+ if (loadedConfig.policy.transactional.checkpoint?.enabled) {
309
+ notes.push('Recovery checkpoint: enabled — repo-local pre-images are persisted before observed-safe transactional apply.');
310
+ notes.push(...recoveryApprovalSetupNotes());
311
+ if (!recoveryNotificationConfigured(loadedConfig)) {
312
+ warnings.push(recoveryNotificationSetupWarning());
313
+ }
314
+ const signingKeyPath = approvalSigningKeyPath(loadedConfig.controlPlane.enabled
315
+ ? belayStateDir(loadedConfig, repoLocalDir)
316
+ : (loadedConfig.controlPlane.configDir ?? defaultControlPlaneDir()));
317
+ if (!existsSync(signingKeyPath)) {
318
+ notes.push(`Approval signing key not yet created at ${signingKeyPath}; it will be generated on the first signed recovery approval request.`);
319
+ }
320
+ if (!loadedConfig.policy.transactional.enabled) {
321
+ warnings.push('Recovery checkpoint is enabled but transactional execution is disabled; checkpoints will not be created until transactional execution is enabled.');
322
+ }
323
+ try {
324
+ const stateDir = belayStateDir(loadedConfig, repoLocalDir);
325
+ const checkpoints = await listRecoveryCheckpoints(stateDir, repoRoot);
326
+ if (checkpoints.length > 0) {
327
+ notes.push(`Recovery checkpoints in this repository: ${checkpoints.length}.`);
328
+ }
329
+ const advisories = summarizeRecoveryCheckpointDiagnostics(checkpoints);
330
+ warnings.push(...advisories);
331
+ }
332
+ catch {
333
+ // Checkpoint inspection is advisory only.
334
+ }
335
+ }
288
336
  if (loadedConfig.sandbox.enabled) {
289
337
  const sandbox = await sandboxStatus({ targetDir: repoRoot });
290
338
  notes.push(`Sandbox capability broker: enabled (runtime=${loadedConfig.sandbox.runtime}, fs-scope entries=${sandbox.fsScopeAllowlistCount}, full-isolation=${sandbox.l1FullActive}).`);
@@ -294,6 +342,44 @@ export async function doctorProject(options = {}) {
294
342
  for (const issue of sandbox.issues) {
295
343
  warnings.push(issue);
296
344
  }
345
+ for (const advisory of sandbox.advisories) {
346
+ warnings.push(advisory);
347
+ }
348
+ }
349
+ const boundaryDriver = loadedConfig.capability?.boundaryDriver ??
350
+ (loadedConfig.sandbox.runtime === 'container' ? 'container' : null);
351
+ if (loadedConfig.policy.transactional.fileCheckpoint.enabled &&
352
+ boundaryDriver !== 'container') {
353
+ warnings.push('File checkpoint requires an attested workspace-isolating boundary driver; configure the container boundary and run belay session start.');
354
+ }
355
+ if (loadedConfig.capability || boundaryDriver === 'container') {
356
+ const attestationPath = boundaryAttestationPath(repoRoot, loadedConfig);
357
+ const attestationFormat = await inspectBoundaryAttestationFile(attestationPath);
358
+ if (attestationFormat === 'legacy') {
359
+ warnings.push('Boundary attestation uses an unsigned legacy format. Run belay session start to re-sign it.');
360
+ }
361
+ else if (attestationFormat === 'invalid') {
362
+ warnings.push('Boundary attestation file is present but invalid. Run belay session start to regenerate it.');
363
+ }
364
+ }
365
+ if (boundaryDriver === 'container') {
366
+ const session = await boundarySessionStatus({ repoRoot, config: loadedConfig });
367
+ if (!session.attestation) {
368
+ warnings.push('Container boundary driver is configured but no attestation was found. Run belay session start.');
369
+ }
370
+ else if (!session.fresh) {
371
+ warnings.push('Boundary attestation is stale or expired. Run belay session start to refresh.');
372
+ }
373
+ else if (loadedConfig.policy.transactional.fileCheckpoint.enabled &&
374
+ session.attestation.isolatesWorkspaceMounts !== true) {
375
+ warnings.push('File checkpoint is enabled but the fresh boundary attestation does not prove workspace-mount isolation. Run belay session start with a compatible boundary.');
376
+ }
377
+ else if (session.attestation.probeSignals.includes('repo-mount-ro-default')) {
378
+ notes.push('Container boundary defaults to read-only directory mounts; predicted writes use read-write mounts for the working directory only.');
379
+ }
380
+ else if (session.attestation.probeSignals.includes('repo-mount-rw')) {
381
+ notes.push('Container boundary mounts the working directory read-write (repo-mount-rw). Policy grants scope widening; paths outside the mount are not container-enforced.');
382
+ }
297
383
  }
298
384
  if (loadedConfig.egress.enabled) {
299
385
  const egress = await egressStatus({ targetDir: repoRoot });
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { loadApprovalState, loadConfigFile } from '../config-io.js';
3
3
  import { compactApprovals } from '../core/approval.js';
4
+ import { extractJudgeFallbackReason, formatJudgeRecoveryHint, inferProviderIdFromFallbackReason, isJudgeInfrastructureFailure, } from '../core/judge-fallback-hints.js';
4
5
  import { classifyForReport } from './classify-for-report.js';
5
6
  export async function explainCommand(options) {
6
7
  const repoRoot = path.resolve(options.targetDir ?? process.cwd());
@@ -134,5 +135,14 @@ export function formatExplainReport(report) {
134
135
  ? 'Observed assessment: measured in an isolated git worktree at gate time. Observed-safe commands are applied once and the hook denies re-execution (transactional_already_applied).'
135
136
  : 'Observed assessment: not applicable (transactional path not eligible).',
136
137
  ];
137
- return `${lines.join('\n')}\n`;
138
+ const recoveryLines = [];
139
+ if (isJudgeInfrastructureFailure(result)) {
140
+ const fallbackReason = extractJudgeFallbackReason(result);
141
+ const providerId = inferProviderIdFromFallbackReason(fallbackReason);
142
+ const recoveryHint = formatJudgeRecoveryHint(providerId, fallbackReason);
143
+ if (recoveryHint) {
144
+ recoveryLines.push('', 'Recovery:', ` ${recoveryHint}`, ' Then verify: belay judge test --live-probe');
145
+ }
146
+ }
147
+ return `${[...lines, ...recoveryLines].join('\n')}\n`;
138
148
  }
@@ -63,10 +63,11 @@ export async function judgeStatus(options = {}) {
63
63
  });
64
64
  const { resolved } = resolveJudgeModel(judge);
65
65
  const transport = resolveJudgeTransport(judge);
66
+ const effectiveTransport = transport === 'cursor-cli' && judge.runtime?.session.enabled ? 'cursor-acp' : transport;
66
67
  const lines = [
67
68
  `Judge providerId : ${judge.providerId ?? '(inferred)'}`,
68
69
  `Judge driver : ${judge.provider}`,
69
- `Transport : ${transport}`,
70
+ `Transport : ${effectiveTransport}`,
70
71
  `Endpoint : ${judge.endpoint ?? '(none)'}`,
71
72
  `Model : ${resolved} (requested: ${judge.model})`,
72
73
  `Credential : ${credential.mode} (${credential.sourceKind})${credential.source ? ` → ${credential.source}` : ''} ${credential.key ? '✓ set' : '✗ missing'}`,
@@ -82,7 +83,7 @@ export async function judgeStatus(options = {}) {
82
83
  endpoint: judge.endpoint,
83
84
  model: judge.model,
84
85
  modelResolved: resolved,
85
- transport,
86
+ transport: effectiveTransport,
86
87
  credential,
87
88
  cloudConsent: judge.cloudConsent ?? null,
88
89
  cloudJudgeActive: isCloudJudgeConfig(judge) && hasValidCloudConsent(judge),