@guilz-dev/belay 0.9.3 → 0.10.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 (110) hide show
  1. package/README.md +38 -3
  2. package/dist/adapters/codex/hooks.d.ts +1 -0
  3. package/dist/adapters/codex/hooks.js +3 -0
  4. package/dist/adapters/cursor/dispatcher-generation.d.ts +3 -0
  5. package/dist/adapters/cursor/dispatcher-generation.js +5 -0
  6. package/dist/adapters/cursor/hook-dispatch-entry.d.ts +10 -0
  7. package/dist/adapters/cursor/hook-dispatch-entry.js +137 -0
  8. package/dist/adapters/cursor/hook-router.d.ts +23 -0
  9. package/dist/adapters/cursor/hook-router.js +273 -0
  10. package/dist/adapters/cursor/hooks.d.ts +5 -1
  11. package/dist/adapters/cursor/hooks.js +112 -10
  12. package/dist/adapters/cursor/routing-config-trust.d.ts +1 -0
  13. package/dist/adapters/cursor/routing-config-trust.js +67 -0
  14. package/dist/adapters/cursor/routing-layout.d.ts +5 -0
  15. package/dist/adapters/cursor/routing-layout.js +29 -0
  16. package/dist/adapters/cursor/runtime-entry.d.ts +17 -0
  17. package/dist/adapters/cursor/runtime-entry.js +74 -49
  18. package/dist/adapters/layouts/scope.d.ts +5 -0
  19. package/dist/adapters/layouts/scope.js +71 -0
  20. package/dist/adapters/shared/gate-runtime.js +29 -17
  21. package/dist/bundle/claude-runtime.mjs +2428 -2072
  22. package/dist/bundle/codex-runtime.mjs +2440 -2084
  23. package/dist/bundle/cursor-dispatcher.mjs +517 -0
  24. package/dist/bundle/cursor-runtime.mjs +2521 -2133
  25. package/dist/cli.js +53 -4
  26. package/dist/commands/approval-token.d.ts +10 -0
  27. package/dist/commands/approval-token.js +26 -0
  28. package/dist/commands/audit.d.ts +2 -1
  29. package/dist/commands/audit.js +2 -2
  30. package/dist/commands/config.d.ts +1 -1
  31. package/dist/commands/config.js +28 -2
  32. package/dist/commands/doctor.js +149 -17
  33. package/dist/commands/dogfood-check.d.ts +3 -0
  34. package/dist/commands/dogfood-check.js +116 -0
  35. package/dist/commands/dogfood.d.ts +1 -0
  36. package/dist/commands/dogfood.js +4 -3
  37. package/dist/commands/health-snapshot.js +9 -4
  38. package/dist/commands/judge.js +2 -2
  39. package/dist/commands/recovery-checkpoints.d.ts +3 -3
  40. package/dist/commands/recovery-checkpoints.js +2 -11
  41. package/dist/config-io.d.ts +1 -0
  42. package/dist/config-io.js +19 -3
  43. package/dist/core/audit-legacy-archive.js +43 -25
  44. package/dist/core/classify-tool.js +16 -8
  45. package/dist/core/dogfood-environment.d.ts +8 -0
  46. package/dist/core/dogfood-environment.js +55 -0
  47. package/dist/core/effect-ir/argv-delegate.d.ts +9 -0
  48. package/dist/core/effect-ir/argv-delegate.js +42 -0
  49. package/dist/core/effect-ir/shell-lower/argv-delegate-gate.d.ts +3 -0
  50. package/dist/core/effect-ir/shell-lower/argv-delegate-gate.js +40 -0
  51. package/dist/core/effect-ir/shell-lower/augment.d.ts +4 -0
  52. package/dist/core/effect-ir/shell-lower/augment.js +97 -0
  53. package/dist/core/effect-ir/shell-lower/context.d.ts +10 -0
  54. package/dist/core/effect-ir/shell-lower/context.js +1 -0
  55. package/dist/core/effect-ir/shell-lower/decode-process.d.ts +9 -0
  56. package/dist/core/effect-ir/shell-lower/decode-process.js +201 -0
  57. package/dist/core/effect-ir/shell-lower/decoders/belay.d.ts +2 -0
  58. package/dist/core/effect-ir/shell-lower/decoders/belay.js +41 -0
  59. package/dist/core/effect-ir/shell-lower/decoders/builtins.d.ts +7 -0
  60. package/dist/core/effect-ir/shell-lower/decoders/builtins.js +194 -0
  61. package/dist/core/effect-ir/shell-lower/decoders/decode-package-exec.d.ts +4 -0
  62. package/dist/core/effect-ir/shell-lower/decoders/decode-package-exec.js +68 -0
  63. package/dist/core/effect-ir/shell-lower/decoders/docker.d.ts +3 -0
  64. package/dist/core/effect-ir/shell-lower/decoders/docker.js +43 -0
  65. package/dist/core/effect-ir/shell-lower/decoders/filesystem.d.ts +9 -0
  66. package/dist/core/effect-ir/shell-lower/decoders/filesystem.js +217 -0
  67. package/dist/core/effect-ir/shell-lower/decoders/prisma.d.ts +2 -0
  68. package/dist/core/effect-ir/shell-lower/decoders/prisma.js +54 -0
  69. package/dist/core/effect-ir/shell-lower/decoders/ruby.d.ts +11 -0
  70. package/dist/core/effect-ir/shell-lower/decoders/ruby.js +147 -0
  71. package/dist/core/effect-ir/shell-lower/decoders/toolchain.d.ts +7 -0
  72. package/dist/core/effect-ir/shell-lower/decoders/toolchain.js +155 -0
  73. package/dist/core/effect-ir/shell-lower/requirement.d.ts +12 -0
  74. package/dist/core/effect-ir/shell-lower/requirement.js +65 -0
  75. package/dist/core/effect-ir/shell-lower/segment.d.ts +11 -0
  76. package/dist/core/effect-ir/shell-lower/segment.js +82 -0
  77. package/dist/core/effect-ir/shell-lower/tokens.d.ts +26 -0
  78. package/dist/core/effect-ir/shell-lower/tokens.js +183 -0
  79. package/dist/core/effect-ir/shell-lower.d.ts +2 -7
  80. package/dist/core/effect-ir/shell-lower.js +60 -1502
  81. package/dist/core/egress-approval.js +0 -18
  82. package/dist/core/glob.js +32 -16
  83. package/dist/core/integrity.d.ts +2 -2
  84. package/dist/core/integrity.js +52 -12
  85. package/dist/core/notify.d.ts +8 -2
  86. package/dist/core/notify.js +63 -7
  87. package/dist/core/recovery/operator-guidance.js +4 -4
  88. package/dist/core/replay-scrub.d.ts +2 -0
  89. package/dist/core/replay-scrub.js +8 -0
  90. package/dist/core/repo-config-trust.d.ts +31 -0
  91. package/dist/core/repo-config-trust.js +152 -0
  92. package/dist/core/shell-substitution.js +2 -2
  93. package/dist/core/shell-unparseable.js +3 -1
  94. package/dist/corpus/benign-probe-cores.d.ts +1 -1
  95. package/dist/corpus/benign-probe-cores.js +0 -1
  96. package/dist/defaults.d.ts +7 -3
  97. package/dist/defaults.js +29 -47
  98. package/dist/installer/bootstrap.d.ts +1 -0
  99. package/dist/installer/bootstrap.js +2 -2
  100. package/dist/installer/runtime-artifacts.js +43 -13
  101. package/dist/installer/scope-config.js +2 -2
  102. package/dist/installer.js +48 -13
  103. package/dist/node-resolution.d.ts +1 -0
  104. package/dist/node-resolution.js +61 -0
  105. package/dist/templates.d.ts +7 -4
  106. package/dist/templates.js +42 -4
  107. package/dist/types.d.ts +20 -0
  108. package/dist/version.d.ts +1 -1
  109. package/dist/version.js +1 -1
  110. package/package.json +5 -1
