@guilz-dev/belay 0.2.0 → 0.4.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 (180) hide show
  1. package/README.md +39 -12
  2. package/dist/adapters/cursor/runtime-entry.js +1 -0
  3. package/dist/adapters/shared/gate-runtime.d.ts +11 -14
  4. package/dist/adapters/shared/gate-runtime.js +289 -23
  5. package/dist/bundle/claude-runtime.mjs +4435 -708
  6. package/dist/bundle/codex-runtime.mjs +4448 -710
  7. package/dist/bundle/cursor-runtime.mjs +4437 -710
  8. package/dist/cli.js +333 -11
  9. package/dist/commands/approve.d.ts +1 -0
  10. package/dist/commands/approve.js +56 -26
  11. package/dist/commands/config.d.ts +70 -0
  12. package/dist/commands/config.js +577 -0
  13. package/dist/commands/doctor.js +13 -2
  14. package/dist/commands/explain.js +6 -1
  15. package/dist/commands/harvest.d.ts +29 -0
  16. package/dist/commands/harvest.js +72 -0
  17. package/dist/commands/health-snapshot.d.ts +6 -0
  18. package/dist/commands/health-snapshot.js +34 -0
  19. package/dist/commands/judge.d.ts +100 -0
  20. package/dist/commands/judge.js +304 -0
  21. package/dist/commands/metrics.js +34 -2
  22. package/dist/commands/quality.d.ts +38 -0
  23. package/dist/commands/quality.js +105 -0
  24. package/dist/commands/simulate.d.ts +1 -0
  25. package/dist/commands/simulate.js +7 -2
  26. package/dist/commands/standing-allow.d.ts +10 -0
  27. package/dist/commands/standing-allow.js +26 -0
  28. package/dist/commands/status.js +2 -2
  29. package/dist/commands/stdin-key.d.ts +1 -0
  30. package/dist/commands/stdin-key.js +8 -0
  31. package/dist/commands/tui.d.ts +48 -0
  32. package/dist/commands/tui.js +150 -0
  33. package/dist/config-io.js +10 -5
  34. package/dist/conformance/guarantee-table.js +12 -0
  35. package/dist/conformance/types.d.ts +2 -0
  36. package/dist/core/approval-replay-cli.d.ts +7 -0
  37. package/dist/core/approval-replay-cli.js +36 -0
  38. package/dist/core/approval-replay.d.ts +45 -0
  39. package/dist/core/approval-replay.js +141 -0
  40. package/dist/core/approval-service.d.ts +15 -0
  41. package/dist/core/approval-service.js +47 -2
  42. package/dist/core/approval.d.ts +24 -3
  43. package/dist/core/approval.js +47 -4
  44. package/dist/core/audit-analysis.d.ts +7 -1
  45. package/dist/core/audit-analysis.js +111 -0
  46. package/dist/core/audit-io.d.ts +2 -0
  47. package/dist/core/audit-io.js +14 -0
  48. package/dist/core/audit-metrics.d.ts +7 -0
  49. package/dist/core/audit-metrics.js +24 -4
  50. package/dist/core/audit-query.d.ts +1 -0
  51. package/dist/core/audit-query.js +3 -0
  52. package/dist/core/audit-replay-context.d.ts +35 -0
  53. package/dist/core/audit-replay-context.js +88 -0
  54. package/dist/core/audit-types.d.ts +24 -1
  55. package/dist/core/audit-types.js +1 -1
  56. package/dist/core/capability/index.d.ts +4 -3
  57. package/dist/core/capability/index.js +3 -2
  58. package/dist/core/capability/paths.d.ts +2 -1
  59. package/dist/core/capability/paths.js +76 -8
  60. package/dist/core/capability/reasons.d.ts +3 -0
  61. package/dist/core/capability/reasons.js +8 -0
  62. package/dist/core/capability/trusted-workspace-roots.d.ts +25 -0
  63. package/dist/core/capability/trusted-workspace-roots.js +149 -0
  64. package/dist/core/capability/types.d.ts +11 -1
  65. package/dist/core/capability-approval.d.ts +2 -1
  66. package/dist/core/capability-approval.js +100 -2
  67. package/dist/core/classify-subagent.js +2 -20
  68. package/dist/core/classify-tool.js +129 -30
  69. package/dist/core/config-layers.js +2 -1
  70. package/dist/core/config.d.ts +40 -2
  71. package/dist/core/config.js +159 -11
  72. package/dist/core/credential-store.d.ts +11 -0
  73. package/dist/core/credential-store.js +60 -0
  74. package/dist/core/gate-engine.js +104 -13
  75. package/dist/core/harvest.d.ts +53 -0
  76. package/dist/core/harvest.js +276 -0
  77. package/dist/core/index.d.ts +9 -3
  78. package/dist/core/index.js +6 -2
  79. package/dist/core/integrity.d.ts +2 -0
  80. package/dist/core/integrity.js +13 -0
  81. package/dist/core/judge-api-key.d.ts +19 -0
  82. package/dist/core/judge-api-key.js +74 -0
  83. package/dist/core/judge-cloud-consent.d.ts +13 -0
  84. package/dist/core/judge-cloud-consent.js +38 -0
  85. package/dist/core/judge-config.d.ts +41 -4
  86. package/dist/core/judge-config.js +263 -57
  87. package/dist/core/judge-doctor.d.ts +9 -1
  88. package/dist/core/judge-doctor.js +199 -94
  89. package/dist/core/judge-model-discovery.d.ts +24 -0
  90. package/dist/core/judge-model-discovery.js +168 -0
  91. package/dist/core/judge-model-policy.d.ts +5 -0
  92. package/dist/core/judge-model-policy.js +21 -0
  93. package/dist/core/judge-runtime-detection.d.ts +9 -0
  94. package/dist/core/judge-runtime-detection.js +68 -0
  95. package/dist/core/path-utils.d.ts +11 -0
  96. package/dist/core/path-utils.js +56 -6
  97. package/dist/core/reclassify.d.ts +3 -0
  98. package/dist/core/reclassify.js +44 -14
  99. package/dist/core/replay-scrub.d.ts +9 -0
  100. package/dist/core/replay-scrub.js +43 -0
  101. package/dist/core/shell-tokenizer.d.ts +2 -0
  102. package/dist/core/shell-tokenizer.js +82 -23
  103. package/dist/core/standing-allow.d.ts +50 -0
  104. package/dist/core/standing-allow.js +175 -0
  105. package/dist/core/transactional/diff-evaluator.js +1 -19
  106. package/dist/core/types.d.ts +17 -0
  107. package/dist/core/verdict/adapter.d.ts +1 -0
  108. package/dist/core/verdict/adapter.js +17 -2
  109. package/dist/core/verdict/containment.d.ts +8 -2
  110. package/dist/core/verdict/containment.js +45 -10
  111. package/dist/core/verdict/judge-audit.d.ts +1 -0
  112. package/dist/core/verdict/judge-audit.js +32 -1
  113. package/dist/core/verdict/judge-baseline.d.ts +20 -0
  114. package/dist/core/verdict/judge-baseline.js +48 -0
  115. package/dist/core/verdict/judge-broker-service.d.ts +42 -0
  116. package/dist/core/verdict/judge-broker-service.js +279 -0
  117. package/dist/core/verdict/judge-catalog.d.ts +40 -0
  118. package/dist/core/verdict/judge-catalog.js +148 -0
  119. package/dist/core/verdict/judge-cli-fingerprint.d.ts +5 -0
  120. package/dist/core/verdict/judge-cli-fingerprint.js +61 -0
  121. package/dist/core/verdict/judge-cli.d.ts +46 -0
  122. package/dist/core/verdict/judge-cli.js +401 -0
  123. package/dist/core/verdict/judge-factory.d.ts +15 -4
  124. package/dist/core/verdict/judge-factory.js +127 -15
  125. package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
  126. package/dist/core/verdict/judge-provider-matrix.js +66 -0
  127. package/dist/core/verdict/judge-runtime-config.d.ts +42 -0
  128. package/dist/core/verdict/judge-runtime-config.js +92 -0
  129. package/dist/core/verdict/judge-session-broker.d.ts +48 -0
  130. package/dist/core/verdict/judge-session-broker.js +195 -0
  131. package/dist/core/verdict/judge-session-guard.d.ts +27 -0
  132. package/dist/core/verdict/judge-session-guard.js +91 -0
  133. package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
  134. package/dist/core/verdict/judge-session-kill-switch.js +43 -0
  135. package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
  136. package/dist/core/verdict/judge-session-mutex.js +23 -0
  137. package/dist/core/verdict/judge-shadow.d.ts +24 -0
  138. package/dist/core/verdict/judge-shadow.js +116 -0
  139. package/dist/core/verdict/judge-transport.d.ts +40 -0
  140. package/dist/core/verdict/judge-transport.js +258 -0
  141. package/dist/core/verdict/judge.d.ts +33 -1
  142. package/dist/core/verdict/judge.js +84 -14
  143. package/dist/core/verdict/parser.js +6 -1
  144. package/dist/core/verdict/persistent-paths.d.ts +8 -0
  145. package/dist/core/verdict/persistent-paths.js +52 -0
  146. package/dist/core/verdict/types.d.ts +19 -2
  147. package/dist/core/verdict/verdict.js +166 -49
  148. package/dist/corpus/adversarial-probe.d.ts +86 -0
  149. package/dist/corpus/adversarial-probe.js +220 -0
  150. package/dist/corpus/evaluate.d.ts +45 -12
  151. package/dist/corpus/evaluate.js +62 -5
  152. package/dist/corpus/gates.d.ts +35 -0
  153. package/dist/corpus/gates.js +81 -0
  154. package/dist/corpus/judge-accuracy.d.ts +8 -0
  155. package/dist/corpus/judge-accuracy.js +39 -0
  156. package/dist/corpus/must-allow-commands.d.ts +5 -0
  157. package/dist/corpus/must-allow-commands.js +13 -0
  158. package/dist/corpus/mutators.d.ts +28 -0
  159. package/dist/corpus/mutators.js +151 -0
  160. package/dist/corpus/ratchet.d.ts +42 -0
  161. package/dist/corpus/ratchet.js +116 -0
  162. package/dist/corpus/runtime-match.d.ts +19 -0
  163. package/dist/corpus/runtime-match.js +45 -0
  164. package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
  165. package/dist/corpus/standing-allow-catalog.generated.js +79 -0
  166. package/dist/corpus/types.d.ts +47 -0
  167. package/dist/corpus/types.js +157 -0
  168. package/dist/installer.js +69 -15
  169. package/dist/judge-broker-daemon.d.ts +1 -0
  170. package/dist/judge-broker-daemon.js +123 -0
  171. package/dist/services/sandbox-service.d.ts +1 -0
  172. package/dist/services/sandbox-service.js +2 -0
  173. package/dist/types.d.ts +7 -0
  174. package/dist/version.d.ts +1 -1
  175. package/dist/version.js +1 -1
  176. package/package.json +7 -3
  177. package/skills/belay/SKILL.md +15 -8
  178. package/skills/belay/belay-approve.md +17 -0
  179. package/dist/commands/init-wizard.d.ts +0 -21
  180. package/dist/commands/init-wizard.js +0 -63
