@guilz-dev/belay 0.1.1 → 0.2.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 (61) hide show
  1. package/README.md +16 -3
  2. package/dist/adapters/shared/gate-runtime.js +12 -4
  3. package/dist/bundle/claude-runtime.mjs +155 -36
  4. package/dist/bundle/codex-runtime.mjs +155 -36
  5. package/dist/bundle/cursor-runtime.mjs +155 -36
  6. package/dist/cli.js +4 -4
  7. package/dist/commands/classify-for-report.js +3 -3
  8. package/dist/commands/doctor.js +1 -1
  9. package/dist/commands/explain.js +14 -14
  10. package/dist/commands/init-wizard.d.ts +5 -0
  11. package/dist/commands/init-wizard.js +24 -11
  12. package/dist/commands/recover.js +2 -2
  13. package/dist/core/approval.d.ts +3 -0
  14. package/dist/core/approval.js +18 -3
  15. package/dist/core/audit-query.js +5 -1
  16. package/dist/core/classify-tool.js +1 -1
  17. package/dist/core/config.d.ts +1 -1
  18. package/dist/core/config.js +2 -2
  19. package/dist/core/gate-contract.d.ts +1 -1
  20. package/dist/core/gate-contract.js +1 -1
  21. package/dist/core/gate-engine.js +2 -2
  22. package/dist/core/index.d.ts +2 -2
  23. package/dist/core/index.js +2 -2
  24. package/dist/core/judge-config.d.ts +5 -1
  25. package/dist/core/judge-config.js +17 -1
  26. package/dist/core/judge-doctor.js +2 -2
  27. package/dist/core/types.d.ts +5 -3
  28. package/dist/core/{v2 → verdict}/adapter.js +9 -3
  29. package/dist/core/{v2 → verdict}/egress-classify.js +3 -0
  30. package/dist/core/{v2 → verdict}/judge.js +10 -12
  31. package/dist/core/{v2 → verdict}/launcher-resolve.js +72 -1
  32. package/dist/core/{v2 → verdict}/verdict.js +16 -0
  33. package/dist/corpus/evaluate.js +2 -2
  34. package/dist/installer.js +1 -0
  35. package/dist/types.d.ts +1 -1
  36. package/dist/version.d.ts +1 -1
  37. package/dist/version.js +1 -1
  38. package/package.json +2 -1
  39. /package/dist/core/{v2 → verdict}/adapter.d.ts +0 -0
  40. /package/dist/core/{v2 → verdict}/containment.d.ts +0 -0
  41. /package/dist/core/{v2 → verdict}/containment.js +0 -0
  42. /package/dist/core/{v2 → verdict}/egress-classify.d.ts +0 -0
  43. /package/dist/core/{v2 → verdict}/fingerprint.d.ts +0 -0
  44. /package/dist/core/{v2 → verdict}/fingerprint.js +0 -0
  45. /package/dist/core/{v2 → verdict}/index.d.ts +0 -0
  46. /package/dist/core/{v2 → verdict}/index.js +0 -0
  47. /package/dist/core/{v2 → verdict}/judge-audit.d.ts +0 -0
  48. /package/dist/core/{v2 → verdict}/judge-audit.js +0 -0
  49. /package/dist/core/{v2 → verdict}/judge-factory.d.ts +0 -0
  50. /package/dist/core/{v2 → verdict}/judge-factory.js +0 -0
  51. /package/dist/core/{v2 → verdict}/judge-outbound.d.ts +0 -0
  52. /package/dist/core/{v2 → verdict}/judge-outbound.js +0 -0
  53. /package/dist/core/{v2 → verdict}/judge.d.ts +0 -0
  54. /package/dist/core/{v2 → verdict}/launcher-resolve.d.ts +0 -0
  55. /package/dist/core/{v2 → verdict}/overrides.d.ts +0 -0
  56. /package/dist/core/{v2 → verdict}/overrides.js +0 -0
  57. /package/dist/core/{v2 → verdict}/parser.d.ts +0 -0
  58. /package/dist/core/{v2 → verdict}/parser.js +0 -0
  59. /package/dist/core/{v2 → verdict}/types.d.ts +0 -0
  60. /package/dist/core/{v2 → verdict}/types.js +0 -0
  61. /package/dist/core/{v2 → verdict}/verdict.d.ts +0 -0
@@ -86,7 +86,7 @@ var LEGACY_POLICY_V3 = {
86
86
  fenceWarnThreshold: DEFAULT_FENCE_WARN_THRESHOLD
87
87
  };
88
88
  var DEFAULT_POLICY_V3 = {
89
- unknownLocalEffect: "deny",
89
+ unknownLocalEffect: "allow_flagged",
90
90
  unparseableShell: "deny",
91
91
  codexUnmappedTool: "deny",
92
92
  confidenceThresholds: { ...DEFAULT_CONFIDENCE_THRESHOLDS },
@@ -703,16 +703,25 @@ import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile2 } from
703
703
  import path16 from "node:path";
704
704
 
705
705
  // src/core/approval.ts
