@guilz-dev/belay 0.2.0 → 0.3.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.
- package/README.md +39 -12
- package/dist/adapters/shared/gate-runtime.js +13 -5
- package/dist/bundle/claude-runtime.mjs +1659 -371
- package/dist/bundle/codex-runtime.mjs +1658 -371
- package/dist/bundle/cursor-runtime.mjs +1659 -371
- package/dist/cli.js +198 -8
- package/dist/commands/approve.js +11 -0
- package/dist/commands/config.d.ts +70 -0
- package/dist/commands/config.js +494 -0
- package/dist/commands/doctor.js +2 -1
- package/dist/commands/health-snapshot.d.ts +6 -0
- package/dist/commands/health-snapshot.js +34 -0
- package/dist/commands/judge.d.ts +90 -0
- package/dist/commands/judge.js +285 -0
- package/dist/commands/status.js +2 -2
- package/dist/commands/stdin-key.d.ts +1 -0
- package/dist/commands/stdin-key.js +8 -0
- package/dist/conformance/guarantee-table.js +12 -0
- package/dist/conformance/types.d.ts +2 -0
- package/dist/core/audit-io.d.ts +2 -0
- package/dist/core/audit-io.js +14 -0
- package/dist/core/capability/index.d.ts +2 -2
- package/dist/core/capability/index.js +2 -2
- package/dist/core/capability/paths.d.ts +1 -0
- package/dist/core/capability/paths.js +74 -6
- package/dist/core/capability/reasons.d.ts +3 -0
- package/dist/core/capability/reasons.js +8 -0
- package/dist/core/classify-tool.js +97 -27
- package/dist/core/config-layers.js +2 -1
- package/dist/core/config.d.ts +22 -2
- package/dist/core/config.js +110 -11
- package/dist/core/credential-store.d.ts +11 -0
- package/dist/core/credential-store.js +60 -0
- package/dist/core/gate-engine.js +104 -13
- package/dist/core/integrity.d.ts +2 -0
- package/dist/core/integrity.js +13 -0
- package/dist/core/judge-api-key.d.ts +19 -0
- package/dist/core/judge-api-key.js +74 -0
- package/dist/core/judge-cloud-consent.d.ts +13 -0
- package/dist/core/judge-cloud-consent.js +38 -0
- package/dist/core/judge-config.d.ts +41 -4
- package/dist/core/judge-config.js +263 -57
- package/dist/core/judge-doctor.d.ts +6 -1
- package/dist/core/judge-doctor.js +147 -96
- package/dist/core/judge-model-discovery.d.ts +24 -0
- package/dist/core/judge-model-discovery.js +168 -0
- package/dist/core/judge-model-policy.d.ts +5 -0
- package/dist/core/judge-model-policy.js +21 -0
- package/dist/core/judge-runtime-detection.d.ts +9 -0
- package/dist/core/judge-runtime-detection.js +68 -0
- package/dist/core/transactional/diff-evaluator.js +1 -19
- package/dist/core/types.d.ts +2 -0
- package/dist/core/verdict/adapter.d.ts +1 -0
- package/dist/core/verdict/adapter.js +7 -1
- package/dist/core/verdict/containment.d.ts +5 -0
- package/dist/core/verdict/containment.js +32 -2
- package/dist/core/verdict/judge-catalog.d.ts +40 -0
- package/dist/core/verdict/judge-catalog.js +148 -0
- package/dist/core/verdict/judge-cli.d.ts +23 -0
- package/dist/core/verdict/judge-cli.js +280 -0
- package/dist/core/verdict/judge-factory.d.ts +15 -4
- package/dist/core/verdict/judge-factory.js +117 -14
- package/dist/core/verdict/judge.d.ts +20 -1
- package/dist/core/verdict/judge.js +83 -14
- package/dist/core/verdict/persistent-paths.d.ts +8 -0
- package/dist/core/verdict/persistent-paths.js +52 -0
- package/dist/core/verdict/types.d.ts +6 -2
- package/dist/core/verdict/verdict.js +161 -47
- package/dist/installer.js +66 -15
- package/dist/types.d.ts +7 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/belay/SKILL.md +8 -7
- package/dist/commands/init-wizard.d.ts +0 -21
- package/dist/commands/init-wizard.js +0 -63
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { relativeWithinRepo } from '../path-utils.js';
|
|
1
|
+
import { resolveMutationTarget } from '../path-utils.js';
|
|
3
2
|
import { extractRedirectTargets, tokenizeShell } from '../shell-tokenizer.js';
|
|
4
|
-
import { analyzePathTargets, cwdRelative, resolveTrustedPath } from './containment.js';
|
|
3
|
+
import { analyzePathTargets, cwdRelative, isDestructiveHighStakesMutation, resolveTrustedPath, touchesProtectedRoot, } from './containment.js';
|
|
5
4
|
import { classifyEgressTool } from './egress-classify.js';
|
|
6
5
|
import { verdictFingerprint } from './fingerprint.js';
|
|
7
|
-
import { prescanInterpreterCode, tier1RequiresAsk } from './judge.js';
|
|
6
|
+
import { mutationPrescanRequiresAsk, prescanInterpreterCode, tier1RequiresAsk } from './judge.js';
|
|
8
7
|
import { isRoutineLauncher, resolveLauncherRecipe } from './launcher-resolve.js';
|
|
9
8
|
import { allowFromCustomOverride, askFromCustomExternal, customAllowMatch, customExternalMatch, } from './overrides.js';
|
|
10
9
|
import { extractRecursiveScript, isBareInterpreter, isVariableIndirectHead, parseSegment, peelTransparentWrappers, redactCommand, segmentOpacity, splitTopLevelSegments, substitutionInners, } from './parser.js';
|
|
@@ -257,13 +256,45 @@ function tier0ExternalMatch(key, head, tokens) {
|
|
|
257
256
|
}
|
|
258
257
|
return false;
|
|
259
258
|
}
|
|
259
|
+
function isBelayJudgeSelfCommand(tokens) {
|
|
260
|
+
return (tokens[0] === 'belay' &&
|
|
261
|
+
tokens[1] === 'judge' &&
|
|
262
|
+
(tokens[2] === 'use' ||
|
|
263
|
+
tokens[2] === 'status' ||
|
|
264
|
+
tokens[2] === 'list' ||
|
|
265
|
+
tokens[2] === 'test' ||
|
|
266
|
+
tokens[2] === 'consent'));
|
|
267
|
+
}
|
|
268
|
+
function isBelayConfigSelfCommand(tokens) {
|
|
269
|
+
if (tokens[0] !== 'belay' || tokens[1] !== 'config') {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
const sub = tokens[2];
|
|
273
|
+
if (!sub) {
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
if (sub === 'list' || sub === 'get' || sub === 'judge') {
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
if (sub === 'credential') {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
if (sub === 'set' || sub === 'unset') {
|
|
283
|
+
const pathKey = tokens[3] ?? '';
|
|
284
|
+
return pathKey.startsWith('judge.');
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
260
288
|
function isBelaySelfCommand(tokens) {
|
|
261
289
|
const head = tokens[0] ?? '';
|
|
262
290
|
const subcommand = tokens[1] ?? '';
|
|
263
|
-
return head === 'belay' &&
|
|
291
|
+
return (head === 'belay' &&
|
|
292
|
+
(BELAY_SELF_COMMANDS.has(subcommand) ||
|
|
293
|
+
isBelayJudgeSelfCommand(tokens) ||
|
|
294
|
+
isBelayConfigSelfCommand(tokens)));
|
|
264
295
|
}
|
|
265
296
|
function tier0HighStakesRm(tokens, context) {
|
|
266
|
-
const head = tokens[0] ?? '';
|
|
297
|
+
const head = (tokens[0] ?? '').split('/').pop() ?? '';
|
|
267
298
|
if (head !== 'rm') {
|
|
268
299
|
return null;
|
|
269
300
|
}
|
|
@@ -286,31 +317,40 @@ function tier0HighStakesRm(tokens, context) {
|
|
|
286
317
|
signals: ['missing_trusted_cwd', ...analysis.signals],
|
|
287
318
|
});
|
|
288
319
|
}
|
|
289
|
-
if (analysis.isHighStakes) {
|
|
290
|
-
return askVerdict({
|
|
291
|
-
location: analysis.location,
|
|
292
|
-
opacity: 'transparent',
|
|
293
|
-
effect: 'local_mutation',
|
|
294
|
-
confidence: 'deterministic',
|
|
295
|
-
reason: 'high_stakes_path',
|
|
296
|
-
signals: ['high_stakes_path', ...analysis.signals],
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
320
|
for (const target of targets) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
321
|
+
const normalized = target.replace(/^['"]|['"]$/g, '');
|
|
322
|
+
if (normalized === '~' || normalized === '$HOME' || normalized.startsWith('~/')) {
|
|
323
|
+
return askVerdict({
|
|
324
|
+
location: 'repo_outside',
|
|
325
|
+
opacity: 'transparent',
|
|
326
|
+
effect: 'local_mutation',
|
|
327
|
+
confidence: 'deterministic',
|
|
328
|
+
reason: 'high_stakes_path',
|
|
329
|
+
signals: ['catastrophic_home_deletion', ...analysis.signals],
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (analysis.isHighStakes && analysis.signals.includes('high_stakes_path')) {
|
|
334
|
+
let destructiveHighStakes = false;
|
|
335
|
+
for (const target of targets) {
|
|
336
|
+
const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ??
|
|
337
|
+
resolveMutationTarget(target, context.cwd);
|
|
338
|
+
if (resolved &&
|
|
339
|
+
isDestructiveHighStakesMutation(head, resolved, context.repoRoot, context.sensitivePaths, context.protectedArtifactRoots)) {
|
|
340
|
+
destructiveHighStakes = true;
|
|
341
|
+
break;
|
|
312
342
|
}
|
|
313
343
|
}
|
|
344
|
+
if (destructiveHighStakes) {
|
|
345
|
+
return askVerdict({
|
|
346
|
+
location: analysis.location,
|
|
347
|
+
opacity: 'transparent',
|
|
348
|
+
effect: 'local_mutation',
|
|
349
|
+
confidence: 'deterministic',
|
|
350
|
+
reason: 'high_stakes_path',
|
|
351
|
+
signals: ['high_stakes_path', ...analysis.signals],
|
|
352
|
+
});
|
|
353
|
+
}
|
|
314
354
|
}
|
|
315
355
|
return null;
|
|
316
356
|
}
|
|
@@ -515,6 +555,9 @@ async function evaluateSegment(command, context, depth) {
|
|
|
515
555
|
effect = 'local_mutation';
|
|
516
556
|
}
|
|
517
557
|
const pathArgs = extractPathArgs(peeled);
|
|
558
|
+
if (extractRedirectTargets(peeled).length > 0 && effect === 'read_only') {
|
|
559
|
+
effect = 'local_mutation';
|
|
560
|
+
}
|
|
518
561
|
const pathAnalysis = analyzePathTargets({
|
|
519
562
|
targets: pathArgs,
|
|
520
563
|
cwd: context.cwd,
|
|
@@ -545,15 +588,45 @@ async function evaluateSegment(command, context, depth) {
|
|
|
545
588
|
});
|
|
546
589
|
}
|
|
547
590
|
}
|
|
548
|
-
if (
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
591
|
+
if ((effect === 'local_mutation' || effect === 'unknown') &&
|
|
592
|
+
context.protectedArtifactRoots &&
|
|
593
|
+
context.protectedArtifactRoots.length > 0) {
|
|
594
|
+
for (const target of pathArgs) {
|
|
595
|
+
const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ??
|
|
596
|
+
resolveMutationTarget(target, context.cwd);
|
|
597
|
+
if (resolved && touchesProtectedRoot(resolved, context.protectedArtifactRoots)) {
|
|
598
|
+
return askVerdict({
|
|
599
|
+
location: pathAnalysis.location,
|
|
600
|
+
opacity: 'transparent',
|
|
601
|
+
effect: 'local_mutation',
|
|
602
|
+
confidence: 'deterministic',
|
|
603
|
+
reason: 'high_stakes_path',
|
|
604
|
+
signals: ['high_stakes_path', 'control_plane_path'],
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (effect === 'local_mutation' || effect === 'unknown') {
|
|
610
|
+
let destructiveHighStakes = false;
|
|
611
|
+
for (const target of pathArgs) {
|
|
612
|
+
const resolved = resolveTrustedPath(target, context.cwd, context.trustedCwd) ??
|
|
613
|
+
resolveMutationTarget(target, context.cwd);
|
|
614
|
+
if (resolved &&
|
|
615
|
+
isDestructiveHighStakesMutation(segment.head, resolved, context.repoRoot, context.sensitivePaths, context.protectedArtifactRoots)) {
|
|
616
|
+
destructiveHighStakes = true;
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (destructiveHighStakes) {
|
|
621
|
+
return askVerdict({
|
|
622
|
+
location: pathAnalysis.location,
|
|
623
|
+
opacity: 'transparent',
|
|
624
|
+
effect: 'local_mutation',
|
|
625
|
+
confidence: 'deterministic',
|
|
626
|
+
reason: 'high_stakes_path',
|
|
627
|
+
signals: pathAnalysis.signals,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
557
630
|
}
|
|
558
631
|
if (segment.head === 'find' && isFindDangerous(peeled)) {
|
|
559
632
|
return askVerdict({
|
|
@@ -565,15 +638,15 @@ async function evaluateSegment(command, context, depth) {
|
|
|
565
638
|
signals: ['find_dangerous_action'],
|
|
566
639
|
});
|
|
567
640
|
}
|
|
568
|
-
if (
|
|
569
|
-
|
|
570
|
-
return
|
|
641
|
+
if (effect === 'read_only' &&
|
|
642
|
+
(pathAnalysis.location === 'repo_outside' || pathAnalysis.location === 'mixed')) {
|
|
643
|
+
return allowVerdict({
|
|
571
644
|
location: pathAnalysis.location,
|
|
572
|
-
opacity
|
|
573
|
-
effect:
|
|
645
|
+
opacity,
|
|
646
|
+
effect: 'read_only',
|
|
574
647
|
confidence: 'deterministic',
|
|
575
|
-
reason: '
|
|
576
|
-
signals: ['
|
|
648
|
+
reason: 'read_only',
|
|
649
|
+
signals: ['read_only', ...pathAnalysis.signals],
|
|
577
650
|
});
|
|
578
651
|
}
|
|
579
652
|
if (pathAnalysis.location === 'unknown' &&
|
|
@@ -588,7 +661,34 @@ async function evaluateSegment(command, context, depth) {
|
|
|
588
661
|
signals: ['unknown_location_mutation'],
|
|
589
662
|
});
|
|
590
663
|
}
|
|
591
|
-
const
|
|
664
|
+
const outsideMutation = pathAnalysis.location === 'repo_outside' || pathAnalysis.location === 'mixed';
|
|
665
|
+
const mutationPrescan = (effect === 'local_mutation' || effect === 'unknown') && pathArgs.length > 0
|
|
666
|
+
? mutationPrescanRequiresAsk({
|
|
667
|
+
targets: pathArgs,
|
|
668
|
+
cwd: context.cwd,
|
|
669
|
+
repoRoot: context.repoRoot,
|
|
670
|
+
trustedCwd: context.trustedCwd,
|
|
671
|
+
sensitivePaths: context.sensitivePaths,
|
|
672
|
+
})
|
|
673
|
+
: null;
|
|
674
|
+
if (mutationPrescan) {
|
|
675
|
+
return askVerdict({
|
|
676
|
+
location: pathAnalysis.location === 'unknown'
|
|
677
|
+
? 'unknown'
|
|
678
|
+
: pathAnalysis.location === 'repo_outside' || pathAnalysis.location === 'mixed'
|
|
679
|
+
? pathAnalysis.location
|
|
680
|
+
: 'repo_local',
|
|
681
|
+
opacity,
|
|
682
|
+
effect: 'local_mutation',
|
|
683
|
+
confidence: 'deterministic',
|
|
684
|
+
reason: 'tier1_catastrophic',
|
|
685
|
+
signals: ['tier1_catastrophic', mutationPrescan.reason],
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
const needsTier1 = effect === 'unknown' ||
|
|
689
|
+
TIER0_EXTERNAL_HEADS.has(segment.head) ||
|
|
690
|
+
egressClass === 'ambiguous' ||
|
|
691
|
+
(outsideMutation && effect !== 'read_only');
|
|
592
692
|
let tier1Trace;
|
|
593
693
|
if (needsTier1) {
|
|
594
694
|
const tier1Text = recursiveScript ?? command;
|
|
@@ -600,9 +700,13 @@ async function evaluateSegment(command, context, depth) {
|
|
|
600
700
|
tier1Trace = context.judge.lastTrace;
|
|
601
701
|
if (tier1RequiresAsk(tier1)) {
|
|
602
702
|
return askVerdict({
|
|
603
|
-
location: pathAnalysis.location === 'unknown'
|
|
703
|
+
location: pathAnalysis.location === 'unknown'
|
|
704
|
+
? 'unknown'
|
|
705
|
+
: pathAnalysis.location === 'repo_outside' || pathAnalysis.location === 'mixed'
|
|
706
|
+
? pathAnalysis.location
|
|
707
|
+
: 'repo_local',
|
|
604
708
|
opacity,
|
|
605
|
-
effect: tier1.
|
|
709
|
+
effect: !tier1.local_recoverable ? 'remote_mutation' : effect,
|
|
606
710
|
confidence: 'llm',
|
|
607
711
|
reason: 'tier1_catastrophic',
|
|
608
712
|
signals: ['tier1_catastrophic', tier1.reason],
|
|
@@ -610,6 +714,16 @@ async function evaluateSegment(command, context, depth) {
|
|
|
610
714
|
});
|
|
611
715
|
}
|
|
612
716
|
}
|
|
717
|
+
if (outsideMutation && effect !== 'read_only') {
|
|
718
|
+
return withJudgeTrace(allowVerdict({
|
|
719
|
+
location: pathAnalysis.location,
|
|
720
|
+
opacity,
|
|
721
|
+
effect: 'local_mutation',
|
|
722
|
+
confidence: tier1Trace ? 'llm' : 'deterministic',
|
|
723
|
+
reason: 'repo_outside_local_mutation',
|
|
724
|
+
signals: ['repo_outside_local_mutation', ...pathAnalysis.signals],
|
|
725
|
+
}), tier1Trace);
|
|
726
|
+
}
|
|
613
727
|
if (pathAnalysis.location === 'repo_local' &&
|
|
614
728
|
(effect === 'read_only' || effect === 'local_mutation') &&
|
|
615
729
|
opacity !== 'opaque') {
|
package/dist/installer.js
CHANGED
|
@@ -7,9 +7,10 @@ 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 {
|
|
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';
|
|
13
14
|
import { bootstrapStateFiles, writeSkillArtifacts } from './installer/bootstrap.js';
|
|
14
15
|
import { writeRuntimeArtifacts } from './installer/runtime-artifacts.js';
|
|
15
16
|
import { applyInstallScope, resolveOperationScope } from './installer/scope-config.js';
|
|
@@ -104,32 +105,62 @@ function resolveAdapterName(options, repoRoot) {
|
|
|
104
105
|
}
|
|
105
106
|
return 'cursor';
|
|
106
107
|
}
|
|
107
|
-
async function
|
|
108
|
-
if (
|
|
108
|
+
async function auditJudgeMigrationIfNeeded(repoRoot, adapterName, before, after, by) {
|
|
109
|
+
if (before.providerId === after.providerId && before.provider === after.provider) {
|
|
109
110
|
return;
|
|
110
111
|
}
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
const config = await loadConfigFile(repoRoot, adapterName);
|
|
113
|
+
await appendCliAuditEvent(repoRoot, config, {
|
|
114
|
+
event: 'judge_provider_changed',
|
|
115
|
+
from: { providerId: before.providerId, provider: before.provider },
|
|
116
|
+
to: { providerId: after.providerId, provider: after.provider },
|
|
117
|
+
by,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async function applyInitJudgeConfig(repoRoot, adapterName, options, isFreshBeforeInstall) {
|
|
121
|
+
if (options.skipJudgeWrite) {
|
|
122
|
+
return;
|
|
116
123
|
}
|
|
117
|
-
const isFresh = isFreshConfigInput(existingConfig);
|
|
118
124
|
const mergedConfig = await loadConfigFile(repoRoot, adapterName);
|
|
119
|
-
const hasExplicitJudgeFlags = options.judgeProfile ||
|
|
125
|
+
const hasExplicitJudgeFlags = options.judgeProfile ||
|
|
126
|
+
options.judgeProvider ||
|
|
127
|
+
options.judgeProviderId ||
|
|
128
|
+
options.judgeModel ||
|
|
129
|
+
options.judgeEndpoint;
|
|
120
130
|
const judge = resolveInitJudgeConfig({
|
|
121
|
-
isFresh,
|
|
131
|
+
isFresh: isFreshBeforeInstall,
|
|
122
132
|
hasExplicitJudgeFlags: Boolean(hasExplicitJudgeFlags),
|
|
123
133
|
judgeProfile: options.judgeProfile,
|
|
124
134
|
judgeProvider: options.judgeProvider,
|
|
135
|
+
judgeProviderId: options.judgeProviderId,
|
|
125
136
|
judgeModel: options.judgeModel,
|
|
126
137
|
judgeEndpoint: options.judgeEndpoint,
|
|
127
138
|
acceptCloudJudge: options.acceptCloudJudge,
|
|
139
|
+
interactiveConsent: Boolean(options.acceptCloudJudge && process.stdin.isTTY),
|
|
140
|
+
cloudConsentApprovalId: options.cloudConsentApprovalId,
|
|
128
141
|
existingJudge: mergedConfig.judge,
|
|
129
|
-
|
|
142
|
+
adapter: adapterName,
|
|
130
143
|
});
|
|
131
|
-
const
|
|
144
|
+
const migrated = options.migrateJudgeDefault === true
|
|
145
|
+
? migrateImplicitLocalJudgeIfNeeded(mergedConfig.judge, adapterName)
|
|
146
|
+
: null;
|
|
147
|
+
let finalJudge = migrated ?? judge;
|
|
148
|
+
if (options.judgeCredentialMode) {
|
|
149
|
+
finalJudge = {
|
|
150
|
+
...finalJudge,
|
|
151
|
+
credential: options.judgeCredentialMode === 'apiKey'
|
|
152
|
+
? { mode: 'apiKey', ref: 'store:judge' }
|
|
153
|
+
: { mode: 'project' },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const configWithJudge = normalizeConfig({ ...mergedConfig, version: 4, judge: finalJudge });
|
|
157
|
+
if (isCloudJudgeConfig(configWithJudge.judge) && !hasValidCloudConsent(configWithJudge.judge)) {
|
|
158
|
+
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');
|
|
159
|
+
}
|
|
132
160
|
await writeConfigFile(repoRoot, configWithJudge, adapterName);
|
|
161
|
+
if (migrated) {
|
|
162
|
+
await auditJudgeMigrationIfNeeded(repoRoot, adapterName, mergedConfig.judge, finalJudge, options.migrateJudgeDefault ? 'belay init --migrate-judge-default' : 'belay init');
|
|
163
|
+
}
|
|
133
164
|
}
|
|
134
165
|
async function refreshIntegrityManifest(repoRoot, adapterName) {
|
|
135
166
|
const layout = getAdapter(adapterName).layout;
|
|
@@ -142,8 +173,19 @@ export async function initProject(options = {}) {
|
|
|
142
173
|
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
143
174
|
const adapterName = resolveAdapterName(options, repoRoot);
|
|
144
175
|
const adapter = getAdapter(adapterName);
|
|
176
|
+
const configPath = adapter.layout.configPath(repoRoot);
|
|
177
|
+
let isFreshBeforeInstall = true;
|
|
178
|
+
if (await pathExists(configPath)) {
|
|
179
|
+
try {
|
|
180
|
+
const raw = await readFile(configPath, 'utf8');
|
|
181
|
+
isFreshBeforeInstall = isFreshConfigInput(JSON.parse(raw));
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
isFreshBeforeInstall = false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
145
187
|
const result = await adapter.install(repoRoot, options);
|
|
146
|
-
await applyInitJudgeConfig(repoRoot, adapterName, options);
|
|
188
|
+
await applyInitJudgeConfig(repoRoot, adapterName, options, isFreshBeforeInstall);
|
|
147
189
|
if (options.preset) {
|
|
148
190
|
const existing = await loadConfigFile(repoRoot, adapterName);
|
|
149
191
|
const presetConfig = mergeConfig(applyConfigPreset(options.preset));
|
|
@@ -165,6 +207,15 @@ export async function upgradeProject(options = {}) {
|
|
|
165
207
|
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
166
208
|
const adapterName = resolveAdapterName(options, repoRoot);
|
|
167
209
|
await getAdapter(adapterName).upgrade(repoRoot, options);
|
|
210
|
+
if (options.migrateJudgeDefault === true) {
|
|
211
|
+
const mergedConfig = await loadConfigFile(repoRoot, adapterName);
|
|
212
|
+
const migrated = migrateImplicitLocalJudgeIfNeeded(mergedConfig.judge, adapterName);
|
|
213
|
+
if (migrated) {
|
|
214
|
+
const configWithJudge = normalizeConfig({ ...mergedConfig, version: 4, judge: migrated });
|
|
215
|
+
await writeConfigFile(repoRoot, configWithJudge, adapterName);
|
|
216
|
+
await auditJudgeMigrationIfNeeded(repoRoot, adapterName, mergedConfig.judge, migrated, 'belay upgrade --migrate-judge-default');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
168
219
|
await refreshIntegrityManifest(repoRoot, adapterName);
|
|
169
220
|
return { repoRoot, adapter: adapterName };
|
|
170
221
|
}
|
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.
|
|
1
|
+
export declare const PACKAGE_VERSION = "0.3.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
|
+
export const PACKAGE_VERSION = '0.3.0';
|
package/package.json
CHANGED
package/skills/belay/SKILL.md
CHANGED
|
@@ -29,10 +29,10 @@ you to the CLI. It does not classify commands itself.
|
|
|
29
29
|
|
|
30
30
|
## Prerequisites
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
|
@@ -48,16 +48,17 @@ For recovery advice after a block, use `/belay recover` or `belay recover`.
|
|
|
48
48
|
|
|
49
49
|
## Install or repair
|
|
50
50
|
|
|
51
|
-
-
|
|
52
|
-
-
|
|
51
|
+
- Interactive setup: `belay config`
|
|
52
|
+
- Non-interactive install: `npx @guilz-dev/belay init --with-skill`
|
|
53
53
|
- Health check: `belay doctor`
|
|
54
54
|
|
|
55
|
-
Do not run init or doctor implicitly from this skill — only when the user asks.
|
|
55
|
+
Do not run init, config, or doctor implicitly from this skill — only when the user asks.
|
|
56
56
|
|
|
57
57
|
## CLI mapping
|
|
58
58
|
|
|
59
59
|
| User intent | Command |
|
|
60
60
|
| --- | --- |
|
|
61
|
+
| Set up or change judge settings | `belay config` |
|
|
61
62
|
| Why was this blocked? | `belay explain --command "..."` |
|
|
62
63
|
| Explain latest pending ask | `belay explain` |
|
|
63
64
|
| Status / dogfood / audit visibility | `belay status` |
|
|
@@ -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
|
-
}
|