@guilz-dev/belay 0.1.0 → 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 (62) hide show
  1. package/README.md +59 -12
  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 +15 -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 +2 -1
  38. package/package.json +5 -2
  39. package/skills/belay/SKILL.md +19 -5
  40. /package/dist/core/{v2 → verdict}/adapter.d.ts +0 -0
  41. /package/dist/core/{v2 → verdict}/containment.d.ts +0 -0
  42. /package/dist/core/{v2 → verdict}/containment.js +0 -0
  43. /package/dist/core/{v2 → verdict}/egress-classify.d.ts +0 -0
  44. /package/dist/core/{v2 → verdict}/fingerprint.d.ts +0 -0
  45. /package/dist/core/{v2 → verdict}/fingerprint.js +0 -0
  46. /package/dist/core/{v2 → verdict}/index.d.ts +0 -0
  47. /package/dist/core/{v2 → verdict}/index.js +0 -0
  48. /package/dist/core/{v2 → verdict}/judge-audit.d.ts +0 -0
  49. /package/dist/core/{v2 → verdict}/judge-audit.js +0 -0
  50. /package/dist/core/{v2 → verdict}/judge-factory.d.ts +0 -0
  51. /package/dist/core/{v2 → verdict}/judge-factory.js +0 -0
  52. /package/dist/core/{v2 → verdict}/judge-outbound.d.ts +0 -0
  53. /package/dist/core/{v2 → verdict}/judge-outbound.js +0 -0
  54. /package/dist/core/{v2 → verdict}/judge.d.ts +0 -0
  55. /package/dist/core/{v2 → verdict}/launcher-resolve.d.ts +0 -0
  56. /package/dist/core/{v2 → verdict}/overrides.d.ts +0 -0
  57. /package/dist/core/{v2 → verdict}/overrides.js +0 -0
  58. /package/dist/core/{v2 → verdict}/parser.d.ts +0 -0
  59. /package/dist/core/{v2 → verdict}/parser.js +0 -0
  60. /package/dist/core/{v2 → verdict}/types.d.ts +0 -0
  61. /package/dist/core/{v2 → verdict}/types.js +0 -0
  62. /package/dist/core/{v2 → verdict}/verdict.d.ts +0 -0
@@ -1,13 +1,21 @@
1
+ /** Cursor may invoke the same shell gate more than once per retry; lease covers that window. */
2
+ export const APPROVAL_EXECUTION_LEASE_MS = 60_000;
1
3
  export function nowIso() {
2
4
  return new Date().toISOString();
3
5
  }
4
6
  export function isExpired(approval) {
5
7
  return Date.parse(approval.expiresAt) <= Date.now();
6
8
  }
9
+ export function isExecutionLeaseExpired(approval) {
10
+ if (!approval.executionLeaseExpiresAt) {
11
+ return false;
12
+ }
13
+ return Date.parse(approval.executionLeaseExpiresAt) <= Date.now();
14
+ }
7
15
  export function compactApprovals(state) {
8
16
  return {
9
17
  version: state.version,
10
- approvals: state.approvals.filter((approval) => !isExpired(approval)),
18
+ approvals: state.approvals.filter((approval) => !isExpired(approval) && !isExecutionLeaseExpired(approval)),
11
19
  };
12
20
  }