package/dist/installer.js CHANGED
@@ -7,9 +7,11 @@ import { resolveScopedPaths } from './adapters/layouts/scope.js';
7
7
  import { getAdapter } from './adapters/registry.js';
8
8
  import { dogfoodProject } from './commands/dogfood.js';
9
9
  import { detectAdapterName, loadConfigFile, mergeAndWriteConfig, writeConfigFile, } from './config-io.js';
10
- import { isFreshConfigInput, mergeConfig, normalizeConfig } from './core/config.js';
10
+ import { appendCliAuditEvent } from './core/audit-io.js';
11
+ import { isFreshConfigInput, mergeConfig, normalizeConfig, } from './core/config.js';
11
12
  import { runtimeIntegrityFiles, writeIntegrityManifest } from './core/integrity.js';
12
- import { resolveInitJudgeConfig } from './core/judge-config.js';
13
+ import { hasValidCloudConsent, isCloudJudgeConfig, migrateImplicitLocalJudgeIfNeeded, resolveInitJudgeConfig, } from './core/judge-config.js';
14
+ import { resolveJudgeTransport } from './core/judge-runtime-detection.js';
13
15
  import { bootstrapStateFiles, writeSkillArtifacts } from './installer/bootstrap.js';
14
16
  import { writeRuntimeArtifacts } from './installer/runtime-artifacts.js';