package/dist/defaults.js CHANGED
@@ -1,62 +1,42 @@
1
1
  import path from 'node:path';
2
2
  import { cursorLayout } from './adapters/layouts/cursor.js';
3
- import { buildRunnerInvocation } from './adapters/layouts/scope.js';
3
+ import { buildAbsoluteRunnerInvocation, buildLegacyAbsoluteRunnerInvocation, buildLegacyBarePowerShellRunnerInvocation, buildLegacyQuotedAbsoluteRunnerInvocation, buildRunnerInvocation, } from './adapters/layouts/scope.js';
4
4
  import { DEFAULT_CONFIG_V3 } from './core/config.js';
5
5
  export { PACKAGE_NAME } from './branding.js';
6
6
  export const DEFAULT_CONFIG = DEFAULT_CONFIG_V3;
7
- function runnerCommand(platform, hooksDir, repoRoot, hookScript, ...args) {
8
- return buildRunnerInvocation(platform, hooksDir, repoRoot, hookScript, ...args);
7
+ function runnerCommand(platform, hooksDir, _repoRoot, hookScript, runnerPathMode, ...args) {
8
+ if (runnerPathMode === 'absolute') {
9
+ return buildAbsoluteRunnerInvocation(platform, hooksDir, hookScript, ...args);
10
+ }
11
+ if (runnerPathMode === 'legacy-absolute') {
12
+ return buildLegacyAbsoluteRunnerInvocation(platform, hooksDir, hookScript, ...args);
13
+ }
14
+ if (runnerPathMode === 'legacy-quoted-absolute') {
15
+ return buildLegacyQuotedAbsoluteRunnerInvocation(platform, hooksDir, hookScript, ...args);
16
+ }
17
+ if (runnerPathMode === 'legacy-bare-powershell-absolute') {
18
+ return buildLegacyBarePowerShellRunnerInvocation(platform, hooksDir, hookScript, ...args);
19
+ }
20
+ return buildRunnerInvocation(platform, hooksDir, _repoRoot, hookScript, ...args);
9
21
  }
10
- export function getManagedHookEntries(platform = process.platform, hooksDir, repoRoot) {
22
+ export function getManagedHookEntries(platform = process.platform, hooksDir, repoRoot, runnerPathMode = 'absolute') {
11
23
  const resolvedRepo = path.resolve(repoRoot ?? process.cwd());
12
24
  const resolvedHooksDir = hooksDir ?? cursorLayout.hooksDir(resolvedRepo);
13
- const toolGate = runnerCommand(platform, resolvedHooksDir, resolvedRepo, 'belay-tool-gate', 'preToolUse');
14
- const subagentGate = runnerCommand(platform, resolvedHooksDir, resolvedRepo, 'belay-tool-gate', 'subagentStart');
15
- return [
25
+ const toolGate = runnerCommand(platform, resolvedHooksDir, resolvedRepo, 'belay-tool-gate', runnerPathMode, 'preToolUse');
26
+ const subagentGate = runnerCommand(platform, resolvedHooksDir, resolvedRepo, 'belay-tool-gate', runnerPathMode, 'subagentStart');
27
+ const entries = [
16
28
  {
17
29
  event: 'beforeSubmitPrompt',
18
30
  definition: {
19
- command: runnerCommand(platform, resolvedHooksDir, resolvedRepo, 'belay-before-submit'),
31
+ command: runnerCommand(platform, resolvedHooksDir, resolvedRepo, 'belay-before-submit', runnerPathMode),
20
32
  placement: 'prepend',
21
33
  },
22
34
  },
23
35
  {
24
36
  event: 'beforeShellExecution',
25
37
  definition: {
26
- command: runnerCommand(platform, resolvedHooksDir, resolvedRepo, 'belay-shell-gate'),
27
- placement: 'prepend',
28
- },
29
- },
30
- {
31
- event: 'preToolUse',
32
- definition: {
33
- command: toolGate,
34
- placement: 'prepend',
35
- matcher: 'Task',
36
- },
37
- },
38
- {
39
- event: 'preToolUse',
40
- definition: {
41
- command: toolGate,
42
- placement: 'prepend',
43
- matcher: 'Write',
44
- },
45
- },
46
- {
47
- event: 'preToolUse',
48
- definition: {
49
- command: toolGate,
50
- placement: 'prepend',
51
- matcher: 'StrReplace',
52
- },
53
- },
54
- {
55
- event: 'preToolUse',
56
- definition: {
57
- command: toolGate,
38
+ command: runnerCommand(platform, resolvedHooksDir, resolvedRepo, 'belay-shell-gate', runnerPathMode),
58
39
  placement: 'prepend',
59
- matcher: 'Delete',
60
40
  },
61
41
  },
62
42
  {
@@ -64,8 +44,6 @@ export function getManagedHookEntries(platform = process.platform, hooksDir, rep
64
44
  definition: {
65
45
  command: toolGate,
66
46
  placement: 'prepend',
67
- // Agent Shell tool (preToolUse) — distinct from terminal beforeShellExecution.
68
- matcher: 'Shell',
69
47
  },
70
48
  },
71
49
  {
@@ -119,32 +97,36 @@ export function getManagedHookEntries(platform = process.platform, hooksDir, rep
119
97
  {
120
98
  event: 'postToolUse',
121
99
  definition: {
122
- command: runnerCommand(platform, resolvedHooksDir, resolvedRepo, 'belay-audit', 'postToolUse'),
100
+ command: runnerCommand(platform, resolvedHooksDir, resolvedRepo, 'belay-audit', runnerPathMode, 'postToolUse'),
123
101
  placement: 'append',
124
102
  },
125
103
  },
126
104
  {
127
105
  event: 'postToolUseFailure',
128
106
  definition: {
129
- command: runnerCommand(platform, resolvedHooksDir, resolvedRepo, 'belay-audit', 'postToolUseFailure'),
107
+ command: runnerCommand(platform, resolvedHooksDir, resolvedRepo, 'belay-audit', runnerPathMode, 'postToolUseFailure'),
130
108
  placement: 'append',
131
109
  },
132
110
  },
133
111
  {
134
112
  event: 'stop',
135
113
  definition: {
136
- command: runnerCommand(platform, resolvedHooksDir, resolvedRepo, 'belay-audit', 'stop'),
114
+ command: runnerCommand(platform, resolvedHooksDir, resolvedRepo, 'belay-audit', runnerPathMode, 'stop'),
137
115
  placement: 'append',
138
116
  },
139
117
  },
140
118
  {
141
119
  event: 'sessionEnd',
142
120
  definition: {
143
- command: runnerCommand(platform, resolvedHooksDir, resolvedRepo, 'belay-audit', 'sessionEnd'),
121
+ command: runnerCommand(platform, resolvedHooksDir, resolvedRepo, 'belay-audit', runnerPathMode, 'sessionEnd'),
144
122
  placement: 'append',
145
123
  },
146
124
  },
147
125
  ];
126
+ return entries.map(({ event, definition }) => ({
127
+ event,
128
+ definition: { ...definition, failClosed: true },
129
+ }));
148
130
  }
149
131
  /** @deprecated Use getManagedHookEntries instead. */
150
132
  export function getManagedHookEvents(platform = process.platform) {
@@ -1,5 +1,6 @@
1
1
  import type { ScopedPaths } from '../adapters/layouts/scope.js';
2
2
  import type { AdapterName } from '../adapters/layouts/types.js';
3
3
  import type { BelayConfigV3 } from '../core/config.js';
4
+ export declare const CURSOR_COMMAND_ARTIFACTS: readonly ["belay-approve.md", "belay-why.md", "belay-explain.md", "belay-status.md", "belay-report.md", "belay-recover.md"];
4
5
  export declare function bootstrapStateFiles(repoRoot: string, config: BelayConfigV3, paths: ScopedPaths): Promise<void>;
5
6
  export declare function writeSkillArtifacts(adapterName: AdapterName, paths: ScopedPaths): Promise<void>;
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import { approvedApprovalsPath, pendingApprovalsPath } from '../config-io.js';
5
5
  import { EMPTY_APPROVALS } from '../defaults.js';
6
6
  const BUNDLED_SKILL_TEMPLATE_URL = new URL('../../skills/belay/SKILL.md', import.meta.url);
7
- const BUNDLED_COMMAND_TEMPLATES = [
7
+ export const CURSOR_COMMAND_ARTIFACTS = [
8
8
  'belay-approve.md',
9
9
  'belay-why.md',
10
10
  'belay-explain.md',
@@ -53,7 +53,7 @@ export async function writeSkillArtifacts(adapterName, paths) {
53
53
  await writeFile(path.join(paths.skillsDir, 'SKILL.md'), bundledSkill, 'utf8');
54
54
  if (adapterName === 'cursor' && paths.commandsDir) {
55
55
  await mkdir(paths.commandsDir, { recursive: true });
56
- for (const fileName of BUNDLED_COMMAND_TEMPLATES) {
56
+ for (const fileName of CURSOR_COMMAND_ARTIFACTS) {
57
57
  const bundledCommand = await readBundledTemplate(new URL(`../../skills/belay/${fileName}`, import.meta.url));
58
58
  await writeFile(path.join(paths.commandsDir, fileName), bundledCommand, 'utf8');
59
59
  }
@@ -1,23 +1,53 @@
1
- import { chmod, mkdir, writeFile } from 'node:fs/promises';
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chmod, mkdir, realpath, rename, rm, writeFile } from 'node:fs/promises';
2
3
  import path from 'node:path';
3
- import { buildRunnerScript, buildWindowsRunnerScript } from '../node-resolution.js';
4
- import { renderAuditHook, renderBeforeSubmitHook, renderRuntimeCore, renderShellGateHook, renderToolGateHook, } from '../templates.js';
4
+ import { buildRunnerScript, buildWindowsPowerShellRunnerScript, buildWindowsRunnerScript, } from '../node-resolution.js';
5
+ import { renderAuditHook, renderBeforeSubmitHook, renderCursorDispatcher, renderRuntimeCore, renderShellGateHook, renderToolGateHook, } from '../templates.js';
5
6
  async function writeFileMaybeExecutable(filePath, content, executable = false) {
6
7
  await mkdir(path.dirname(filePath), { recursive: true });
7
- await writeFile(filePath, content, 'utf8');
8
- if (executable) {
9
- await chmod(filePath, 0o755);
8
+ const temporaryPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
9
+ try {
10
+ await writeFile(temporaryPath, content, { encoding: 'utf8', flag: 'wx' });
11
+ if (executable) {
12
+ await chmod(temporaryPath, 0o755);
13
+ }
14
+ await rename(temporaryPath, filePath);
15
+ }
16
+ finally {
17
+ await rm(temporaryPath, { force: true });
10
18
  }
11
19
  }
12
20
  export async function writeRuntimeArtifacts(adapterName, paths) {
13
21
  const { hooksDir, runtimeDir } = paths;
22
+ const cursorOrigin = adapterName === 'cursor'
23
+ ? paths.scope === 'global'
24
+ ? { scope: 'global' }
25
+ : { scope: 'project', repoRoot: await realpath(paths.repoRoot) }
26
+ : undefined;
27
+ const artifacts = {
28
+ auditHook: renderAuditHook(adapterName, cursorOrigin),
29
+ beforeSubmitHook: renderBeforeSubmitHook(adapterName, cursorOrigin),
30
+ core: await renderRuntimeCore(adapterName),
31
+ dispatcher: adapterName === 'cursor' ? await renderCursorDispatcher() : undefined,
32
+ runner: buildRunnerScript(process.execPath),
33
+ runnerCmd: buildWindowsRunnerScript(process.execPath),
34
+ runnerPowerShell: adapterName === 'cursor' ? buildWindowsPowerShellRunnerScript(process.execPath) : undefined,
35
+ shellGateHook: renderShellGateHook(adapterName, cursorOrigin),
36
+ toolGateHook: renderToolGateHook(adapterName, cursorOrigin),
37
+ };
14
38
  await mkdir(hooksDir, { recursive: true });
15
39
  await mkdir(runtimeDir, { recursive: true });
16
- await writeFileMaybeExecutable(path.join(hooksDir, 'belay-before-submit.mjs'), renderBeforeSubmitHook());
17
- await writeFileMaybeExecutable(path.join(hooksDir, 'belay-shell-gate.mjs'), renderShellGateHook());
18
- await writeFileMaybeExecutable(path.join(hooksDir, 'belay-tool-gate.mjs'), renderToolGateHook());
19
- await writeFileMaybeExecutable(path.join(hooksDir, 'belay-audit.mjs'), renderAuditHook());
20
- await writeFileMaybeExecutable(path.join(runtimeDir, 'core.mjs'), await renderRuntimeCore(adapterName));
21
- await writeFileMaybeExecutable(path.join(hooksDir, 'belay-runner'), buildRunnerScript(process.execPath), true);
22
- await writeFileMaybeExecutable(path.join(hooksDir, 'belay-runner.cmd'), buildWindowsRunnerScript(process.execPath));
40
+ await writeFileMaybeExecutable(path.join(runtimeDir, 'core.mjs'), artifacts.core);
41
+ if (artifacts.dispatcher !== undefined) {
42
+ await writeFileMaybeExecutable(path.join(runtimeDir, 'dispatcher.mjs'), artifacts.dispatcher);
43
+ }
44
+ await writeFileMaybeExecutable(path.join(hooksDir, 'belay-runner'), artifacts.runner, true);
45
+ await writeFileMaybeExecutable(path.join(hooksDir, 'belay-runner.cmd'), artifacts.runnerCmd);
46
+ if (artifacts.runnerPowerShell !== undefined) {
47
+ await writeFileMaybeExecutable(path.join(hooksDir, 'belay-runner.ps1'), artifacts.runnerPowerShell);
48
+ }
49
+ await writeFileMaybeExecutable(path.join(hooksDir, 'belay-before-submit.mjs'), artifacts.beforeSubmitHook);
50
+ await writeFileMaybeExecutable(path.join(hooksDir, 'belay-shell-gate.mjs'), artifacts.shellGateHook);
51
+ await writeFileMaybeExecutable(path.join(hooksDir, 'belay-tool-gate.mjs'), artifacts.toolGateHook);
52
+ await writeFileMaybeExecutable(path.join(hooksDir, 'belay-audit.mjs'), artifacts.auditHook);
23
53
  }
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { getAdapterLayout } from '../adapters/layouts/index.js';
3
3
  import { resolveInstallScope, resolveScopedPaths, } from '../adapters/layouts/scope.js';
4
- import { loadConfigFile, writeConfigFile } from '../config-io.js';
4
+ import { loadConfigFile, writeTrustedConfigFile } from '../config-io.js';
5
5
  export async function resolveOperationScope(repoRoot, adapter, options = {}) {
6
6
  const layout = getAdapterLayout(adapter);
7
7
  let persisted;
@@ -17,7 +17,7 @@ export async function applyInstallScope(repoRoot, adapter, scope, config) {
17
17
  return current;
18
18
  }
19
19
  const updated = { ...current, installScope: scope };
20
- await writeConfigFile(repoRoot, updated, adapter);
20
+ await writeTrustedConfigFile(repoRoot, updated, adapter);
21
21
  return updated;
22
22
  }
23
23
  export function pathsForOperation(adapter, scope, repoRoot) {
package/dist/installer.js CHANGED
@@ -1,19 +1,19 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
- import { mergeCursorHooksFile, stripCursorHooksFile } from './adapters/cursor/hooks.js';
4
+ import { hasManagedCursorHookEntries, mergeCursorHooksFile, stripCursorHooksFile, } from './adapters/cursor/hooks.js';
5
5
  import { cursorLayout } from './adapters/layouts/cursor.js';
6
6
  import { resolveScopedPaths } from './adapters/layouts/scope.js';
7
7
  import { getAdapter } from './adapters/registry.js';
8
8
  import { dogfoodProject } from './commands/dogfood.js';
9
- import { detectAdapterName, loadConfigFile, mergeAndWriteConfig, writeConfigFile, } from './config-io.js';
9
+ import { detectAdapterName, loadConfigFile, mergeAndWriteConfig, writeTrustedConfigFile, } from './config-io.js';
10
10
  import { appendCliAuditEvent } from './core/audit-io.js';
11
11
  import { archiveLegacyAuditLogIfNeeded } from './core/audit-legacy-archive.js';
12
12
  import { isFreshConfigInput, mergeConfig, normalizeConfig, } from './core/config.js';
13
13
  import { runtimeIntegrityFiles, writeIntegrityManifest } from './core/integrity.js';
14
14
  import { hasValidCloudConsent, isCloudJudgeConfig, migrateImplicitLocalJudgeIfNeeded, resolveInitJudgeConfig, } from './core/judge-config.js';
15
15
  import { resolveJudgeTransport } from './core/judge-runtime-detection.js';
16
- import { bootstrapStateFiles, writeSkillArtifacts } from './installer/bootstrap.js';
16
+ import { bootstrapStateFiles, CURSOR_COMMAND_ARTIFACTS, writeSkillArtifacts, } from './installer/bootstrap.js';
17
17
  import { writeRuntimeArtifacts } from './installer/runtime-artifacts.js';
18
18
  import { applyInstallScope, resolveOperationScope } from './installer/scope-config.js';
19
19
  import { applyConfigPreset } from './presets.js';
@@ -64,7 +64,6 @@ export async function initCursorProject(options = {}) {
64
64
  const mergedHooks = mergeCursorHooksFile(hooksFile, process.platform, paths.hooksDir, repoRoot);
65
65
  await ensureDir(paths.hooksDir);
66
66
  const config = await mergeAndWriteConfig(repoRoot, 'cursor');
67
- await applyInstallScope(repoRoot, 'cursor', scope, config);
68
67
  await writeRuntimeArtifacts('cursor', paths);
69
68
  await bootstrapStateFiles(repoRoot, config, paths);
70
69
  if (withSkill) {
@@ -72,8 +71,12 @@ export async function initCursorProject(options = {}) {
72
71
  }
73
72
  await mkdir(path.dirname(paths.hooksSettingsPath), { recursive: true });
74
73
  await writeFile(paths.hooksSettingsPath, `${JSON.stringify(mergedHooks, null, 2)}\n`, 'utf8');
74
+ const installedConfig = await applyInstallScope(repoRoot, 'cursor', scope, config);
75
+ if (scope === 'global') {
76
+ await cleanupStaleProjectCursorInstall(repoRoot);
77
+ }
75
78
  await writeIntegrityManifest(repoRoot, cursorLayout, runtimeIntegrityFiles(cursorLayout, paths));
76
- await archiveLegacyAuditLogIfNeeded(repoRoot, config);
79
+ await archiveLegacyAuditLogIfNeeded(repoRoot, installedConfig);
77
80
  return { repoRoot, withSkill };
78
81
  }
79
82
  export async function upgradeCursorProject(options = {}) {
@@ -81,17 +84,31 @@ export async function upgradeCursorProject(options = {}) {
81
84
  const scope = await resolveOperationScope(repoRoot, 'cursor', options);
82
85
  const paths = resolveScopedPaths(cursorLayout, scope, repoRoot);
83
86
  const config = await mergeAndWriteConfig(repoRoot, 'cursor');
84
- await applyInstallScope(repoRoot, 'cursor', scope, config);
85
87
  await writeRuntimeArtifacts('cursor', paths);
86
88
  const hooksFile = await loadHooksFile(paths.hooksSettingsPath);
87
89
  const merged = mergeCursorHooksFile(hooksFile, process.platform, paths.hooksDir, repoRoot);
88
90
  await mkdir(path.dirname(paths.hooksSettingsPath), { recursive: true });
89
91
  await writeFile(paths.hooksSettingsPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8');
92
+ if (scope === 'project') {
93
+ const globalPaths = resolveScopedPaths(cursorLayout, 'global', repoRoot);
94
+ if (existsSync(globalPaths.hooksSettingsPath)) {
95
+ const globalHooks = await loadHooksFile(globalPaths.hooksSettingsPath);
96
+ if (hasManagedCursorHookEntries(globalHooks, process.platform, globalPaths.hooksDir, repoRoot)) {
97
+ await writeRuntimeArtifacts('cursor', globalPaths);
98
+ const mergedGlobal = mergeCursorHooksFile(globalHooks, process.platform, globalPaths.hooksDir, repoRoot);
99
+ await writeFile(globalPaths.hooksSettingsPath, `${JSON.stringify(mergedGlobal, null, 2)}\n`, 'utf8');
100
+ }
101
+ }
102
+ }
90
103
  if (options.withSkill) {
91
104
  await writeSkillArtifacts('cursor', paths);
92
105
  }
106
+ const installedConfig = await applyInstallScope(repoRoot, 'cursor', scope, config);
107
+ if (scope === 'global') {
108
+ await cleanupStaleProjectCursorInstall(repoRoot);
109
+ }
93
110
  await writeIntegrityManifest(repoRoot, cursorLayout, runtimeIntegrityFiles(cursorLayout, paths));
94
- await archiveLegacyAuditLogIfNeeded(repoRoot, config);
111
+ await archiveLegacyAuditLogIfNeeded(repoRoot, installedConfig);
95
112
  return { repoRoot };
96
113
  }
97
114
  const BELAY_HOOK_ARTIFACTS = [
@@ -101,16 +118,34 @@ const BELAY_HOOK_ARTIFACTS = [
101
118
  'belay-audit.mjs',
102
119
  'belay-runner',
103
120
  'belay-runner.cmd',
121
+ 'belay-runner.ps1',
104
122
  ];
105
123
  async function removeBelayHookArtifacts(paths) {
106
124
  for (const fileName of BELAY_HOOK_ARTIFACTS) {
107
125
  await rm(path.join(paths.hooksDir, fileName), { force: true });
108
126
  }
109
- await rm(paths.runtimeDir, { recursive: true, force: true });
110
- await rm(paths.skillsDir, { recursive: true, force: true });
127
+ for (const fileName of ['core.mjs', 'dispatcher.mjs']) {
128
+ await rm(path.join(paths.runtimeDir, fileName), { force: true });
129
+ }
130
+ await rm(path.join(paths.skillsDir, 'SKILL.md'), { force: true });
111
131
  if (paths.commandsDir) {
112
- await rm(path.join(paths.commandsDir, 'belay.md'), { force: true });
132
+ for (const fileName of CURSOR_COMMAND_ARTIFACTS) {
133
+ await rm(path.join(paths.commandsDir, fileName), { force: true });
134
+ }
135
+ }
136
+ }
137
+ async function cleanupStaleProjectCursorInstall(repoRoot) {
138
+ const projectPaths = resolveScopedPaths(cursorLayout, 'project', repoRoot);
139
+ if (!existsSync(projectPaths.hooksSettingsPath)) {
140
+ return;
141
+ }
142
+ const projectHooks = await loadHooksFile(projectPaths.hooksSettingsPath);
143
+ if (!hasManagedCursorHookEntries(projectHooks, process.platform, projectPaths.hooksDir, repoRoot)) {
144
+ return;
113
145
  }
146
+ const stripped = stripCursorHooksFile(projectHooks, process.platform, projectPaths.hooksDir, repoRoot);
147
+ await writeFile(projectPaths.hooksSettingsPath, `${JSON.stringify(stripped, null, 2)}\n`, 'utf8');
148
+ await removeBelayHookArtifacts(projectPaths);
114
149
  }
115
150
  export async function uninstallCursorProject(options = {}) {
116
151
  const repoRoot = path.resolve(options.targetDir ?? process.cwd());
@@ -195,7 +230,7 @@ async function applyInitJudgeConfig(repoRoot, adapterName, options, isFreshBefor
195
230
  !hasValidCloudConsent(configWithJudge.judge)) {
196
231
  process.stderr.write('Warning: Cloud judge saved without recorded consent. Tier1 cloud judge will fail closed until consent is granted (belay judge consent + belay approve, or TTY --accept-cloud-judge).\n');
197
232
  }
198
- await writeConfigFile(repoRoot, configWithJudge, adapterName);
233
+ await writeTrustedConfigFile(repoRoot, configWithJudge, adapterName);
199
234
  if (migrated) {
200
235
  await auditJudgeMigrationIfNeeded(repoRoot, adapterName, mergedConfig.judge, finalJudge, options.migrateJudgeDefault ? 'belay init --migrate-judge-default' : 'belay init');
201
236
  }
@@ -228,7 +263,7 @@ export async function initProject(options = {}) {
228
263
  const existing = await loadConfigFile(repoRoot, adapterName);
229
264
  const presetConfig = mergeConfig(applyConfigPreset(options.preset));
230
265
  const merged = mergeConfig(presetConfig, existing);
231
- await writeConfigFile(repoRoot, merged, adapterName);
266
+ await writeTrustedConfigFile(repoRoot, merged, adapterName);
232
267
  }
233
268
  if (options.dogfood === true) {
234
269
  await dogfoodProject({ targetDir: repoRoot, adapter: adapterName });
@@ -250,7 +285,7 @@ export async function upgradeProject(options = {}) {
250
285
  const migrated = migrateImplicitLocalJudgeIfNeeded(mergedConfig.judge, adapterName);
251
286
  if (migrated) {
252
287
  const configWithJudge = normalizeConfig({ ...mergedConfig, version: 4, judge: migrated });
253
- await writeConfigFile(repoRoot, configWithJudge, adapterName);
288
+ await writeTrustedConfigFile(repoRoot, configWithJudge, adapterName);
254
289
  await auditJudgeMigrationIfNeeded(repoRoot, adapterName, mergedConfig.judge, migrated, 'belay upgrade --migrate-judge-default');
255
290
  }
256
291
  }
@@ -6,3 +6,4 @@ export interface NodeResolutionResult {
6
6
  export declare function resolveNodeBinary(): NodeResolutionResult;
7
7
  export declare function buildRunnerScript(defaultNodePath: string): string;
8
8
  export declare function buildWindowsRunnerScript(defaultNodePath: string): string;
9
+ export declare function buildWindowsPowerShellRunnerScript(defaultNodePath: string): string;
@@ -235,3 +235,64 @@ echo {}
235
235
  exit /b 0
236
236
  `;
237
237
  }
238
+ export function buildWindowsPowerShellRunnerScript(defaultNodePath) {
239
+ const encodedDefault = Buffer.from(defaultNodePath, 'utf8').toString('base64');
240
+ return `$ErrorActionPreference = 'Stop'
241
+
242
+ $DefaultNode = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedDefault}'))
243
+ $HookName = if ($args.Count -gt 0) { [string] $args[0] } else { '' }
244
+ $HookArguments = if ($args.Count -gt 1) { @($args[1..($args.Count - 1)]) } else { @() }
245
+
246
+ function Resolve-Node {
247
+ if ($DefaultNode -and (Test-Path -LiteralPath $DefaultNode -PathType Leaf)) {
248
+ return $DefaultNode
249
+ }
250
+
251
+ $PathNode = Get-Command node.exe -CommandType Application -ErrorAction SilentlyContinue
252
+ if ($null -ne $PathNode) {
253
+ return $PathNode.Source
254
+ }
255
+
256
+ if ($env:USERPROFILE) {
257
+ $NvmRoot = Join-Path $env:USERPROFILE '.nvm'
258
+ if (Test-Path -LiteralPath $NvmRoot -PathType Container) {
259
+ foreach ($VersionDirectory in Get-ChildItem -LiteralPath $NvmRoot -Directory | Sort-Object Name -Descending) {
260
+ $Candidate = Join-Path $VersionDirectory.FullName 'node.exe'
261
+ if (Test-Path -LiteralPath $Candidate -PathType Leaf) {
262
+ return $Candidate
263
+ }
264
+ }
265
+ }
266
+ }
267
+
268
+ return $null
269
+ }
270
+
271
+ function Fail-Audit {
272
+ [Console]::Error.WriteLine('belay: unable to resolve Node for audit hook')
273
+ [Console]::Out.WriteLine('{}')
274
+ exit 0
275
+ }
276
+
277
+ if (-not $HookName) {
278
+ Fail-Audit
279
+ }
280
+
281
+ $NodeBin = Resolve-Node
282
+ if (-not $NodeBin) {
283
+ if ($HookName -eq 'belay-before-submit') {
284
+ [Console]::Out.WriteLine('{"continue":false,"user_message":"belay could not resolve Node. Install or expose Node, run belay doctor, then retry."}')
285
+ exit 0
286
+ }
287
+ if ($HookName -in @('belay-shell-gate', 'belay-tool-gate')) {
288
+ [Console]::Out.WriteLine('{"permission":"deny","user_message":"belay could not resolve Node. Install or expose Node, run belay doctor, then retry."}')
289
+ exit 0
290
+ }
291
+ Fail-Audit
292
+ }
293
+
294
+ $ScriptPath = Join-Path $PSScriptRoot "$HookName.mjs"
295
+ & $NodeBin $ScriptPath @HookArguments
296
+ exit $LASTEXITCODE
297
+ `;
298
+ }
@@ -1,7 +1,10 @@
1
+ import type { CursorHookOrigin } from './adapters/cursor/hook-router.js';
2
+ import type { AdapterName } from './adapters/layouts/types.js';
1
3
  import type { BelayConfigV3 } from './core/config.js';
2
4
  export declare function renderConfig(config: BelayConfigV3): string;
3
- export declare function renderBeforeSubmitHook(): string;
4
- export declare function renderShellGateHook(): string;
5
- export declare function renderToolGateHook(): string;
6
- export declare function renderAuditHook(): string;
5
+ export declare function renderBeforeSubmitHook(adapter: AdapterName, cursorOrigin?: CursorHookOrigin): string;
6
+ export declare function renderShellGateHook(adapter: AdapterName, cursorOrigin?: CursorHookOrigin): string;
7
+ export declare function renderToolGateHook(adapter: AdapterName, cursorOrigin?: CursorHookOrigin): string;
8
+ export declare function renderAuditHook(adapter: AdapterName, cursorOrigin?: CursorHookOrigin): string;
9
+ export declare function renderCursorDispatcher(): Promise<string>;
7
10
  export declare function renderRuntimeCore(adapter?: 'cursor' | 'claude' | 'codex'): Promise<string>;
package/dist/templates.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { CURSOR_DISPATCHER_GENERATION_HEADER } from './adapters/cursor/dispatcher-generation.js';
4
5
  import { hashValue } from './core/fingerprint.js';
5
6
  import { PACKAGE_VERSION } from './version.js';
6
7
  function inlineJson(value) {
@@ -9,26 +10,54 @@ function inlineJson(value) {
9
10
  export function renderConfig(config) {
10
11
  return `${inlineJson(config)}\n`;
11
12
  }
12
- export function renderBeforeSubmitHook() {
13
+ function requireCursorOrigin(origin) {
14
+ if (!origin) {
15
+ throw new Error('Cursor hook origin is required when rendering dispatcher shims.');
16
+ }
17
+ return origin;
18
+ }
19
+ function renderCursorDispatchHook(origin, kind, eventName) {
20
+ return `import { dispatchCursorHook } from '../belay/runtime/dispatcher.mjs'
21
+
22
+ await dispatchCursorHook({
23
+ origin: ${JSON.stringify(origin)},
24
+ kind: ${JSON.stringify(kind)},
25
+ eventName: ${eventName},
26
+ })
27
+ `;
28
+ }
29
+ export function renderBeforeSubmitHook(adapter, cursorOrigin) {
30
+ if (adapter === 'cursor') {
31
+ return renderCursorDispatchHook(requireCursorOrigin(cursorOrigin), 'before-submit', JSON.stringify('beforeSubmitPrompt'));
32
+ }
13
33
  return `import { runBeforeSubmitPromptHook } from '../belay/runtime/core.mjs'
14
34
 
15
35
  await runBeforeSubmitPromptHook()
16
36
  `;
17
37
  }
18
- export function renderShellGateHook() {
38
+ export function renderShellGateHook(adapter, cursorOrigin) {
39
+ if (adapter === 'cursor') {
40
+ return renderCursorDispatchHook(requireCursorOrigin(cursorOrigin), 'shell-gate', JSON.stringify('beforeShellExecution'));
41
+ }
19
42
  return `import { runShellGateHook } from '../belay/runtime/core.mjs'
20
43
 
21
44
  await runShellGateHook()
22
45
  `;
23
46
  }
24
- export function renderToolGateHook() {
47
+ export function renderToolGateHook(adapter, cursorOrigin) {
48
+ if (adapter === 'cursor') {
49
+ return renderCursorDispatchHook(requireCursorOrigin(cursorOrigin), 'tool-gate', "process.argv[2] ?? 'preToolUse'");
50
+ }
25
51
  return `import { runToolGateHook } from '../belay/runtime/core.mjs'
26
52
 
27
53
  const eventName = process.argv[2] ?? 'preToolUse'
28
54
  await runToolGateHook(eventName)
29
55
  `;
30
56
  }
31
- export function renderAuditHook() {
57
+ export function renderAuditHook(adapter, cursorOrigin) {
58
+ if (adapter === 'cursor') {
59
+ return renderCursorDispatchHook(requireCursorOrigin(cursorOrigin), 'audit', "process.argv[2] ?? 'postToolUse'");
60
+ }
32
61
  return `import { runAuditHook } from '../belay/runtime/core.mjs'
33
62
 
34
63
  const eventName = process.argv[2] ?? 'postToolUse'
@@ -44,6 +73,15 @@ async function readRuntimeBundle(adapter = 'cursor') {
44
73
  throw new Error('Runtime bundle missing. Run pnpm build before belay init or upgrade.');
45
74
  }
46
75
  }
76
+ export async function renderCursorDispatcher() {
77
+ const bundlePath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'dist', 'bundle', 'cursor-dispatcher.mjs');
78
+ try {
79
+ return `${CURSOR_DISPATCHER_GENERATION_HEADER}${await readFile(bundlePath, 'utf8')}`;
80
+ }
81
+ catch {
82
+ throw new Error('Runtime bundle missing. Run pnpm build before belay init or upgrade.');
83
+ }
84
+ }
47
85
  export async function renderRuntimeCore(adapter = 'cursor') {
48
86
  const bundle = await readRuntimeBundle(adapter);
49
87
  const withoutStamp = bundle
package/dist/types.d.ts CHANGED
@@ -7,6 +7,7 @@ import type { ApprovalRecord, ClassifyResult } from './core/types.js';
7
7
  export interface HookEntry {
8
8
  command: string;
9
9
  matcher?: string;
10
+ failClosed?: boolean;
10
11
  }
11
12
  export interface HooksFile {
12
13
  version: number;
@@ -237,6 +238,8 @@ export interface DogfoodOptions {
237
238
  targetDir?: string;
238
239
  enforce?: boolean;
239
240
  force?: boolean;
241
+ check?: boolean;
242
+ since?: string;
240
243
  adapter?: AdapterName;
241
244
  }
242
245
  export interface DogfoodResult {
@@ -247,6 +250,23 @@ export interface DogfoodResult {
247
250
  mode: string;
248
251
  unknownLocalEffect: string;
249
252
  }
253
+ export interface DogfoodCheckOptions {
254
+ targetDir?: string;
255
+ adapter?: AdapterName;
256
+ since: string;
257
+ }
258
+ export interface DogfoodCheckResult {
259
+ ok: boolean;
260
+ repoRoot: string;
261
+ since: string;
262
+ gateEvents: number;
263
+ auditModeDenyCount: number;
264
+ hostDeniedAfterAllowCount: number;
265
+ shellPreToolUseCount: number;
266
+ mismatchedCohortCount: number;
267
+ environmentSkewCount: number;
268
+ failures: string[];
269
+ }
250
270
  export type ExplainKind = 'shell' | 'tool' | 'subagent';
251
271
  export interface ExplainReport {
252
272
  repoRoot: string;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const PACKAGE_VERSION = "0.9.3";
1
+ export declare const PACKAGE_VERSION = "0.10.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/sync-version.mjs — do not edit.
2
- export const PACKAGE_VERSION = '0.9.3';
2
+ export const PACKAGE_VERSION = '0.10.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guilz-dev/belay",
3
- "version": "0.9.3",
3
+ "version": "0.10.0",
4
4
  "description": "Belay-style approval and audit gating for agent runtimes.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -56,10 +56,14 @@
56
56
  "lint": "biome check src package.json README.md CHANGELOG.md CONTRIBUTING.md SECURITY.md tsconfig.json tsconfig.build.json vitest.config.ts scripts docs",
57
57
  "typecheck": "tsc --noEmit",
58
58
  "test": "pnpm build && vitest run",
59
+ "test:run": "vitest run",
59
60
  "test:structural": "pnpm build && vitest run src/__tests__/verdict/structural-suite.test.ts",
61
+ "test:structural:run": "vitest run src/__tests__/verdict/structural-suite.test.ts",
60
62
  "test:docker": "pnpm build && vitest run --config vitest.live.config.ts src/__tests__/capability/boundary-container-isolation.test.ts src/__tests__/capability/boundary-container-workspace-mount.test.ts src/__tests__/capability/boundary-driver-container.test.ts src/__tests__/contained-execution-docker.integration.test.ts",
61
63
  "test:llm": "pnpm build && vitest run --config vitest.live.config.ts src/__tests__/verdict/llm/judge-accuracy.test.ts",
62
64
  "test:stable": "pnpm build && vitest run && vitest run && vitest run",
65
+ "test:stable:run": "vitest run && vitest run && vitest run",
66
+ "test:macos": "vitest run src/__tests__/native-seatbelt-boundary-probe.test.ts src/__tests__/git-resource-identity.test.ts src/__tests__/installer-scope.test.ts src/__tests__/cursor-hook-precedence.integration.test.ts src/__tests__/cursor-hooks.test.ts",
63
67
  "corpus": "pnpm build && node scripts/corpus.mjs",
64
68
  "probe:adversarial": "pnpm build && node scripts/adversarial-probe.mjs",
65
69
  "probe:cursor-shell-rewrite": "node scripts/cursor-shell-rewrite-probe.mjs",