706
+ var APPROVAL_EXECUTION_LEASE_MS = 6e4;
706
707
  function nowIso() {
707
708
  return (/* @__PURE__ */ new Date()).toISOString();
708
709
  }
709
710
  function isExpired(approval) {
710
711
  return Date.parse(approval.expiresAt) <= Date.now();
711
712
  }
713
+ function isExecutionLeaseExpired(approval) {
714
+ if (!approval.executionLeaseExpiresAt) {
715
+ return false;
716
+ }
717
+ return Date.parse(approval.executionLeaseExpiresAt) <= Date.now();
718
+ }
712
719
  function compactApprovals(state) {
713
720
  return {
714
721
  version: state.version,
715
- approvals: state.approvals.filter((approval) => !isExpired(approval))
722
+ approvals: state.approvals.filter(
723
+ (approval) => !isExpired(approval) && !isExecutionLeaseExpired(approval)
724
+ )
716
725
  };
717
726
  }
718
727
  function escapeRegex(value) {
@@ -721,8 +730,15 @@ function escapeRegex(value) {
721
730
  }
722
731
  function approvalCommandMatch(prompt, tokenPrefix) {
723
732
  const escapedPrefix = escapeRegex(tokenPrefix);
724
- const match = prompt.match(new RegExp(`^\\s*${escapedPrefix}\\s+(\\S+)\\s*$`, "i"));
725
- return match?.[1] ?? null;
733
+ const linePattern = new RegExp(`^\\s*${escapedPrefix}\\s+(\\S+)\\s*$`, "i");
734
+ for (const line of prompt.split(/\r?\n/)) {
735
+ if (!line.trim()) {
736
+ continue;
737
+ }
738
+ const match = line.match(linePattern);
739
+ return match?.[1] ?? null;
740
+ }
741
+ return null;
726
742
  }
727
743
  function buildRetryInstruction(tokenPrefix, approvalId) {
728
744
  return `To allow the next matching action once, send ${tokenPrefix} ${approvalId} and then retry the original action unchanged.`;
@@ -1249,7 +1265,7 @@ function classifyResultToGateVerdict(params) {
1249
1265
  approvalId,
1250
1266
  user_message,
1251
1267
  agent_message,
1252
- v2: result.v2
1268
+ axes: result.axes
1253
1269
  };
1254
1270
  }
1255
1271
  function unnormalizedGateVerdict(params) {
@@ -1503,7 +1519,7 @@ function matchesSensitivePath(filePath, patterns) {
1503
1519
  return false;
1504
1520
  }
1505
1521
 
1506
- // src/core/v2/judge-audit.ts
1522
+ // src/core/verdict/judge-audit.ts
1507
1523
  function judgeTraceAuditFields(trace) {
1508
1524
  if (!trace) {
1509
1525
  return {};
@@ -1518,7 +1534,7 @@ function judgeTraceAuditFields(trace) {
1518
1534
  };
1519
1535
  }
1520
1536
 
1521
- // src/core/v2/judge-outbound.ts
1537
+ // src/core/verdict/judge-outbound.ts
1522
1538
  var PATH_LIKE = /(?:^|[\s"'`=])(~\/[^\s"'`]+|\/[^\s"'`]+|\.\/[^\s"'`]+|\.\.\/[^\s"'`]+|[A-Za-z]:\\[^\s"'`]+)/g;
1523
1539
  var REDACTED_PLACEHOLDER = /^(?:<redacted>|\[REDACTED\]|<secret>|<high-entropy>|<approval-id>)$/i;
1524
1540
  var URL_CREDENTIALS_PATTERN2 = /\b[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/\s:@]+):([^@\s/]+)@/gi;
@@ -1587,16 +1603,16 @@ function scrubOutboundForJudge(text, options) {
1587
1603
  }
1588
1604
  }
1589
1605
 
1590
- // src/core/v2/judge.ts
1606
+ // src/core/verdict/judge.ts
1591
1607
  var SECRET_PATTERNS = [/\.env\b/i, /\.pem\b/i, /id_rsa\b/i, /credentials/i, /secrets?\b/i];
1592
1608
  var DESTRUCTIVE_VERBS = /\b(rm|rmtree|unlink|delete|truncate|shred|destroy|drop)\b/i;
1593
1609
  var GIT_PATTERNS = /\.git\b/i;
1594
- var TIER1_PROMPT = `Answer ONLY with JSON: {"external_change":boolean,"destroys_outside_repo":boolean,"destroys_history_or_secrets":boolean,"reason":string}
1595
- Default all three booleans to true when unsure. reason should be a short snake_case label.
1610
+ var TIER1_PROMPT = `Answer ONLY with JSON: {"external_change":boolean,"reason":string}
1611
+ reason should be a short snake_case label.
1596
1612
 
1597
- external_change: CREATE/MODIFY/DELETE on database, network endpoint, cloud, remote git, registry, queue. READ/QUERY/GET is NOT a change.
1598
- destroys_outside_repo: DELETE/OVERWRITE files OUTSIDE the current repository working tree.
1599
- destroys_history_or_secrets: DELETE/OVERWRITE .git directory OR secret/credential files (.env, *.pem, id_rsa, credentials).
1613
+ external_change: true if this operation CREATEs, MODIFYs, DELETEs, or SENDs data to any system outside the local machine and its git repository working tree.
1614
+ READ/QUERY/GET/list/describe/SELECT/HTTP GET is NOT a change. Local build, test, and package install inside the repo are NOT external changes.
1615
+ If unsure, answer true.
1600
1616
 
1601
1617
  Command/code:
1602
1618
  `;
@@ -1611,13 +1627,13 @@ function failClosedVerdict(reason) {
1611
1627
  function parseTier1Json(raw) {
1612
1628
  try {
1613
1629
  const parsed = JSON.parse(raw);
1614
- if (typeof parsed.external_change !== "boolean" || typeof parsed.destroys_outside_repo !== "boolean" || typeof parsed.destroys_history_or_secrets !== "boolean") {
1630
+ if (typeof parsed.external_change !== "boolean") {
1615
1631
  return null;
1616
1632
  }
1617
1633
  return {
1618
- external_change: parsed.external_change !== false,
1619
- destroys_outside_repo: parsed.destroys_outside_repo !== false,
1620
- destroys_history_or_secrets: parsed.destroys_history_or_secrets !== false,
1634
+ external_change: parsed.external_change,
1635
+ destroys_outside_repo: false,
1636
+ destroys_history_or_secrets: false,
1621
1637
  reason: typeof parsed.reason === "string" ? parsed.reason : "tier1_llm"
1622
1638
  };
1623
1639
  } catch {
@@ -1840,10 +1856,10 @@ function createOpenAiCompatibleJudge(options) {
1840
1856
  return judge;
1841
1857
  }
1842
1858
  function tier1RequiresAsk(verdict2) {
1843
- return verdict2.external_change || verdict2.destroys_outside_repo || verdict2.destroys_history_or_secrets;
1859
+ return verdict2.external_change || verdict2.destroys_history_or_secrets;
1844
1860
  }
1845
1861
 
1846
- // src/core/v2/judge-factory.ts
1862
+ // src/core/verdict/judge-factory.ts
1847
1863
  var FIXTURE_MODELS_URL = new URL("../../../fixtures/judge-models.json", import.meta.url);
1848
1864
  function resolveCloudModel(requested, pinned) {
1849
1865
  if (requested === "auto") {
@@ -1890,10 +1906,10 @@ function createJudgeFromConfig(config, options = {}) {
1890
1906
  return createDeterministicJudgeStub();
1891
1907
  }
1892
1908
 
1893
- // src/core/v2/verdict.ts
1909
+ // src/core/verdict/verdict.ts
1894
1910
  import path11 from "node:path";
1895
1911
 
1896
- // src/core/v2/containment.ts
1912
+ // src/core/verdict/containment.ts
1897
1913
  import path8 from "node:path";
1898
1914
  function expandHome(token) {
1899
1915
  if (token === "~" || token.startsWith("~/")) {
@@ -1985,7 +2001,7 @@ function cwdRelative(repoRoot, cwd) {
1985
2001
  return relativeWithinRepo(repoRoot, cwd) ?? cwd;
1986
2002
  }
1987
2003
 
1988
- // src/core/v2/egress-classify.ts
2004
+ // src/core/verdict/egress-classify.ts
1989
2005
  var EGRESS_TOOL_HEADS = /* @__PURE__ */ new Set([
1990
2006
  "aws",
1991
2007
  "curl",
@@ -2083,6 +2099,9 @@ function classifyAws(tokens) {
2083
2099
  if (/\bs3\s+rm\b/.test(joined)) {
2084
2100
  return "destructive";
2085
2101
  }
2102
+ if (/\bs3\s+mb\b/.test(joined)) {
2103
+ return "destructive";
2104
+ }
2086
2105
  if (/\bs3\s+sync\b/.test(joined)) {
2087
2106
  return "destructive";
2088
2107
  }
@@ -2193,15 +2212,59 @@ function classifyNetlify(tokens) {
2193
2212
  return "ambiguous";
2194
2213
  }
2195
2214
 
2196
- // src/core/v2/fingerprint.ts
2215
+ // src/core/verdict/fingerprint.ts
2197
2216
  function verdictFingerprint(cwdRelative2, commandRedacted) {
2198
2217
  return hashValue(`v2:${cwdRelative2}:${commandRedacted}`);
2199
2218
  }
2200
2219
 
2201
- // src/core/v2/launcher-resolve.ts
2220
+ // src/core/verdict/launcher-resolve.ts
2202
2221
  import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
2203
2222
  import path9 from "node:path";
2204
2223
  var MAX_RESOLVE_DEPTH = 8;
2224
+ var PNPM_BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
2225
+ "add",
2226
+ "audit",
2227
+ "cache",
2228
+ "config",
2229
+ "deploy",
2230
+ "dlx",
2231
+ "exec",
2232
+ "fetch",
2233
+ "help",
2234
+ "import",
2235
+ "init",
2236
+ "install",
2237
+ "i",
2238
+ "licenses",
2239
+ "link",
2240
+ "list",
2241
+ "outdated",
2242
+ "pack",
2243
+ "patch",
2244
+ "patch-commit",
2245
+ "patch-remove",
2246
+ "publish",
2247
+ "prune",
2248
+ "rebuild",
2249
+ "remove",
2250
+ "rm",
2251
+ "store",
2252
+ "unlink",
2253
+ "update",
2254
+ "up",
2255
+ "why"
2256
+ ]);
2257
+ var PNPM_EXEC_LIKE_HEADS = /* @__PURE__ */ new Set([
2258
+ "vitest",
2259
+ "vite",
2260
+ "biome",
2261
+ "eslint",
2262
+ "jest",
2263
+ "mocha",
2264
+ "tsc",
2265
+ "tsx",
2266
+ "node"
2267
+ ]);
2205
2268
  function readPackageJson(dir) {
2206
2269
  const packagePath = path9.join(dir, "package.json");
2207
2270
  if (!existsSync4(packagePath)) {
@@ -2256,6 +2319,12 @@ function npmScriptName(tokens) {
2256
2319
  if (launcher[0] === "pnpm" && launcher[1] === "run" && launcher[2]) {
2257
2320
  return launcher[2];
2258
2321
  }
2322
+ if (launcher[0] === "pnpm" && launcher[1] === "test") {
2323
+ return "test";
2324
+ }
2325
+ if (launcher[0] === "pnpm" && launcher[1] && !launcher[1].startsWith("-") && !PNPM_BUILTIN_COMMANDS.has(launcher[1])) {
2326
+ return launcher[1];
2327
+ }
2259
2328
  if (launcher[0] === "npm" && launcher[1] && launcher[1] !== "run" && launcher[1] !== "install") {
2260
2329
  return null;
2261
2330
  }
@@ -2377,11 +2446,31 @@ function resolveLauncherRecipe(params) {
2377
2446
  const tokens = params.tokens;
2378
2447
  const scriptName = npmScriptName(tokens);
2379
2448
  if (scriptName) {
2380
- return resolveNpmRecipe(params.cwd, params.repoRoot, scriptName, forwardedArgs(tokens));
2449
+ const resolution = resolveNpmRecipe(
2450
+ params.cwd,
2451
+ params.repoRoot,
2452
+ scriptName,
2453
+ forwardedArgs(tokens)
2454
+ );
2455
+ if (tokens[0] === "pnpm" && tokens[1] && PNPM_EXEC_LIKE_HEADS.has(tokens[1]) && resolution.reason === "npm_script_undefined") {
2456
+ return {
2457
+ recipes: [tokens.slice(1).join(" ")],
2458
+ opaque: false,
2459
+ reason: "pnpm_exec_like"
2460
+ };
2461
+ }
2462
+ return resolution;
2381
2463
  }
2382
2464
  if (tokens[0] === "make" && tokens[1] && !tokens[1].startsWith("-")) {
2383
2465
  return resolveMakeRecipe(params.cwd, params.repoRoot, tokens[1]);
2384
2466
  }
2467
+ if (tokens[0] === "pnpm" && tokens[1] && PNPM_EXEC_LIKE_HEADS.has(tokens[1])) {
2468
+ return {
2469
+ recipes: [tokens.slice(1).join(" ")],
2470
+ opaque: false,
2471
+ reason: "pnpm_exec_like"
2472
+ };
2473
+ }
2385
2474
  return null;
2386
2475
  }
2387
2476
  function isRoutineLauncher(tokens) {
@@ -2397,7 +2486,7 @@ function matchesCustomCommand(normalizedCommand, key, pattern) {
2397
2486
  return normalizedCommand === trimmed || key === trimmed;
2398
2487
  }
2399
2488
 
2400
- // src/core/v2/overrides.ts
2489
+ // src/core/verdict/overrides.ts
2401
2490
  function matchesCustomPatterns(command, segment, patterns) {
2402
2491
  if (!patterns || patterns.length === 0) {
2403
2492
  return false;
@@ -2436,7 +2525,7 @@ function askFromCustomExternal(opacity) {
2436
2525
  };
2437
2526
  }
2438
2527
 
2439
- // src/core/v2/parser.ts
2528
+ // src/core/verdict/parser.ts
2440
2529
  import path10 from "node:path";
2441
2530
 
2442
2531
  // src/core/shell-substitution.ts
@@ -2660,7 +2749,7 @@ function hasUnbalancedDollarParen(command) {
2660
2749
  return depth > 0;
2661
2750
  }
2662
2751
 
2663
- // src/core/v2/parser.ts
2752
+ // src/core/verdict/parser.ts
2664
2753
  var ENV_PREFIX_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=(?:'[^']*'|"[^"]*"|\S+)$/;
2665
2754
  var TRANSPARENT_WRAPPERS = /* @__PURE__ */ new Set([
2666
2755
  "sudo",
@@ -2860,7 +2949,7 @@ function redactCommand(command) {
2860
2949
  return command.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]").replace(/sk-[A-Za-z0-9]{8,}/g, "sk-[REDACTED]").trim();
2861
2950
  }
2862
2951
 
2863
- // src/core/v2/verdict.ts
2952
+ // src/core/verdict/verdict.ts
2864
2953
  var DEFAULT_MAX_DEPTH = 8;
2865
2954
  var TIER0_EXTERNAL_KEYS = /* @__PURE__ */ new Set([
2866
2955
  "git push",
@@ -2943,6 +3032,7 @@ var LOCAL_ROUTINE_HEADS = /* @__PURE__ */ new Set([
2943
3032
  "make",
2944
3033
  "cmake"
2945
3034
  ]);
3035
+ var BELAY_SELF_COMMANDS = /* @__PURE__ */ new Set(["approve", "revoke"]);
2946
3036
  var FIND_DANGEROUS_FLAGS = /* @__PURE__ */ new Set(["-delete", "-exec", "-execdir", "-ok", "-okdir"]);
2947
3037
  function isFindDangerous(tokens) {
2948
3038
  return tokens.some(
@@ -3090,6 +3180,11 @@ function tier0ExternalMatch(key, head, tokens) {
3090
3180
  }
3091
3181
  return false;
3092
3182
  }
3183
+ function isBelaySelfCommand(tokens) {
3184
+ const head = tokens[0] ?? "";
3185
+ const subcommand = tokens[1] ?? "";
3186
+ return head === "belay" && BELAY_SELF_COMMANDS.has(subcommand);
3187
+ }
3093
3188
  function tier0HighStakesRm(tokens, context) {
3094
3189
  const head = tokens[0] ?? "";
3095
3190
  if (head !== "rm") {
@@ -3321,6 +3416,16 @@ async function evaluateSegment(command, context, depth) {
3321
3416
  if (rmVerdict) {
3322
3417
  return rmVerdict;
3323
3418
  }
3419
+ if (isBelaySelfCommand(peeled)) {
3420
+ return allowVerdict({
3421
+ location: "unknown",
3422
+ opacity: "transparent",
3423
+ effect: "local_mutation",
3424
+ confidence: "deterministic",
3425
+ reason: "belay_control_plane_command",
3426
+ signals: ["belay_control_plane_command", segment.head]
3427
+ });
3428
+ }
3324
3429
  let effect = "unknown";
3325
3430
  if (READ_ONLY_KEYS.has(segment.key) || READ_ONLY_KEYS.has(segment.head)) {
3326
3431
  effect = "read_only";
@@ -3541,7 +3646,7 @@ async function verdict(command, context) {
3541
3646
  );
3542
3647
  }
3543
3648
 
3544
- // src/core/v2/adapter.ts
3649
+ // src/core/verdict/adapter.ts
3545
3650
  function buildVerdictContext(params) {
3546
3651
  const protectedArtifactRoots2 = [
3547
3652
  ...params.options?.protectedArtifactRoots ?? [],
@@ -3588,6 +3693,12 @@ function mapLegacyReason(result) {
3588
3693
  if (result.reason === "repo_local_mutation") {
3589
3694
  return "local_mutation";
3590
3695
  }
3696
+ if (result.reason === "tier1_not_restorable") {
3697
+ return "tier1_catastrophic";
3698
+ }
3699
+ if (result.reason === "tier0_restorable" || result.reason === "tier1_restorable") {
3700
+ return result.effect === "local_mutation" ? "local_mutation" : result.reason;
3701
+ }
3591
3702
  return result.reason;
3592
3703
  }
3593
3704
  function verdictToClassifyResult(result) {
@@ -3608,13 +3719,13 @@ function verdictToClassifyResult(result) {
3608
3719
  assessment,
3609
3720
  normalizedCommand: result.commandRedacted,
3610
3721
  summary: result.commandRedacted,
3611
- v2: {
3722
+ axes: {
3612
3723
  location: result.location,
3613
3724
  opacity: result.opacity,
3614
3725
  effect: result.effect,
3615
3726
  confidence: result.confidence,
3616
3727
  would: result.permission,
3617
- by: "v2",
3728
+ by: "verdict",
3618
3729
  commandRedacted: result.commandRedacted,
3619
3730
  commandFingerprint: result.fingerprint,
3620
3731
  signals: result.signals,
@@ -4064,7 +4175,7 @@ function normalizeGatedAction(params) {
4064
4175
  };
4065
4176
  }
4066
4177
  function applyShellPeripheralPolicy(command, action, result, options) {
4067
- if (options.brokerFsScope && result.verdict === "deny_pending_approval" && (result.reason === "outside_repo_mutation" || result.reason === "outside_repo_redirect" || result.reason === "repo_outside_mutation" || result.v2?.location === "repo_outside")) {
4178
+ if (options.brokerFsScope && result.verdict === "deny_pending_approval" && (result.reason === "outside_repo_mutation" || result.reason === "outside_repo_redirect" || result.reason === "repo_outside_mutation" || result.axes?.location === "repo_outside")) {
4068
4179
  const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot);
4069
4180
  if (outsideRepoPaths.length > 0 && options.fsScopeAllowlist && allPathsAllowlisted(outsideRepoPaths, options.fsScopeAllowlist)) {
4070
4181
  return {
@@ -4743,7 +4854,15 @@ async function consumeApprovedApproval(ctx, deps, kind, fingerprint) {
4743
4854
  await deps.writeApprovals(approved.filePath, approved.state);
4744
4855
  return null;
4745
4856
  }
4746
- const [approval] = approved.state.approvals.splice(index, 1);
4857
+ const approval = approved.state.approvals[index];
4858
+ if (approval.executionLeaseExpiresAt) {
4859
+ await deps.writeApprovals(approved.filePath, approved.state);
4860
+ return approval;
4861
+ }
4862
+ approved.state.approvals[index] = {
4863
+ ...approval,
4864
+ executionLeaseExpiresAt: new Date(Date.now() + APPROVAL_EXECUTION_LEASE_MS).toISOString()
4865
+ };
4747
4866
  await deps.writeApprovals(approved.filePath, approved.state);
4748
4867
  return approval;
4749
4868
  }
@@ -4859,8 +4978,8 @@ async function gateDecisionToVerdict(ctx, deps, kind, result, auditExtras = {})
4859
4978
  predictedAssessment: auditExtras.predictedAssessment,
4860
4979
  observedAssessment: auditExtras.observedAssessment,
4861
4980
  mode: ctx.config.mode,
4862
- schemaVersion: result.v2 ? 2 : 1,
4863
- ...result.v2 ?? {},
4981
+ schemaVersion: result.axes ? 2 : 1,
4982
+ ...result.axes ?? {},
4864
4983
  ...auditExtras.transactionalLayer
4865
4984
  };
4866
4985
  if (result.reason === TRANSACTIONAL_ALREADY_APPLIED) {
package/dist/cli.js CHANGED
@@ -65,10 +65,10 @@ function parseArgs(argv) {
65
65
  }
66
66
  if (token === '--judge-profile') {
67
67
  const next = rest[index + 1];
68
- if (!next || next !== 'local-ollama') {
69
- throw new Error('--judge-profile requires local-ollama.');
68
+ if (!next || !['local-ollama', 'cursor', 'claude', 'codex'].includes(next)) {
69
+ throw new Error('--judge-profile requires local-ollama, cursor, claude, or codex.');
70
70
  }
71
- options.judgeProfile = 'local-ollama';
71
+ options.judgeProfile = next;
72
72
  index += 1;
73
73
  continue;
74
74
  }
@@ -329,7 +329,7 @@ function printHelp() {
329
329
  process.stdout.write(`${c}
330
330
 
331
331
  Usage:
332
- ${c} init [--target <dir>] [--adapter cursor|claude|codex] [--scope project|global] [--preset strict|standard|audit-first|l1-full-recommended] [--judge-profile local-ollama] [--judge-provider ollama|openai-compatible] [--judge-model <id|auto>] [--judge-endpoint <url>] [--accept-cloud-judge] [--with-skill] [--dogfood]
332
+ ${c} init [--target <dir>] [--adapter cursor|claude|codex] [--scope project|global] [--preset strict|standard|audit-first|l1-full-recommended] [--judge-profile local-ollama|cursor|claude|codex] [--judge-provider ollama|openai-compatible] [--judge-model <id|auto>] [--judge-endpoint <url>] [--accept-cloud-judge] [--with-skill] [--dogfood]
333
333
  ${c} init-wizard [--target <dir>]
334
334
  (--dogfood runs after --preset and sets mode: audit, overriding preset enforce mode)
335
335
  ${c} upgrade [--target <dir>] [--adapter cursor|claude|codex] [--scope project|global] [--with-skill]
@@ -5,7 +5,7 @@ import { detectAdapterName, loadConfigFile } from '../config-io.js';
5
5
  import { isCapabilityBrokerDemotionActive } from '../core/capability/broker.js';
6
6
  import { classifySubagent, classifyToolUse } from '../core/index.js';
7
7
  import { isTransactionalEligible } from '../core/transactional/index.js';
8
- import { classifyShell } from '../core/v2/adapter.js';
8
+ import { classifyShell } from '../core/verdict/adapter.js';
9
9
  import { egressStatus } from '../services/egress-service.js';
10
10
  import { sandboxStatus } from '../services/sandbox-service.js';
11
11
  export async function classifyForReport(params) {
@@ -57,11 +57,11 @@ export async function classifyForReport(params) {
57
57
  throw new Error(`Unknown classify kind: ${kind}`);
58
58
  }
59
59
  const transactionalEligible = kind === 'shell' && isTransactionalEligible(config, 'shell', result);
60
- const permission = result.v2?.would ??
60
+ const permission = result.axes?.would ??
61
61
  (result.verdict === 'allow' || result.verdict === 'allow_flagged' ? 'allow' : 'ask');
62
62
  const tier = result.reason.startsWith('tier0_') || result.reason === 'external_effect'
63
63
  ? 'Tier0'
64
- : result.v2?.confidence === 'llm' || result.reason === 'unknown_local_effect'
64
+ : result.axes?.confidence === 'llm' || result.reason === 'unknown_local_effect'
65
65
  ? 'Tier1'
66
66
  : 'deterministic';
67
67
  return {
@@ -90,7 +90,7 @@ export async function doctorProject(options = {}) {
90
90
  ? `Install scope: global (hooks/runtime at ${scopedPaths.hooksDir})`
91
91
  : 'Install scope: project');
92
92
  notes.push(`Config mode: ${loadedConfig.mode}`);
93
- notes.push('Verdict engine: v2 (location × opacity × effect × confidence). Shell gates use the v2 classifier; audit records include schemaVersion 2 axes when available.');
93
+ notes.push('Verdict engine (Tier0 + Tier1; location × opacity × effect × confidence). Audit records include schemaVersion 2 axes when available.');
94
94
  const repoLocalDir = repoLocalStateDirFor(repoRoot, loadedConfig);
95
95
  if (loadedConfig.controlPlane.enabled) {
96
96
  notes.push(`Control plane: ${belayStateDir(loadedConfig, repoLocalDir)}`);
@@ -70,15 +70,15 @@ export async function explainCommand(options) {
70
70
  }
71
71
  export function formatExplainReport(report) {
72
72
  const { result } = report;
73
- const judgeFields = result.v2
73
+ const judgeFields = result.axes
74
74
  ? [
75
- result.v2.judgeProvider ? ` judgeProvider: ${result.v2.judgeProvider}` : null,
76
- result.v2.judgeModelResolved ? ` judgeModel: ${result.v2.judgeModelResolved}` : null,
77
- result.v2.judgeLatencyMs !== undefined
78
- ? ` judgeLatencyMs: ${result.v2.judgeLatencyMs}`
75
+ result.axes.judgeProvider ? ` judgeProvider: ${result.axes.judgeProvider}` : null,
76
+ result.axes.judgeModelResolved ? ` judgeModel: ${result.axes.judgeModelResolved}` : null,
77
+ result.axes.judgeLatencyMs !== undefined
78
+ ? ` judgeLatencyMs: ${result.axes.judgeLatencyMs}`
79
79
  : null,
80
- result.v2.judgeFallbackReason
81
- ? ` judgeFallbackReason: ${result.v2.judgeFallbackReason}`
80
+ result.axes.judgeFallbackReason
81
+ ? ` judgeFallbackReason: ${result.axes.judgeFallbackReason}`
82
82
  : null,
83
83
  ].filter((line) => line !== null)
84
84
  : [];
@@ -106,15 +106,15 @@ export function formatExplainReport(report) {
106
106
  `Verdict: ${result.verdict}`,
107
107
  `Reason: ${result.reason}`,
108
108
  `Fingerprint: ${result.fingerprint}`,
109
- ...(result.v2
109
+ ...(result.axes
110
110
  ? [
111
111
  '',
112
- 'v2 axes:',
113
- ` location: ${result.v2.location}`,
114
- ` opacity: ${result.v2.opacity}`,
115
- ` effect: ${result.v2.effect}`,
116
- ` confidence: ${result.v2.confidence}`,
117
- ` would: ${result.v2.would}`,
112
+ 'verdict axes:',
113
+ ` location: ${result.axes.location}`,
114
+ ` opacity: ${result.axes.opacity}`,
115
+ ` effect: ${result.axes.effect}`,
116
+ ` confidence: ${result.axes.confidence}`,
117
+ ` would: ${result.axes.would}`,
118
118
  ...(judgeFields.length > 0 ? ['judgeTrace:', ...judgeFields] : []),
119
119
  ]
120
120
  : []),
@@ -3,8 +3,13 @@ export interface WizardAnswers {
3
3
  adapter: AdapterName;
4
4
  scope: 'project' | 'global';
5
5
  withSkill: boolean;
6
+ judgeProfile: 'local-ollama' | 'cursor' | 'claude' | 'codex';
6
7
  dogfood: boolean;
7
8
  }
9
+ export declare function parseAdapter(value: string | undefined): AdapterName;
10
+ export declare function parseScope(value: string | undefined): 'project' | 'global';
11
+ export declare function parseYesNo(value: string | undefined, defaultValue: boolean): boolean;
12
+ export declare function parseJudgeProfile(value: string | undefined, defaultProfile: 'local-ollama' | 'cursor' | 'claude' | 'codex'): 'local-ollama' | 'cursor' | 'claude' | 'codex';
8
13
  export declare function buildInitOptionsFromWizard(answers: WizardAnswers, targetDir?: string): InitOptions;
9
14
  export declare function runInitWizard(options?: {
10
15
  targetDir?: string;
@@ -1,22 +1,22 @@
1
1
  import { stdin as input, stdout as output } from 'node:process';
2
2
  import readline from 'node:readline/promises';
3
3
  import { initProject } from '../installer.js';
4
- function parseAdapter(value) {
5
- const normalized = (value ?? 'cursor').trim().toLowerCase();
4
+ export function parseAdapter(value) {
5
+ const normalized = (value?.trim() || 'cursor').toLowerCase();
6
6
  if (normalized === 'claude' || normalized === 'codex' || normalized === 'cursor') {
7
7
  return normalized;
8
8
  }
9
9
  throw new Error(`Unknown adapter: ${value ?? '(empty)'}`);
10
10
  }
11
- function parseScope(value) {
12
- const normalized = (value ?? 'project').trim().toLowerCase();
11
+ export function parseScope(value) {
12
+ const normalized = (value?.trim() || 'project').toLowerCase();
13
13
  if (normalized === 'global' || normalized === 'project') {
14
14
  return normalized;
15
15
  }
16
16
  throw new Error(`Unknown scope: ${value ?? '(empty)'}`);
17
17
  }
18
- function parseYesNo(value, defaultValue) {
19
- const normalized = (value ?? (defaultValue ? 'y' : 'n')).trim().toLowerCase();
18
+ export function parseYesNo(value, defaultValue) {
19
+ const normalized = (value?.trim() || (defaultValue ? 'y' : 'n')).toLowerCase();
20
20
  if (['y', 'yes', 'true', '1'].includes(normalized)) {
21
21
  return true;
22
22
  }
@@ -25,12 +25,23 @@ function parseYesNo(value, defaultValue) {
25
25
  }
26
26
  return defaultValue;
27
27
  }
28
+ export function parseJudgeProfile(value, defaultProfile) {
29
+ const normalized = (value?.trim() || defaultProfile).toLowerCase();
30
+ if (normalized === 'local-ollama' ||
31
+ normalized === 'cursor' ||
32
+ normalized === 'claude' ||
33
+ normalized === 'codex') {
34
+ return normalized;
35
+ }
36
+ throw new Error(`Unknown judge profile: ${value ?? '(empty)'}`);
37
+ }
28
38
  export function buildInitOptionsFromWizard(answers, targetDir) {
29
39
  return {
30
40
  targetDir,
31
41
  adapter: answers.adapter,
32
42
  scope: answers.scope,
33
43
  withSkill: answers.withSkill,
44
+ judgeProfile: answers.judgeProfile,
34
45
  dogfood: answers.dogfood,
35
46
  };
36
47
  }
@@ -38,11 +49,13 @@ export async function runInitWizard(options = {}) {
38
49
  const rl = readline.createInterface({ input, output });
39
50
  try {
40
51
  output.write('belay init wizard\n');
41
- const adapter = parseAdapter(await rl.question('Adapter (cursor/claude/codex) [cursor]: '));
42
- const scope = parseScope(await rl.question('Install scope (project/global) [project]: '));
43
- const withSkill = parseYesNo(await rl.question('Install SKILL.md and slash commands? (y/n) [y]: '), true);
44
- const dogfood = parseYesNo(await rl.question('Start in audit dogfood mode? (y/n) [n]: '), false);
45
- return initProject(buildInitOptionsFromWizard({ adapter, scope, withSkill, dogfood }, options.targetDir));
52
+ const adapter = parseAdapter(await rl.question('Adapter [cursor | claude | codex] (cursor): '));
53
+ const scope = parseScope(await rl.question('Install scope [project | global] (project): '));
54
+ const withSkill = parseYesNo(await rl.question('Install SKILL.md and slash commands? [y | n] (y): '), true);
55
+ // Show Tier1 judge choice explicitly so init defaults are visible in wizard UX.
56
+ const defaultJudgeProfile = adapter;
57
+ const judgeProfile = parseJudgeProfile(await rl.question(`Tier1 judge profile [cursor | claude | codex | local-ollama] (${defaultJudgeProfile}): `), defaultJudgeProfile);
58
+ return initProject(buildInitOptionsFromWizard({ adapter, scope, withSkill, judgeProfile, dogfood: false }, options.targetDir));
46
59
  }
47
60
  finally {
48
61
  rl.close();
@@ -20,8 +20,8 @@ export async function recoverProject(options = {}) {
20
20
  target = {
21
21
  summary: classified.input,
22
22
  reason: classified.result.reason,
23
- effect: classified.result.v2?.effect,
24
- location: classified.result.v2?.location,
23
+ effect: classified.result.axes?.effect,
24
+ location: classified.result.axes?.location,
25
25
  permission: classified.permission,
26
26
  assessment: classified.result.assessment,
27
27
  };
@@ -1,6 +1,9 @@
1
1
  import type { ApprovalRecord, ApprovalStateFile } from './types.js';
2
+ /** Cursor may invoke the same shell gate more than once per retry; lease covers that window. */
3
+ export declare const APPROVAL_EXECUTION_LEASE_MS = 60000;
2
4
  export declare function nowIso(): string;
3
5
  export declare function isExpired(approval: ApprovalRecord): boolean;
6
+ export declare function isExecutionLeaseExpired(approval: ApprovalRecord): boolean;
4
7
  export declare function compactApprovals(state: ApprovalStateFile): ApprovalStateFile;
5
8
  export declare function mergeApprovalStates(target: ApprovalStateFile, source: ApprovalStateFile): ApprovalStateFile;
6
9
  export declare function escapeRegex(value: string): string;