15
17
  import { applyInstallScope, resolveOperationScope } from './installer/scope-config.js';
@@ -104,32 +106,64 @@ function resolveAdapterName(options, repoRoot) {
104
106
  }
105
107
  return 'cursor';
106
108
  }
107
- async function applyInitJudgeConfig(repoRoot, adapterName, options) {
108
- if (options.skipJudgeWrite) {
109
+ async function auditJudgeMigrationIfNeeded(repoRoot, adapterName, before, after, by) {
110
+ if (before.providerId === after.providerId && before.provider === after.provider) {
109
111
  return;
110
112
  }
111
- const layout = getAdapter(adapterName).layout;
112
- const configPath = layout.configPath(repoRoot);
113
- let existingConfig = {};
114
- if (await pathExists(configPath)) {
115
- existingConfig = JSON.parse(await readFile(configPath, 'utf8'));
113
+ const config = await loadConfigFile(repoRoot, adapterName);
114
+ await appendCliAuditEvent(repoRoot, config, {
115
+ event: 'judge_provider_changed',
116
+ from: { providerId: before.providerId, provider: before.provider },
117
+ to: { providerId: after.providerId, provider: after.provider },
118
+ by,
119
+ });
120
+ }
121
+ async function applyInitJudgeConfig(repoRoot, adapterName, options, isFreshBeforeInstall) {
122
+ if (options.skipJudgeWrite) {
123
+ return;
116
124
  }
117
- const isFresh = isFreshConfigInput(existingConfig);
118
125
  const mergedConfig = await loadConfigFile(repoRoot, adapterName);
119
- const hasExplicitJudgeFlags = options.judgeProfile || options.judgeProvider || options.judgeModel || options.judgeEndpoint;
126
+ const hasExplicitJudgeFlags = options.judgeProfile ||
127
+ options.judgeProvider ||
128
+ options.judgeProviderId ||
129
+ options.judgeModel ||
130
+ options.judgeEndpoint;
120
131
  const judge = resolveInitJudgeConfig({
121
- isFresh,
132
+ isFresh: isFreshBeforeInstall,
122
133
  hasExplicitJudgeFlags: Boolean(hasExplicitJudgeFlags),
123
134
  judgeProfile: options.judgeProfile,
124
135
  judgeProvider: options.judgeProvider,
136
+ judgeProviderId: options.judgeProviderId,
125
137
  judgeModel: options.judgeModel,
126
138
  judgeEndpoint: options.judgeEndpoint,
127
139
  acceptCloudJudge: options.acceptCloudJudge,
140
+ interactiveConsent: Boolean(options.acceptCloudJudge && process.stdin.isTTY),
141
+ cloudConsentApprovalId: options.cloudConsentApprovalId,
128
142
  existingJudge: mergedConfig.judge,
129
- defaultJudgeProfile: adapterName,
143
+ adapter: adapterName,
130
144
  });
131
- const configWithJudge = normalizeConfig({ ...mergedConfig, version: 4, judge });
145
+ const migrated = options.migrateJudgeDefault === true
146
+ ? migrateImplicitLocalJudgeIfNeeded(mergedConfig.judge, adapterName)
147
+ : null;
148
+ let finalJudge = migrated ?? judge;
149
+ if (options.judgeCredentialMode) {
150
+ finalJudge = {
151
+ ...finalJudge,
152
+ credential: options.judgeCredentialMode === 'apiKey'
153
+ ? { mode: 'apiKey', ref: 'store:judge' }
154
+ : { mode: 'project' },
155
+ };
156
+ }
157
+ const configWithJudge = normalizeConfig({ ...mergedConfig, version: 4, judge: finalJudge });
158
+ if (isCloudJudgeConfig(configWithJudge.judge) &&
159
+ resolveJudgeTransport(configWithJudge.judge) === 'http' &&
160
+ !hasValidCloudConsent(configWithJudge.judge)) {
161
+ 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');
162
+ }
132
163
  await writeConfigFile(repoRoot, configWithJudge, adapterName);
164
+ if (migrated) {
165
+ await auditJudgeMigrationIfNeeded(repoRoot, adapterName, mergedConfig.judge, finalJudge, options.migrateJudgeDefault ? 'belay init --migrate-judge-default' : 'belay init');
166
+ }
133
167
  }
134
168
  async function refreshIntegrityManifest(repoRoot, adapterName) {
135
169
  const layout = getAdapter(adapterName).layout;
@@ -142,8 +176,19 @@ export async function initProject(options = {}) {
142
176
  const repoRoot = path.resolve(options.targetDir ?? process.cwd());
143
177
  const adapterName = resolveAdapterName(options, repoRoot);
144
178
  const adapter = getAdapter(adapterName);
179
+ const configPath = adapter.layout.configPath(repoRoot);
180
+ let isFreshBeforeInstall = true;
181
+ if (await pathExists(configPath)) {
182
+ try {
183
+ const raw = await readFile(configPath, 'utf8');
184
+ isFreshBeforeInstall = isFreshConfigInput(JSON.parse(raw));
185
+ }
186
+ catch {
187
+ isFreshBeforeInstall = false;
188
+ }
189
+ }
145
190
  const result = await adapter.install(repoRoot, options);
146
- await applyInitJudgeConfig(repoRoot, adapterName, options);
191
+ await applyInitJudgeConfig(repoRoot, adapterName, options, isFreshBeforeInstall);
147
192
  if (options.preset) {
148
193
  const existing = await loadConfigFile(repoRoot, adapterName);
149
194
  const presetConfig = mergeConfig(applyConfigPreset(options.preset));
@@ -165,6 +210,15 @@ export async function upgradeProject(options = {}) {
165
210
  const repoRoot = path.resolve(options.targetDir ?? process.cwd());
166
211
  const adapterName = resolveAdapterName(options, repoRoot);
167
212
  await getAdapter(adapterName).upgrade(repoRoot, options);
213
+ if (options.migrateJudgeDefault === true) {
214
+ const mergedConfig = await loadConfigFile(repoRoot, adapterName);
215
+ const migrated = migrateImplicitLocalJudgeIfNeeded(mergedConfig.judge, adapterName);
216
+ if (migrated) {
217
+ const configWithJudge = normalizeConfig({ ...mergedConfig, version: 4, judge: migrated });
218
+ await writeConfigFile(repoRoot, configWithJudge, adapterName);
219
+ await auditJudgeMigrationIfNeeded(repoRoot, adapterName, mergedConfig.judge, migrated, 'belay upgrade --migrate-judge-default');
220
+ }
221
+ }
168
222
  await refreshIntegrityManifest(repoRoot, adapterName);
169
223
  return { repoRoot, adapter: adapterName };
170
224
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,123 @@
1
+ import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
2
+ import { createServer } from 'node:net';
3
+ import path from 'node:path';
4
+ import { cleanupJudgeBrokerArtifacts, JUDGE_BROKER_SESSION, judgeBrokerPaths, } from './core/verdict/judge-broker-service.js';
5
+ import { runCliJsonWithTimeouts } from './core/verdict/judge-cli.js';
6
+ import { DEFAULT_JUDGE_SESSION_CONFIG, normalizeJudgeSessionConfig, resolveSessionEvalTimeoutMs, } from './core/verdict/judge-runtime-config.js';
7
+ import { JudgeSessionBroker, } from './core/verdict/judge-session-broker.js';
8
+ async function loadBrokerSessionConfig(stateDir) {
9
+ const sessionConfigPath = path.join(stateDir, JUDGE_BROKER_SESSION);
10
+ try {
11
+ const raw = JSON.parse(await readFile(sessionConfigPath, 'utf8'));
12
+ return normalizeJudgeSessionConfig({ ...raw, enabled: true });
13
+ }
14
+ catch {
15
+ return normalizeJudgeSessionConfig({ ...DEFAULT_JUDGE_SESSION_CONFIG, enabled: true });
16
+ }
17
+ }
18
+ async function main() {
19
+ const repoRoot = process.env.BELAY_JUDGE_BROKER_REPO_ROOT;
20
+ const stateDir = process.env.BELAY_JUDGE_BROKER_STATE_DIR;
21
+ if (!repoRoot || !stateDir) {
22
+ process.stderr.write('BELAY_JUDGE_BROKER_REPO_ROOT and BELAY_JUDGE_BROKER_STATE_DIR are required.\n');
23
+ process.exitCode = 1;
24
+ return;
25
+ }
26
+ const paths = judgeBrokerPaths(stateDir);
27
+ await mkdir(stateDir, { recursive: true, mode: 0o700 });
28
+ const sessionConfig = await loadBrokerSessionConfig(stateDir);
29
+ const defaultEvalTimeoutMs = resolveSessionEvalTimeoutMs(sessionConfig, 25_000);
30
+ const broker = new JudgeSessionBroker({
31
+ config: sessionConfig,
32
+ runCommand: (invocation, timeoutMs) => runCliJsonWithTimeouts(invocation, { evalTimeoutMs: timeoutMs }),
33
+ });
34
+ let idleTimer = null;
35
+ let server;
36
+ const scheduleIdleShutdown = () => {
37
+ if (idleTimer) {
38
+ clearTimeout(idleTimer);
39
+ }
40
+ idleTimer = setTimeout(() => {
41
+ void shutdown('idle_timeout');
42
+ }, sessionConfig.maxIdleMs);
43
+ };
44
+ const shutdown = async (reason) => {
45
+ broker.stopAll('manual_stop');
46
+ server?.close();
47
+ await cleanupJudgeBrokerArtifacts(paths);
48
+ process.stderr.write(`judge broker stopped (${reason})\n`);
49
+ process.exit(0);
50
+ };
51
+ server = createServer((socket) => {
52
+ scheduleIdleShutdown();
53
+ let buffer = '';
54
+ socket.on('data', (chunk) => {
55
+ buffer += String(chunk);
56
+ const lines = buffer.split('\n');
57
+ buffer = lines.pop() ?? '';
58
+ for (const line of lines) {
59
+ if (!line.trim()) {
60
+ continue;
61
+ }
62
+ void handleLine(line, socket, broker, defaultEvalTimeoutMs).catch((error) => {
63
+ const id = safeMessageId(line);
64
+ socket.write(`${JSON.stringify({
65
+ type: 'error',
66
+ id,
67
+ error: error instanceof Error ? error.message : 'judge broker handler failed',
68
+ })}\n`);
69
+ });
70
+ }
71
+ });
72
+ });
73
+ await unlink(paths.socketPath).catch(() => undefined);
74
+ await new Promise((resolve, reject) => {
75
+ server.once('error', reject);
76
+ server.listen(paths.socketPath, () => resolve());
77
+ });
78
+ const status = {
79
+ pid: process.pid,
80
+ repoRoot: path.resolve(repoRoot),
81
+ socketPath: paths.socketPath,
82
+ startedAt: new Date().toISOString(),
83
+ };
84
+ await writeFile(paths.statusPath, JSON.stringify(status), { encoding: 'utf8', mode: 0o600 });
85
+ await writeFile(paths.pidPath, String(process.pid), { encoding: 'utf8', mode: 0o600 });
86
+ process.on('SIGTERM', () => {
87
+ void shutdown('sigterm');
88
+ });
89
+ process.on('SIGINT', () => {
90
+ void shutdown('sigint');
91
+ });
92
+ scheduleIdleShutdown();
93
+ process.stdout.write(`judge broker listening on ${paths.socketPath} for ${repoRoot}\n`);
94
+ }
95
+ function safeMessageId(line) {
96
+ try {
97
+ const parsed = JSON.parse(line);
98
+ return parsed.id ?? 'unknown';
99
+ }
100
+ catch {
101
+ return 'unknown';
102
+ }
103
+ }
104
+ async function handleLine(line, socket, broker, defaultEvalTimeoutMs) {
105
+ const message = JSON.parse(line);
106
+ if (message.type === 'stop') {
107
+ socket.write(`${JSON.stringify({ type: 'stopped', id: message.id })}\n`);
108
+ broker.stopAll('manual_stop');
109
+ return;
110
+ }
111
+ if (message.type === 'invalidate' && typeof message.sessionKey === 'string') {
112
+ broker.invalidateSession(message.sessionKey, message.reason ?? 'parse_failure');
113
+ socket.write(`${JSON.stringify({ type: 'invalidated', id: message.id })}\n`);
114
+ return;
115
+ }
116
+ if (message.type !== 'evaluate' || !message.request) {
117
+ socket.write(`${JSON.stringify({ type: 'error', id: message.id, error: 'unsupported message' })}\n`);
118
+ return;
119
+ }
120
+ const result = await broker.evaluate(message.request, message.timeoutMs ?? defaultEvalTimeoutMs);
121
+ socket.write(`${JSON.stringify({ type: 'result', id: message.id, result })}\n`);
122
+ }
123
+ await main();
@@ -24,6 +24,7 @@ export interface SandboxStatusReport {
24
24
  export declare function sandboxStatus(options?: SandboxServiceOptions): Promise<SandboxStatusReport>;
25
25
  export declare function createCapabilityApprovalStore(repoRoot: string, config: Awaited<ReturnType<typeof loadConfigFile>>): {
26
26
  allowlistPath: string;
27
+ trustedRootsPath: string;
27
28
  loadPending(): Promise<{
28
29
  filePath: string;
29
30
  state: import("../types.js").ApprovalStateFile;
@@ -2,6 +2,7 @@ import path from 'node:path';
2
2
  import { loadConfigFile, repoLocalStateDirFor } from '../config-io.js';
3
3
  import { fsScopeAllowlistPath, loadFsScopeAllowlist } from '../core/capability/allowlist.js';
4
4
  import { evaluateL1FullStatus, isCapabilityBrokerDemotionActive, } from '../core/capability/broker.js';
5
+ import { trustedWorkspaceRootsPath } from '../core/capability/trusted-workspace-roots.js';
5
6
  import { configuredControlPlaneDir } from '../core/config.js';
6
7
  import { verifyControlPlaneIsolation } from '../core/control-plane-isolation.js';
7
8
  import { egressStatus } from './egress-service.js';
@@ -48,6 +49,7 @@ export function createCapabilityApprovalStore(repoRoot, config) {
48
49
  const repoLocalDir = repoLocalStateDirFor(repoRoot, config);
49
50
  return {
50
51
  allowlistPath: fsScopeAllowlistPath(config, repoLocalDir),
52
+ trustedRootsPath: trustedWorkspaceRootsPath(config, repoLocalDir),
51
53
  async loadPending() {
52
54
  const { loadApprovalState, pendingApprovalsPath } = await import('../config-io.js');
53
55
  const filePath = pendingApprovalsPath(repoRoot, config);
package/dist/types.d.ts CHANGED
@@ -22,18 +22,25 @@ export interface InitOptions {
22
22
  preset?: import('./presets.js').ConfigPresetName;
23
23
  judgeProfile?: 'local-ollama' | 'cursor' | 'claude' | 'codex';
24
24
  judgeProvider?: 'ollama' | 'openai-compatible' | 'cursor';
25
+ judgeProviderId?: 'ollama' | 'codex' | 'claude' | 'cursor' | 'local' | 'openai';
25
26
  judgeModel?: string;
26
27
  judgeEndpoint?: string;
28
+ judgeCredentialMode?: 'project' | 'apiKey';
27
29
  /** Acknowledge cloud judge egress + redaction limits (R19). Required for openai-compatible provider. */
28
30
  acceptCloudJudge?: boolean;
31
+ cloudConsentApprovalId?: string;
29
32
  /** When true, skip writing judge config (used when user declines cloud consent). */
30
33
  skipJudgeWrite?: boolean;
34
+ /** Opt-in: migrate implicit factory-default ollama judge to host default provider. */
35
+ migrateJudgeDefault?: boolean;
31
36
  }
32
37
  export interface UpgradeOptions {
33
38
  targetDir?: string;
34
39
  withSkill?: boolean;
35
40
  adapter?: AdapterName;
36
41
  scope?: InstallScope;
42
+ /** Opt-in: migrate implicit factory-default ollama judge to host default provider. */
43
+ migrateJudgeDefault?: boolean;
37
44
  }
38
45
  export interface DoctorOptions {
39
46
  targetDir?: string;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const PACKAGE_VERSION = "0.2.0";
1
+ export declare const PACKAGE_VERSION = "0.4.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.2.0';
2
+ export const PACKAGE_VERSION = '0.4.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guilz-dev/belay",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Belay-style approval and audit gating for agent runtimes.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,15 +48,19 @@
48
48
  "agent-belay-logo.png"
49
49
  ],
50
50
  "scripts": {
51
- "build": "rm -rf dist && node scripts/sync-version.mjs && 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/generate-standing-allow-catalog.mjs && tsc -p tsconfig.build.json && node scripts/build-runtime.mjs",
52
52
  "check:version": "node scripts/check-cli-version.mjs",
53
+ "dogfood": "./scripts/dev-dogfood.sh",
54
+ "dev-refresh": "./scripts/dev-refresh.sh",
53
55
  "prepublishOnly": "pnpm build && node scripts/check-cli-version.mjs",
54
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",
55
57
  "typecheck": "tsc --noEmit",
56
58
  "test": "pnpm build && vitest run",
57
59
  "test:structural": "pnpm build && vitest run src/__tests__/verdict/structural-suite.test.ts",
58
60
  "test:stable": "pnpm build && vitest run && vitest run && vitest run",
59
- "corpus": "pnpm build && node scripts/corpus.mjs"
61
+ "corpus": "pnpm build && node scripts/corpus.mjs",
62
+ "probe:adversarial": "pnpm build && node scripts/adversarial-probe.mjs",
63
+ "corpus:ratchet": "pnpm build && node scripts/corpus-ratchet.mjs"
60
64
  },
61
65
  "devDependencies": {
62
66
  "@biomejs/biome": "2.4.15",
@@ -29,16 +29,22 @@ you to the CLI. It does not classify commands itself.
29
29
 
30
30
  ## Prerequisites
31
31
 
32
- Run `npx @guilz-dev/belay init` in the project root before relying on enforcement.
33
- If you only installed this skill via `npx skills add`, approval instructions are
34
- available, but the runtime gate is not installed yet. Run `belay doctor` to
35
- check whether hooks are present.
32
+ `npx skills add` installs this skill only — the runtime gate is **not installed**
33
+ until hooks are configured. Run `belay config` in the project root for interactive
34
+ setup (or `npx @guilz-dev/belay init` for non-interactive install). Run
35
+ `belay doctor` to check whether hooks are present.
36
36
 
37
37
  ## When belay blocks an action
38
38
 
39
39
  1. Read the approval ID in the deny message.
40
40
  2. Approve once with `/belay-approve <approval-id>` or `belay approve <approval-id>`.
41
- 3. Retry the original action unchanged.
41
+ 3. Retry the original action unchanged (default `approval.flow` is `one_step` for shell — the
42
+ approval response includes a replay hint when auto-replay is enabled).
43
+
44
+ For shell-only CLI replay after approval: `belay approve <approval-id> --replay`.
45
+
46
+ To restore the legacy two-step UX (approve, then always retry manually), set
47
+ `approval.flow` to `two_step` in `belay.config.json`.
42
48
 
43
49
  For why it was blocked, use `/belay why <command>` or `belay explain --command "<command>"`.
44
50
  For the latest pending ask, use `/belay explain` or `belay explain`.
@@ -48,16 +54,17 @@ For recovery advice after a block, use `/belay recover` or `belay recover`.
48
54
 
49
55
  ## Install or repair
50
56
 
51
- - Full install: `npx @guilz-dev/belay init --with-skill`
52
- - Interactive wizard: `npx @guilz-dev/belay init-wizard`
57
+ - Interactive setup: `belay config`
58
+ - Non-interactive install: `npx @guilz-dev/belay init --with-skill`
53
59
  - Health check: `belay doctor`
54
60
 
55
- Do not run init or doctor implicitly from this skill — only when the user asks.
61
+ Do not run init, config, or doctor implicitly from this skill — only when the user asks.
56
62
 
57
63
  ## CLI mapping
58
64
 
59
65
  | User intent | Command |
60
66
  | --- | --- |
67
+ | Set up or change judge settings | `belay config` |
61
68
  | Why was this blocked? | `belay explain --command "..."` |
62
69
  | Explain latest pending ask | `belay explain` |
63
70
  | Status / dogfood / audit visibility | `belay status` |
@@ -5,3 +5,20 @@ belay approve <approval-id>
5
5
  ```
6
6
 
7
7
  Then retry the original action unchanged.
8
+
9
+ With default `approval.flow: one_step`, shell approvals return a replay hint from editor
10
+ hooks. Tool and subagent approvals still require a manual retry.
11
+
12
+ To run a denied shell command from the CLI after approval (explicit opt-in; do not also retry via hooks):
13
+
14
+ ```bash
15
+ belay approve <approval-id> --replay
16
+ ```
17
+
18
+ Successful CLI replay consumes the one-shot grant. On failure, the approval stays active for one hook retry.
19
+
20
+ Restore legacy two-step UX in `belay.config.json`:
21
+
22
+ ```json
23
+ { "approval": { "flow": "two_step" } }
24
+ ```
@@ -1,21 +0,0 @@
1
- import type { AdapterName, InitOptions } from '../types.js';
2
- export interface WizardAnswers {
3
- adapter: AdapterName;
4
- scope: 'project' | 'global';
5
- withSkill: boolean;
6
- judgeProfile: 'local-ollama' | 'cursor' | 'claude' | 'codex';
7
- dogfood: boolean;
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';
13
- export declare function buildInitOptionsFromWizard(answers: WizardAnswers, targetDir?: string): InitOptions;
14
- export declare function runInitWizard(options?: {
15
- targetDir?: string;
16
- }): Promise<{
17
- repoRoot: string;
18
- withSkill: boolean;
19
- dogfood: boolean;
20
- adapter: import("../adapters/layouts/types.js").AdapterName;
21
- }>;
@@ -1,63 +0,0 @@
1
- import { stdin as input, stdout as output } from 'node:process';
2
- import readline from 'node:readline/promises';
3
- import { initProject } from '../installer.js';
4
- export function parseAdapter(value) {
5
- const normalized = (value?.trim() || 'cursor').toLowerCase();
6
- if (normalized === 'claude' || normalized === 'codex' || normalized === 'cursor') {
7
- return normalized;
8
- }
9
- throw new Error(`Unknown adapter: ${value ?? '(empty)'}`);
10
- }
11
- export function parseScope(value) {
12
- const normalized = (value?.trim() || 'project').toLowerCase();
13
- if (normalized === 'global' || normalized === 'project') {
14
- return normalized;
15
- }
16
- throw new Error(`Unknown scope: ${value ?? '(empty)'}`);
17
- }
18
- export function parseYesNo(value, defaultValue) {
19
- const normalized = (value?.trim() || (defaultValue ? 'y' : 'n')).toLowerCase();
20
- if (['y', 'yes', 'true', '1'].includes(normalized)) {
21
- return true;
22
- }
23
- if (['n', 'no', 'false', '0'].includes(normalized)) {
24
- return false;
25
- }
26
- return defaultValue;
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
- }
38
- export function buildInitOptionsFromWizard(answers, targetDir) {
39
- return {
40
- targetDir,
41
- adapter: answers.adapter,
42
- scope: answers.scope,
43
- withSkill: answers.withSkill,
44
- judgeProfile: answers.judgeProfile,
45
- dogfood: answers.dogfood,
46
- };
47
- }
48
- export async function runInitWizard(options = {}) {
49
- const rl = readline.createInterface({ input, output });
50
- try {
51
- output.write('belay init wizard\n');
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));
59
- }
60
- finally {
61
- rl.close();
62
- }
63
- }