13
21
  export function mergeApprovalStates(target, source) {
@@ -31,8 +39,15 @@ export function escapeRegex(value) {
31
39
  }
32
40
  export function approvalCommandMatch(prompt, tokenPrefix) {
33
41
  const escapedPrefix = escapeRegex(tokenPrefix);
34
- const match = prompt.match(new RegExp(`^\\s*${escapedPrefix}\\s+(\\S+)\\s*$`, 'i'));
35
- return match?.[1] ?? null;
42
+ const linePattern = new RegExp(`^\\s*${escapedPrefix}\\s+(\\S+)\\s*$`, 'i');
43
+ for (const line of prompt.split(/\r?\n/)) {
44
+ if (!line.trim()) {
45
+ continue;
46
+ }
47
+ const match = line.match(linePattern);
48
+ return match?.[1] ?? null;
49
+ }
50
+ return null;
36
51
  }
37
52
  export function buildRetryInstruction(tokenPrefix, approvalId) {
38
53
  return `To allow the next matching action once, send ${tokenPrefix} ${approvalId} and then retry the original action unchanged.`;
@@ -1,6 +1,10 @@
1
1
  import { GATE_EVENTS } from './audit-types.js';
2
2
  export function toAuditRecord(value) {
3
- return value;
3
+ const record = { ...value };
4
+ if (record.by === 'v2') {
5
+ record.by = 'verdict';
6
+ }
7
+ return record;
4
8
  }
5
9
  export function parseTimestamp(value) {
6
10
  if (!value) {
@@ -3,7 +3,7 @@ import { canonicalStringify, toolFingerprint } from './fingerprint.js';
3
3
  import { matchesSensitivePath } from './glob.js';
4
4
  import { pathWithinRoot, relativeWithinRepo } from './path-utils.js';
5
5
  import { scrubValue } from './scrub.js';
6
- import { classifyShell } from './v2/adapter.js';
6
+ import { classifyShell } from './verdict/adapter.js';
7
7
  const DEFAULT_SENSITIVE_PATHS = ['.env', '.env.*', '**/credentials/**'];
8
8
  const FILE_WRITE_TOOL_NAMES = new Set(['write']);
9
9
  const FILE_EDIT_TOOL_NAMES = new Set([
@@ -155,7 +155,7 @@ export declare const DEFAULT_CONFIDENCE_THRESHOLDS: BelayConfidenceThresholds;
155
155
  export declare const DEFAULT_MODEL_ASSIST: BelayModelAssistConfig;
156
156
  export declare const DEFAULT_TRANSACTIONAL_V3: BelayTransactionalConfig;
157
157
  export declare const LEGACY_POLICY_V3: BelayPolicyConfig;
158
- /** Fresh v0.4+ install defaults (fail-closed). */
158
+ /** Fresh install defaults: recoverable-first with opaque/unparseable fail-closed. */
159
159
  export declare const DEFAULT_POLICY_V3: BelayPolicyConfig;
160
160
  export declare const DEFAULT_OVERRIDES_V3: BelayOverridesConfig;
161
161
  export declare const DEFAULT_REDACTION_V3: BelayRedactionConfig;
@@ -45,9 +45,9 @@ export const LEGACY_POLICY_V3 = {
45
45
  transactional: { ...DEFAULT_TRANSACTIONAL_V3 },
46
46
  fenceWarnThreshold: DEFAULT_FENCE_WARN_THRESHOLD,
47
47
  };
48
- /** Fresh v0.4+ install defaults (fail-closed). */
48
+ /** Fresh install defaults: recoverable-first with opaque/unparseable fail-closed. */
49
49
  export const DEFAULT_POLICY_V3 = {
50
- unknownLocalEffect: 'deny',
50
+ unknownLocalEffect: 'allow_flagged',
51
51
  unparseableShell: 'deny',
52
52
  codexUnmappedTool: 'deny',
53
53
  confidenceThresholds: { ...DEFAULT_CONFIDENCE_THRESHOLDS },
@@ -28,7 +28,7 @@ export interface GateVerdict extends GatePermissionResponse {
28
28
  approvalId?: string;
29
29
  wouldBlock: boolean;
30
30
  mode: 'enforce' | 'audit';
31
- v2?: ClassifyResult['v2'];
31
+ axes?: ClassifyResult['axes'];
32
32
  }
33
33
  export declare function isGatedAction(value: unknown): value is GatedAction;
34
34
  export declare function classifyResultToGateVerdict(params: {
@@ -25,7 +25,7 @@ export function classifyResultToGateVerdict(params) {
25
25
  approvalId,
26
26
  user_message,
27
27
  agent_message,
28
- v2: result.v2,
28
+ axes: result.axes,
29
29
  };
30
30
  }
31
31
  export function unnormalizedGateVerdict(params) {
@@ -5,7 +5,7 @@ import { classifyToolUse } from './classify-tool.js';
5
5
  import { classifierOptionsFromConfig } from './config.js';
6
6
  import { GATE_CONTRACT_VERSION } from './gate-contract.js';
7
7
  import { mergeAgentAssessment } from './judgment.js';
8
- import { classifyShell } from './v2/adapter.js';
8
+ import { classifyShell } from './verdict/adapter.js';
9
9
  export class GateNormalizationError extends Error {
10
10
  reason = 'normalization_failed';
11
11
  constructor(message) {
@@ -98,7 +98,7 @@ function applyShellPeripheralPolicy(command, action, result, options) {
98
98
  (result.reason === 'outside_repo_mutation' ||
99
99
  result.reason === 'outside_repo_redirect' ||
100
100
  result.reason === 'repo_outside_mutation' ||
101
- result.v2?.location === 'repo_outside')) {
101
+ result.axes?.location === 'repo_outside')) {
102
102
  const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot);
103
103
  if (outsideRepoPaths.length > 0 &&
104
104
  options.fsScopeAllowlist &&
@@ -1,4 +1,4 @@
1
- export { approvalCommandMatch, buildRetryInstruction, compactApprovals, createApprovalRecord, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
1
+ export { APPROVAL_EXECUTION_LEASE_MS, approvalCommandMatch, buildRetryInstruction, compactApprovals, createApprovalRecord, isExecutionLeaseExpired, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
2
2
  export type { AuditMetricsReport } from './audit-metrics.js';
3
3
  export { computeAuditMetrics, parseAuditNdjson } from './audit-metrics.js';
4
4
  export { classifySubagent } from './classify-subagent.js';
@@ -16,4 +16,4 @@ export { findCommandSubstitutions, MAX_SUBSTITUTION_DEPTH } from './shell-substi
16
16
  export type { TransactionalDiffEvaluation, TransactionalExecutionResult, } from './transactional/index.js';
17
17
  export { isTransactionalEligible, runTransactionalExecution } from './transactional/index.js';
18
18
  export type { ApprovalRecord, ApprovalStateFile, Assessment, ClassifierOptions, ClassifyResult, HookVerdict, Reversibility, ScrubOptions, UnknownLocalEffectPolicy, } from './types.js';
19
- export { buildVerdictContext, classifyShell, verdict, verdictToClassifyResult, } from './v2/index.js';
19
+ export { buildVerdictContext, classifyShell, verdict, verdictToClassifyResult, } from './verdict/index.js';
@@ -1,4 +1,4 @@
1
- export { approvalCommandMatch, buildRetryInstruction, compactApprovals, createApprovalRecord, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
1
+ export { APPROVAL_EXECUTION_LEASE_MS, approvalCommandMatch, buildRetryInstruction, compactApprovals, createApprovalRecord, isExecutionLeaseExpired, isExpired, mergeApprovalStates, nowIso, } from './approval.js';
2
2
  export { computeAuditMetrics, parseAuditNdjson } from './audit-metrics.js';
3
3
  export { classifySubagent } from './classify-subagent.js';
4
4
  export { classifyToolUse } from './classify-tool.js';
@@ -12,4 +12,4 @@ export { canonicalPath, hasOutsideRepoPath, normalizeToken, pathWithinRoot, rela
12
12
  export { scrubString, scrubValue } from './scrub.js';
13
13
  export { findCommandSubstitutions, MAX_SUBSTITUTION_DEPTH } from './shell-substitution.js';
14
14
  export { isTransactionalEligible, runTransactionalExecution } from './transactional/index.js';
15
- export { buildVerdictContext, classifyShell, verdict, verdictToClassifyResult, } from './v2/index.js';
15
+ export { buildVerdictContext, classifyShell, verdict, verdictToClassifyResult, } from './verdict/index.js';
@@ -1,6 +1,9 @@
1
1
  import type { BelayJudgeConfig } from './config.js';
2
- export type JudgeProfileName = 'local-ollama';
2
+ export type JudgeProfileName = 'local-ollama' | 'cursor' | 'claude' | 'codex';
3
3
  export declare const JUDGE_PROFILE_LOCAL_OLLAMA: BelayJudgeConfig;
4
+ export declare const JUDGE_PROFILE_CURSOR: BelayJudgeConfig;
5
+ export declare const JUDGE_PROFILE_CLAUDE: BelayJudgeConfig;
6
+ export declare const JUDGE_PROFILE_CODEX: BelayJudgeConfig;
4
7
  export declare const JUDGE_PROFILES: Record<JudgeProfileName, BelayJudgeConfig>;
5
8
  export interface ResolveJudgeConfigInput {
6
9
  judgeProfile?: JudgeProfileName;
@@ -26,4 +29,5 @@ export declare function resolveInitJudgeConfig(input: {
26
29
  judgeEndpoint?: string;
27
30
  acceptCloudJudge?: boolean;
28
31
  existingJudge?: BelayJudgeConfig;
32
+ defaultJudgeProfile?: JudgeProfileName;
29
33
  }): BelayJudgeConfig;
@@ -6,8 +6,24 @@ export const JUDGE_PROFILE_LOCAL_OLLAMA = {
6
6
  timeoutMs: 25000,
7
7
  keepAlive: '30m',
8
8
  };
9
+ export const JUDGE_PROFILE_CURSOR = {
10
+ provider: 'openai-compatible',
11
+ model: 'auto',
12
+ endpoint: 'https://api.openai.com/v1',
13
+ timeoutMs: 8000,
14
+ keepAlive: null,
15
+ };
16
+ export const JUDGE_PROFILE_CLAUDE = {
17
+ ...JUDGE_PROFILE_CURSOR,
18
+ };
19
+ export const JUDGE_PROFILE_CODEX = {
20
+ ...JUDGE_PROFILE_CURSOR,
21
+ };
9
22
  export const JUDGE_PROFILES = {
10
23
  'local-ollama': JUDGE_PROFILE_LOCAL_OLLAMA,
24
+ cursor: JUDGE_PROFILE_CURSOR,
25
+ claude: JUDGE_PROFILE_CLAUDE,
26
+ codex: JUDGE_PROFILE_CODEX,
11
27
  };
12
28
  export function resolveJudgeConfig(input = {}) {
13
29
  if (input.judgeProvider) {
@@ -81,5 +97,5 @@ export function resolveInitJudgeConfig(input) {
81
97
  assertJudgeEndpoint(judge);
82
98
  return judge;
83
99
  }
84
- return resolveJudgeConfig({ judgeProfile: 'local-ollama' });
100
+ return resolveJudgeConfig({ judgeProfile: input.defaultJudgeProfile ?? 'cursor' });
85
101
  }
@@ -1,8 +1,8 @@
1
1
  import { normalizeJudgeProvider, scrubOptionsFromConfig } from './config.js';
2
2
  import { resolveJudgeApiKey } from './judge-api-key.js';
3
3
  import { assertJudgeEndpoint } from './judge-config.js';
4
- import { createOllamaJudge, createOpenAiCompatibleJudge } from './v2/judge.js';
5
- import { loadPinnedJudgeModels, resolveCloudModel } from './v2/judge-factory.js';
4
+ import { createOllamaJudge, createOpenAiCompatibleJudge } from './verdict/judge.js';
5
+ import { loadPinnedJudgeModels, resolveCloudModel } from './verdict/judge-factory.js';
6
6
  export async function diagnoseJudge(config) {
7
7
  const issues = [];
8
8
  const warnings = [];
@@ -9,7 +9,7 @@ export interface Assessment {
9
9
  confidence: number;
10
10
  signals: string[];
11
11
  }
12
- export interface V2TraceFields {
12
+ export interface VerdictAxes {
13
13
  location: string;
14
14
  opacity: string;
15
15
  effect: string;
@@ -33,7 +33,7 @@ export interface ClassifyResult {
33
33
  assessment: Assessment;
34
34
  normalizedCommand?: string;
35
35
  summary?: string;
36
- v2?: V2TraceFields;
36
+ axes?: VerdictAxes;
37
37
  }
38
38
  export type UnknownLocalEffectPolicy = 'allow_flagged' | 'deny';
39
39
  export type UnparseableShellPolicy = 'allow_flagged' | 'deny';
@@ -68,7 +68,7 @@ export interface ClassifierOptions {
68
68
  brokerFsScope?: boolean;
69
69
  fsScopeAllowlist?: FsScopeAllowlistFile;
70
70
  /** Test override: inject Tier1 judge without changing config.judge. */
71
- tier1Judge?: import('./v2/types.js').Tier1Judge;
71
+ tier1Judge?: import('./verdict/types.js').Tier1Judge;
72
72
  }
73
73
  export interface ApprovalRecord {
74
74
  approvalId: string;
@@ -80,6 +80,8 @@ export interface ApprovalRecord {
80
80
  createdAt: string;
81
81
  expiresAt: string;
82
82
  approvedAt?: string;
83
+ /** Short-lived lease so duplicate hook invocations for one retry can share approval. */
84
+ executionLeaseExpiresAt?: string;
83
85
  /** Original gated input for explain-last-ask (ApprovalState v2). */
84
86
  input?: string;
85
87
  inputKind?: 'shell' | 'tool' | 'subagent';
@@ -48,6 +48,12 @@ function mapLegacyReason(result) {
48
48
  if (result.reason === 'repo_local_mutation') {
49
49
  return 'local_mutation';
50
50
  }
51
+ if (result.reason === 'tier1_not_restorable') {
52
+ return 'tier1_catastrophic';
53
+ }
54
+ if (result.reason === 'tier0_restorable' || result.reason === 'tier1_restorable') {
55
+ return result.effect === 'local_mutation' ? 'local_mutation' : result.reason;
56
+ }
51
57
  return result.reason;
52
58
  }
53
59
  export function verdictToClassifyResult(result) {
@@ -87,13 +93,13 @@ export function verdictToClassifyResult(result) {
87
93
  assessment,
88
94
  normalizedCommand: result.commandRedacted,
89
95
  summary: result.commandRedacted,
90
- v2: {
96
+ axes: {
91
97
  location: result.location,
92
98
  opacity: result.opacity,
93
99
  effect: result.effect,
94
100
  confidence: result.confidence,
95
101
  would: result.permission,
96
- by: 'v2',
102
+ by: 'verdict',
97
103
  commandRedacted: result.commandRedacted,
98
104
  commandFingerprint: result.fingerprint,
99
105
  signals: result.signals,
@@ -111,7 +117,7 @@ export function verdictAuditFields(result) {
111
117
  effect: result.effect,
112
118
  confidence: result.confidence,
113
119
  would: result.permission,
114
- by: 'v2',
120
+ by: 'verdict',
115
121
  signals: result.signals,
116
122
  ...judgeTraceAuditFields(result.judgeTrace),
117
123
  };
@@ -102,6 +102,9 @@ function classifyAws(tokens) {
102
102
  if (/\bs3\s+rm\b/.test(joined)) {
103
103
  return 'destructive';
104
104
  }
105
+ if (/\bs3\s+mb\b/.test(joined)) {
106
+ return 'destructive';
107
+ }
105
108
  if (/\bs3\s+sync\b/.test(joined)) {
106
109
  return 'destructive';
107
110
  }
@@ -2,12 +2,12 @@ import { scrubOutboundForJudge } from './judge-outbound.js';
2
2
  const SECRET_PATTERNS = [/\.env\b/i, /\.pem\b/i, /id_rsa\b/i, /credentials/i, /secrets?\b/i];
3
3
  const DESTRUCTIVE_VERBS = /\b(rm|rmtree|unlink|delete|truncate|shred|destroy|drop)\b/i;
4
4
  const GIT_PATTERNS = /\.git\b/i;
5
- const TIER1_PROMPT = `Answer ONLY with JSON: {"external_change":boolean,"destroys_outside_repo":boolean,"destroys_history_or_secrets":boolean,"reason":string}
6
- Default all three booleans to true when unsure. reason should be a short snake_case label.
5
+ const TIER1_PROMPT = `Answer ONLY with JSON: {"external_change":boolean,"reason":string}
6
+ reason should be a short snake_case label.
7
7
 
8
- external_change: CREATE/MODIFY/DELETE on database, network endpoint, cloud, remote git, registry, queue. READ/QUERY/GET is NOT a change.
9
- destroys_outside_repo: DELETE/OVERWRITE files OUTSIDE the current repository working tree.
10
- destroys_history_or_secrets: DELETE/OVERWRITE .git directory OR secret/credential files (.env, *.pem, id_rsa, credentials).
8
+ 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.
9
+ 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.
10
+ If unsure, answer true.
11
11
 
12
12
  Command/code:
13
13
  `;
@@ -22,15 +22,13 @@ function failClosedVerdict(reason) {
22
22
  function parseTier1Json(raw) {
23
23
  try {
24
24
  const parsed = JSON.parse(raw);
25
- if (typeof parsed.external_change !== 'boolean' ||
26
- typeof parsed.destroys_outside_repo !== 'boolean' ||
27
- typeof parsed.destroys_history_or_secrets !== 'boolean') {
25
+ if (typeof parsed.external_change !== 'boolean') {
28
26
  return null;
29
27
  }
30
28
  return {
31
- external_change: parsed.external_change !== false,
32
- destroys_outside_repo: parsed.destroys_outside_repo !== false,
33
- destroys_history_or_secrets: parsed.destroys_history_or_secrets !== false,
29
+ external_change: parsed.external_change,
30
+ destroys_outside_repo: false,
31
+ destroys_history_or_secrets: false,
34
32
  reason: typeof parsed.reason === 'string' ? parsed.reason : 'tier1_llm',
35
33
  };
36
34
  }
@@ -260,5 +258,5 @@ export function createOpenAiCompatibleJudge(options) {
260
258
  /** @deprecated Use createOpenAiCompatibleJudge */
261
259
  export const createCursorJudge = createOpenAiCompatibleJudge;
262
260
  export function tier1RequiresAsk(verdict) {
263
- return (verdict.external_change || verdict.destroys_outside_repo || verdict.destroys_history_or_secrets);
261
+ return verdict.external_change || verdict.destroys_history_or_secrets;
264
262
  }
@@ -1,6 +1,50 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  const MAX_RESOLVE_DEPTH = 8;
4
+ const PNPM_BUILTIN_COMMANDS = new Set([
5
+ 'add',
6
+ 'audit',
7
+ 'cache',
8
+ 'config',
9
+ 'deploy',
10
+ 'dlx',
11
+ 'exec',
12
+ 'fetch',
13
+ 'help',
14
+ 'import',
15
+ 'init',
16
+ 'install',
17
+ 'i',
18
+ 'licenses',
19
+ 'link',
20
+ 'list',
21
+ 'outdated',
22
+ 'pack',
23
+ 'patch',
24
+ 'patch-commit',
25
+ 'patch-remove',
26
+ 'publish',
27
+ 'prune',
28
+ 'rebuild',
29
+ 'remove',
30
+ 'rm',
31
+ 'store',
32
+ 'unlink',
33
+ 'update',
34
+ 'up',
35
+ 'why',
36
+ ]);
37
+ const PNPM_EXEC_LIKE_HEADS = new Set([
38
+ 'vitest',
39
+ 'vite',
40
+ 'biome',
41
+ 'eslint',
42
+ 'jest',
43
+ 'mocha',
44
+ 'tsc',
45
+ 'tsx',
46
+ 'node',
47
+ ]);
4
48
  function readPackageJson(dir) {
5
49
  const packagePath = path.join(dir, 'package.json');
6
50
  if (!existsSync(packagePath)) {
@@ -57,6 +101,15 @@ function npmScriptName(tokens) {
57
101
  if (launcher[0] === 'pnpm' && launcher[1] === 'run' && launcher[2]) {
58
102
  return launcher[2];
59
103
  }
104
+ if (launcher[0] === 'pnpm' && launcher[1] === 'test') {
105
+ return 'test';
106
+ }
107
+ if (launcher[0] === 'pnpm' &&
108
+ launcher[1] &&
109
+ !launcher[1].startsWith('-') &&
110
+ !PNPM_BUILTIN_COMMANDS.has(launcher[1])) {
111
+ return launcher[1];
112
+ }
60
113
  if (launcher[0] === 'npm' && launcher[1] && launcher[1] !== 'run' && launcher[1] !== 'install') {
61
114
  return null;
62
115
  }
@@ -176,11 +229,29 @@ export function resolveLauncherRecipe(params) {
176
229
  const tokens = params.tokens;
177
230
  const scriptName = npmScriptName(tokens);
178
231
  if (scriptName) {
179
- return resolveNpmRecipe(params.cwd, params.repoRoot, scriptName, forwardedArgs(tokens));
232
+ const resolution = resolveNpmRecipe(params.cwd, params.repoRoot, scriptName, forwardedArgs(tokens));
233
+ if (tokens[0] === 'pnpm' &&
234
+ tokens[1] &&
235
+ PNPM_EXEC_LIKE_HEADS.has(tokens[1]) &&
236
+ resolution.reason === 'npm_script_undefined') {
237
+ return {
238
+ recipes: [tokens.slice(1).join(' ')],
239
+ opaque: false,
240
+ reason: 'pnpm_exec_like',
241
+ };
242
+ }
243
+ return resolution;
180
244
  }
181
245
  if (tokens[0] === 'make' && tokens[1] && !tokens[1].startsWith('-')) {
182
246
  return resolveMakeRecipe(params.cwd, params.repoRoot, tokens[1]);
183
247
  }
248
+ if (tokens[0] === 'pnpm' && tokens[1] && PNPM_EXEC_LIKE_HEADS.has(tokens[1])) {
249
+ return {
250
+ recipes: [tokens.slice(1).join(' ')],
251
+ opaque: false,
252
+ reason: 'pnpm_exec_like',
253
+ };
254
+ }
184
255
  return null;
185
256
  }
186
257
  export function isRoutineLauncher(tokens) {
@@ -91,6 +91,7 @@ const LOCAL_ROUTINE_HEADS = new Set([
91
91
  'make',
92
92
  'cmake',
93
93
  ]);
94
+ const BELAY_SELF_COMMANDS = new Set(['approve', 'revoke']);
94
95
  const FIND_DANGEROUS_FLAGS = new Set(['-delete', '-exec', '-execdir', '-ok', '-okdir']);
95
96
  function isFindDangerous(tokens) {
96
97
  return tokens.some((token) => FIND_DANGEROUS_FLAGS.has(token) || token.startsWith('-exec') || token.startsWith('-ok'));
@@ -256,6 +257,11 @@ function tier0ExternalMatch(key, head, tokens) {
256
257
  }
257
258
  return false;
258
259
  }
260
+ function isBelaySelfCommand(tokens) {
261
+ const head = tokens[0] ?? '';
262
+ const subcommand = tokens[1] ?? '';
263
+ return head === 'belay' && BELAY_SELF_COMMANDS.has(subcommand);
264
+ }
259
265
  function tier0HighStakesRm(tokens, context) {
260
266
  const head = tokens[0] ?? '';
261
267
  if (head !== 'rm') {
@@ -488,6 +494,16 @@ async function evaluateSegment(command, context, depth) {
488
494
  if (rmVerdict) {
489
495
  return rmVerdict;
490
496
  }
497
+ if (isBelaySelfCommand(peeled)) {
498
+ return allowVerdict({
499
+ location: 'unknown',
500
+ opacity: 'transparent',
501
+ effect: 'local_mutation',
502
+ confidence: 'deterministic',
503
+ reason: 'belay_control_plane_command',
504
+ signals: ['belay_control_plane_command', segment.head],
505
+ });
506
+ }
491
507
  let effect = 'unknown';
492
508
  if (READ_ONLY_KEYS.has(segment.key) || READ_ONLY_KEYS.has(segment.head)) {
493
509
  effect = 'read_only';
@@ -2,8 +2,8 @@ import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { classifierOptionsFromConfig, DEFAULT_CONFIG_V3 } from '../core/config.js';
5
- import { classifyShell } from '../core/v2/adapter.js';
6
- import { createDeterministicJudgeStub } from '../core/v2/judge.js';
5
+ import { classifyShell } from '../core/verdict/adapter.js';
6
+ import { createDeterministicJudgeStub } from '../core/verdict/judge.js';
7
7
  export function assessmentsDiverge(predicted, observed) {
8
8
  return (predicted.reversibility !== observed.reversibility ||
9
9
  predicted.external !== observed.external ||
package/dist/installer.js CHANGED
@@ -126,6 +126,7 @@ async function applyInitJudgeConfig(repoRoot, adapterName, options) {
126
126
  judgeEndpoint: options.judgeEndpoint,
127
127
  acceptCloudJudge: options.acceptCloudJudge,
128
128
  existingJudge: mergedConfig.judge,
129
+ defaultJudgeProfile: adapterName,
129
130
  });
130
131
  const configWithJudge = normalizeConfig({ ...mergedConfig, version: 4, judge });
131
132
  await writeConfigFile(repoRoot, configWithJudge, adapterName);
package/dist/types.d.ts CHANGED
@@ -20,7 +20,7 @@ export interface InitOptions {
20
20
  adapter?: AdapterName;
21
21
  scope?: InstallScope;
22
22
  preset?: import('./presets.js').ConfigPresetName;
23
- judgeProfile?: 'local-ollama';
23
+ judgeProfile?: 'local-ollama' | 'cursor' | 'claude' | 'codex';
24
24
  judgeProvider?: 'ollama' | 'openai-compatible' | 'cursor';
25
25
  judgeModel?: string;
26
26
  judgeEndpoint?: string;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const PACKAGE_VERSION = "0.0.1";
1
+ export declare const PACKAGE_VERSION = "0.2.0";
package/dist/version.js CHANGED
@@ -1 +1,2 @@
1
- export const PACKAGE_VERSION = '0.0.1';
1
+ // Generated by scripts/sync-version.mjs — do not edit.
2
+ export const PACKAGE_VERSION = '0.2.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guilz-dev/belay",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Belay-style approval and audit gating for agent runtimes.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,10 +48,13 @@
48
48
  "agent-belay-logo.png"
49
49
  ],
50
50
  "scripts": {
51
- "build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/build-runtime.mjs",
51
+ "build": "rm -rf dist && node scripts/sync-version.mjs && tsc -p tsconfig.build.json && node scripts/build-runtime.mjs",
52
+ "check:version": "node scripts/check-cli-version.mjs",
53
+ "prepublishOnly": "pnpm build && node scripts/check-cli-version.mjs",
52
54
  "lint": "biome check src package.json README.md CHANGELOG.md CONTRIBUTING.md SECURITY.md tsconfig.json tsconfig.build.json vitest.config.ts scripts docs",
53
55
  "typecheck": "tsc --noEmit",
54
56
  "test": "pnpm build && vitest run",
57
+ "test:structural": "pnpm build && vitest run src/__tests__/verdict/structural-suite.test.ts",
55
58
  "test:stable": "pnpm build && vitest run && vitest run && vitest run",
56
59
  "corpus": "pnpm build && node scripts/corpus.mjs"
57
60
  },
@@ -2,16 +2,30 @@
2
2
  name: belay
3
3
  description: >-
4
4
  Guides approval when belay blocks a high-risk shell command, subagent launch,
5
- or tool action. Use when an action is denied, blocked, or needs belay-approve, or when
6
- installing or checking belay hook health in a repository.
5
+ or tool action across Cursor, Claude Code, and Codex. Use when an action is
6
+ denied, blocked, or needs belay-approve, or when installing or checking belay
7
+ hook health in a repository.
7
8
  disable-model-invocation: true
8
9
  ---
9
10
 
10
11
  # Belay
11
12
 
12
- Belay installs repo-local hooks that gate high-risk shell commands, tool actions, and
13
- subagent launches. Enforcement lives in hooks; this skill only explains the flow and
14
- routes you to the CLI. It does not classify commands itself.
13
+ Belay is a safety gate for coding agents: it inspects each shell command,
14
+ subagent launch, and file mutation *before* it runs, lets safe-and-local actions
15
+ through, and holds back only the irreversible-and-catastrophic ones for one-shot
16
+ human approval. Every decision is written to an audit log.
17
+
18
+ It runs on **Cursor**, **Claude Code**, and **Codex (experimental)**, wiring the
19
+ same classifier into each agent through its native hooks:
20
+
21
+ | Agent | Hook config |
22
+ | --- | --- |
23
+ | Cursor | `.cursor/hooks.json` |
24
+ | Claude Code | `.claude/settings.json` |
25
+ | Codex | `.codex/config.toml` |
26
+
27
+ Enforcement lives in those hooks; this skill only explains the flow and routes
28
+ you to the CLI. It does not classify commands itself.
15
29
 
16
30
  ## Prerequisites
17
31
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes