@guilz-dev/belay 0.1.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/LICENSE +21 -0
- package/README.md +268 -0
- package/agent-belay-logo.png +0 -0
- package/dist/adapters/claude/adapter.d.ts +7 -0
- package/dist/adapters/claude/adapter.js +114 -0
- package/dist/adapters/claude/hooks.d.ts +13 -0
- package/dist/adapters/claude/hooks.js +49 -0
- package/dist/adapters/claude/runtime-entry.d.ts +4 -0
- package/dist/adapters/claude/runtime-entry.js +260 -0
- package/dist/adapters/codex/adapter.d.ts +7 -0
- package/dist/adapters/codex/adapter.js +73 -0
- package/dist/adapters/codex/hooks.d.ts +21 -0
- package/dist/adapters/codex/hooks.js +78 -0
- package/dist/adapters/codex/runtime-entry.d.ts +4 -0
- package/dist/adapters/codex/runtime-entry.js +237 -0
- package/dist/adapters/cursor/adapter.d.ts +7 -0
- package/dist/adapters/cursor/adapter.js +29 -0
- package/dist/adapters/cursor/hooks.d.ts +2 -0
- package/dist/adapters/cursor/hooks.js +26 -0
- package/dist/adapters/cursor/runtime-entry.d.ts +4 -0
- package/dist/adapters/cursor/runtime-entry.js +143 -0
- package/dist/adapters/layouts/claude.d.ts +2 -0
- package/dist/adapters/layouts/claude.js +40 -0
- package/dist/adapters/layouts/codex.d.ts +2 -0
- package/dist/adapters/layouts/codex.js +43 -0
- package/dist/adapters/layouts/cursor.d.ts +2 -0
- package/dist/adapters/layouts/cursor.js +40 -0
- package/dist/adapters/layouts/index.d.ts +7 -0
- package/dist/adapters/layouts/index.js +23 -0
- package/dist/adapters/layouts/protected-paths.d.ts +3 -0
- package/dist/adapters/layouts/protected-paths.js +15 -0
- package/dist/adapters/layouts/scope.d.ts +19 -0
- package/dist/adapters/layouts/scope.js +86 -0
- package/dist/adapters/layouts/types.d.ts +14 -0
- package/dist/adapters/layouts/types.js +1 -0
- package/dist/adapters/registry.d.ts +4 -0
- package/dist/adapters/registry.js +14 -0
- package/dist/adapters/shared/gate-runtime.d.ts +51 -0
- package/dist/adapters/shared/gate-runtime.js +518 -0
- package/dist/adapters/shared/repo-root.d.ts +2 -0
- package/dist/adapters/shared/repo-root.js +17 -0
- package/dist/adapters/types.d.ts +19 -0
- package/dist/adapters/types.js +1 -0
- package/dist/branding.d.ts +3 -0
- package/dist/branding.js +3 -0
- package/dist/bundle/claude-runtime.mjs +5323 -0
- package/dist/bundle/codex-runtime.mjs +5310 -0
- package/dist/bundle/cursor-runtime.mjs +5208 -0
- package/dist/cleanup-orphans.d.ts +7 -0
- package/dist/cleanup-orphans.js +59 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +631 -0
- package/dist/commands/approve.d.ts +14 -0
- package/dist/commands/approve.js +65 -0
- package/dist/commands/audit.d.ts +59 -0
- package/dist/commands/audit.js +132 -0
- package/dist/commands/classify-for-report.d.ts +9 -0
- package/dist/commands/classify-for-report.js +85 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.js +366 -0
- package/dist/commands/dogfood.d.ts +5 -0
- package/dist/commands/dogfood.js +71 -0
- package/dist/commands/explain.d.ts +3 -0
- package/dist/commands/explain.js +133 -0
- package/dist/commands/health-snapshot.d.ts +2 -0
- package/dist/commands/health-snapshot.js +166 -0
- package/dist/commands/init-wizard.d.ts +16 -0
- package/dist/commands/init-wizard.js +50 -0
- package/dist/commands/metrics.d.ts +7 -0
- package/dist/commands/metrics.js +89 -0
- package/dist/commands/recover.d.ts +3 -0
- package/dist/commands/recover.js +105 -0
- package/dist/commands/report.d.ts +3 -0
- package/dist/commands/report.js +65 -0
- package/dist/commands/revoke.d.ts +5 -0
- package/dist/commands/revoke.js +22 -0
- package/dist/commands/simulate.d.ts +14 -0
- package/dist/commands/simulate.js +55 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.js +107 -0
- package/dist/config-io.d.ts +23 -0
- package/dist/config-io.js +180 -0
- package/dist/conformance/guarantee-table.d.ts +14 -0
- package/dist/conformance/guarantee-table.js +95 -0
- package/dist/conformance/types.d.ts +6 -0
- package/dist/conformance/types.js +1 -0
- package/dist/core/approval-service.d.ts +26 -0
- package/dist/core/approval-service.js +41 -0
- package/dist/core/approval-token.d.ts +11 -0
- package/dist/core/approval-token.js +61 -0
- package/dist/core/approval.d.ts +19 -0
- package/dist/core/approval.js +58 -0
- package/dist/core/audit-analysis.d.ts +10 -0
- package/dist/core/audit-analysis.js +147 -0
- package/dist/core/audit-metrics.d.ts +51 -0
- package/dist/core/audit-metrics.js +155 -0
- package/dist/core/audit-query.d.ts +11 -0
- package/dist/core/audit-query.js +142 -0
- package/dist/core/audit-summary.d.ts +33 -0
- package/dist/core/audit-summary.js +111 -0
- package/dist/core/audit-types.d.ts +65 -0
- package/dist/core/audit-types.js +2 -0
- package/dist/core/capability/allowlist.d.ts +10 -0
- package/dist/core/capability/allowlist.js +53 -0
- package/dist/core/capability/broker.d.ts +17 -0
- package/dist/core/capability/broker.js +29 -0
- package/dist/core/capability/index.d.ts +5 -0
- package/dist/core/capability/index.js +4 -0
- package/dist/core/capability/paths.d.ts +1 -0
- package/dist/core/capability/paths.js +20 -0
- package/dist/core/capability/reasons.d.ts +2 -0
- package/dist/core/capability/reasons.js +4 -0
- package/dist/core/capability/types.d.ts +10 -0
- package/dist/core/capability/types.js +1 -0
- package/dist/core/capability-approval.d.ts +28 -0
- package/dist/core/capability-approval.js +43 -0
- package/dist/core/classify-subagent.d.ts +2 -0
- package/dist/core/classify-subagent.js +69 -0
- package/dist/core/classify-tool.d.ts +3 -0
- package/dist/core/classify-tool.js +311 -0
- package/dist/core/config-layers.d.ts +23 -0
- package/dist/core/config-layers.js +59 -0
- package/dist/core/config.d.ts +219 -0
- package/dist/core/config.js +720 -0
- package/dist/core/control-plane-isolation.d.ts +10 -0
- package/dist/core/control-plane-isolation.js +83 -0
- package/dist/core/custom-command-match.d.ts +2 -0
- package/dist/core/custom-command-match.js +8 -0
- package/dist/core/egress/allowlist.d.ts +7 -0
- package/dist/core/egress/allowlist.js +33 -0
- package/dist/core/egress/env.d.ts +3 -0
- package/dist/core/egress/env.js +17 -0
- package/dist/core/egress/fingerprint.d.ts +3 -0
- package/dist/core/egress/fingerprint.js +35 -0
- package/dist/core/egress/policy.d.ts +8 -0
- package/dist/core/egress/policy.js +47 -0
- package/dist/core/egress/proxy-server.d.ts +21 -0
- package/dist/core/egress/proxy-server.js +263 -0
- package/dist/core/egress/types.d.ts +25 -0
- package/dist/core/egress/types.js +1 -0
- package/dist/core/egress-approval.d.ts +48 -0
- package/dist/core/egress-approval.js +129 -0
- package/dist/core/fingerprint.d.ts +6 -0
- package/dist/core/fingerprint.js +24 -0
- package/dist/core/gate-contract.d.ts +48 -0
- package/dist/core/gate-contract.js +50 -0
- package/dist/core/gate-engine.d.ts +20 -0
- package/dist/core/gate-engine.js +172 -0
- package/dist/core/glob.d.ts +1 -0
- package/dist/core/glob.js +39 -0
- package/dist/core/index.d.ts +19 -0
- package/dist/core/index.js +15 -0
- package/dist/core/integrity.d.ts +15 -0
- package/dist/core/integrity.js +68 -0
- package/dist/core/judge-api-key.d.ts +4 -0
- package/dist/core/judge-api-key.js +11 -0
- package/dist/core/judge-config.d.ts +29 -0
- package/dist/core/judge-config.js +85 -0
- package/dist/core/judge-doctor.d.ts +7 -0
- package/dist/core/judge-doctor.js +124 -0
- package/dist/core/judgment.d.ts +6 -0
- package/dist/core/judgment.js +38 -0
- package/dist/core/notify.d.ts +13 -0
- package/dist/core/notify.js +44 -0
- package/dist/core/path-utils.d.ts +12 -0
- package/dist/core/path-utils.js +107 -0
- package/dist/core/reclassify.d.ts +15 -0
- package/dist/core/reclassify.js +82 -0
- package/dist/core/recover-advice.d.ts +30 -0
- package/dist/core/recover-advice.js +177 -0
- package/dist/core/recover-git-probe.d.ts +8 -0
- package/dist/core/recover-git-probe.js +50 -0
- package/dist/core/recover-select.d.ts +10 -0
- package/dist/core/recover-select.js +60 -0
- package/dist/core/scrub.d.ts +3 -0
- package/dist/core/scrub.js +87 -0
- package/dist/core/shell-substitution.d.ts +6 -0
- package/dist/core/shell-substitution.js +130 -0
- package/dist/core/shell-tokenizer.d.ts +5 -0
- package/dist/core/shell-tokenizer.js +129 -0
- package/dist/core/shell-unparseable.d.ts +4 -0
- package/dist/core/shell-unparseable.js +96 -0
- package/dist/core/transactional/diff-evaluator.d.ts +2 -0
- package/dist/core/transactional/diff-evaluator.js +84 -0
- package/dist/core/transactional/eligibility.d.ts +4 -0
- package/dist/core/transactional/eligibility.js +44 -0
- package/dist/core/transactional/git-worktree.d.ts +13 -0
- package/dist/core/transactional/git-worktree.js +189 -0
- package/dist/core/transactional/index.d.ts +5 -0
- package/dist/core/transactional/index.js +4 -0
- package/dist/core/transactional/reasons.d.ts +4 -0
- package/dist/core/transactional/reasons.js +8 -0
- package/dist/core/transactional/runner.d.ts +2 -0
- package/dist/core/transactional/runner.js +113 -0
- package/dist/core/transactional/types.d.ts +46 -0
- package/dist/core/transactional/types.js +1 -0
- package/dist/core/types.d.ts +90 -0
- package/dist/core/types.js +1 -0
- package/dist/core/v2/adapter.d.ts +14 -0
- package/dist/core/v2/adapter.js +118 -0
- package/dist/core/v2/containment.d.ts +19 -0
- package/dist/core/v2/containment.js +91 -0
- package/dist/core/v2/egress-classify.d.ts +7 -0
- package/dist/core/v2/egress-classify.js +216 -0
- package/dist/core/v2/fingerprint.d.ts +1 -0
- package/dist/core/v2/fingerprint.js +4 -0
- package/dist/core/v2/index.d.ts +12 -0
- package/dist/core/v2/index.js +10 -0
- package/dist/core/v2/judge-audit.d.ts +2 -0
- package/dist/core/v2/judge-audit.js +15 -0
- package/dist/core/v2/judge-factory.d.ts +25 -0
- package/dist/core/v2/judge-factory.js +75 -0
- package/dist/core/v2/judge-outbound.d.ts +12 -0
- package/dist/core/v2/judge-outbound.js +73 -0
- package/dist/core/v2/judge.d.ts +47 -0
- package/dist/core/v2/judge.js +264 -0
- package/dist/core/v2/launcher-resolve.d.ts +12 -0
- package/dist/core/v2/launcher-resolve.js +190 -0
- package/dist/core/v2/overrides.d.ts +7 -0
- package/dist/core/v2/overrides.js +37 -0
- package/dist/core/v2/parser.d.ts +21 -0
- package/dist/core/v2/parser.js +213 -0
- package/dist/core/v2/types.d.ts +67 -0
- package/dist/core/v2/types.js +1 -0
- package/dist/core/v2/verdict.d.ts +2 -0
- package/dist/core/v2/verdict.js +699 -0
- package/dist/corpus/evaluate.d.ts +24 -0
- package/dist/corpus/evaluate.js +69 -0
- package/dist/defaults.d.ts +18 -0
- package/dist/defaults.js +155 -0
- package/dist/egress-daemon.d.ts +1 -0
- package/dist/egress-daemon.js +52 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +15 -0
- package/dist/installer/bootstrap.d.ts +5 -0
- package/dist/installer/bootstrap.js +61 -0
- package/dist/installer/runtime-artifacts.d.ts +3 -0
- package/dist/installer/runtime-artifacts.js +23 -0
- package/dist/installer/scope-config.d.ts +8 -0
- package/dist/installer/scope-config.js +25 -0
- package/dist/installer.d.ts +22 -0
- package/dist/installer.js +169 -0
- package/dist/node-resolution.d.ts +8 -0
- package/dist/node-resolution.js +237 -0
- package/dist/operational-insights.d.ts +19 -0
- package/dist/operational-insights.js +24 -0
- package/dist/presets.d.ts +4 -0
- package/dist/presets.js +95 -0
- package/dist/services/egress-service.d.ts +57 -0
- package/dist/services/egress-service.js +334 -0
- package/dist/services/sandbox-service.d.ts +38 -0
- package/dist/services/sandbox-service.js +95 -0
- package/dist/templates.d.ts +7 -0
- package/dist/templates.js +56 -0
- package/dist/types.d.ts +230 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +65 -0
- package/skills/belay/SKILL.md +52 -0
- package/skills/belay/belay-approve.md +7 -0
- package/skills/belay/belay-explain.md +11 -0
- package/skills/belay/belay-recover.md +13 -0
- package/skills/belay/belay-report.md +7 -0
- package/skills/belay/belay-status.md +9 -0
- package/skills/belay/belay-why.md +11 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { getClaudeManagedHookEntries } from '../adapters/claude/hooks.js';
|
|
6
|
+
import { getCodexManagedHookEntries } from '../adapters/codex/hooks.js';
|
|
7
|
+
import { getAdapterLayout } from '../adapters/layouts/index.js';
|
|
8
|
+
import { resolveScopedPaths } from '../adapters/layouts/scope.js';
|
|
9
|
+
import { detectAdapterName, loadLayeredConfig } from '../config-io.js';
|
|
10
|
+
import { diagnoseJudge } from '../core/judge-doctor.js';
|
|
11
|
+
import { getManagedHookEntries } from '../defaults.js';
|
|
12
|
+
import { sandboxStatus } from '../services/sandbox-service.js';
|
|
13
|
+
function skillCandidates(adapter, repoRoot) {
|
|
14
|
+
const projectAgent = adapter === 'cursor'
|
|
15
|
+
? path.join(repoRoot, '.cursor')
|
|
16
|
+
: adapter === 'claude'
|
|
17
|
+
? path.join(repoRoot, '.claude')
|
|
18
|
+
: path.join(repoRoot, '.codex');
|
|
19
|
+
const home = os.homedir();
|
|
20
|
+
const globalAgent = adapter === 'cursor'
|
|
21
|
+
? path.join(home, '.cursor')
|
|
22
|
+
: adapter === 'claude'
|
|
23
|
+
? path.join(home, '.claude')
|
|
24
|
+
: path.join(home, '.codex');
|
|
25
|
+
return [
|
|
26
|
+
path.join(projectAgent, 'skills', 'belay', 'SKILL.md'),
|
|
27
|
+
path.join(globalAgent, 'skills', 'belay', 'SKILL.md'),
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
async function managedHooksPresent(adapter, hooksPath, hooksDir, repoRoot) {
|
|
31
|
+
const managedEntries = adapter === 'cursor'
|
|
32
|
+
? getManagedHookEntries(process.platform, hooksDir, repoRoot)
|
|
33
|
+
: adapter === 'claude'
|
|
34
|
+
? getClaudeManagedHookEntries(process.platform, hooksDir, repoRoot)
|
|
35
|
+
: getCodexManagedHookEntries(process.platform, hooksDir, repoRoot);
|
|
36
|
+
if (!existsSync(hooksPath)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const content = await readFile(hooksPath, 'utf8');
|
|
40
|
+
if (adapter === 'cursor') {
|
|
41
|
+
const hooksFile = JSON.parse(content);
|
|
42
|
+
return managedEntries.every(({ event, definition }) => (hooksFile.hooks?.[event] ?? []).some((entry) => entry.command === definition.command && entry.matcher === definition.matcher));
|
|
43
|
+
}
|
|
44
|
+
if (adapter === 'claude') {
|
|
45
|
+
const settings = JSON.parse(content);
|
|
46
|
+
return managedEntries.every(({ event, definition }) => (settings.hooks?.[event] ?? []).some((entry) => entry.matcher === definition.matcher &&
|
|
47
|
+
entry.hooks?.some((hook) => hook.command === definition.command)));
|
|
48
|
+
}
|
|
49
|
+
return managedEntries.every(({ definition }) => content.includes(definition.command));
|
|
50
|
+
}
|
|
51
|
+
export async function collectHealthSnapshot(options = {}) {
|
|
52
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
53
|
+
const adapter = options.adapter ?? detectAdapterName(repoRoot);
|
|
54
|
+
const layout = getAdapterLayout(adapter);
|
|
55
|
+
const configPath = layout.configPath(repoRoot);
|
|
56
|
+
let configPresent = existsSync(configPath);
|
|
57
|
+
let installScope = 'project';
|
|
58
|
+
let judgeIssues = [];
|
|
59
|
+
let judgeWarnings = [];
|
|
60
|
+
let judgeNotes = [];
|
|
61
|
+
let containmentPosture = 'best-effort';
|
|
62
|
+
let containmentWarnings = [];
|
|
63
|
+
const additionalRiskSignals = [];
|
|
64
|
+
let l1FullActive = false;
|
|
65
|
+
if (configPresent) {
|
|
66
|
+
try {
|
|
67
|
+
const layered = await loadLayeredConfig(repoRoot, adapter);
|
|
68
|
+
installScope = layered.config.installScope === 'global' ? 'global' : 'project';
|
|
69
|
+
const judgeDoctor = await diagnoseJudge(layered.config);
|
|
70
|
+
judgeIssues = judgeDoctor.issues;
|
|
71
|
+
judgeWarnings = judgeDoctor.warnings;
|
|
72
|
+
judgeNotes = judgeDoctor.notes;
|
|
73
|
+
const sandbox = await sandboxStatus({ targetDir: repoRoot });
|
|
74
|
+
l1FullActive = sandbox.l1FullActive;
|
|
75
|
+
containmentPosture = l1FullActive ? 'l1-full' : 'best-effort';
|
|
76
|
+
if (!layered.config.sandbox.enabled || layered.config.sandbox.runtime === 'none') {
|
|
77
|
+
containmentWarnings.push('sandbox runtime is not enabled');
|
|
78
|
+
}
|
|
79
|
+
if (!layered.config.egress.enabled) {
|
|
80
|
+
containmentWarnings.push('egress proxy is not enabled');
|
|
81
|
+
}
|
|
82
|
+
else if (!sandbox.l1Full.egressProxyRunning) {
|
|
83
|
+
containmentWarnings.push('egress proxy is not running for this repository');
|
|
84
|
+
}
|
|
85
|
+
if (layered.config.controlPlane.isolation.mode === 'none') {
|
|
86
|
+
containmentWarnings.push('control-plane isolation mode is none');
|
|
87
|
+
}
|
|
88
|
+
if (!layered.config.approvalSigning.required) {
|
|
89
|
+
containmentWarnings.push('approval signing is not required');
|
|
90
|
+
}
|
|
91
|
+
if (layered.config.judge.provider === 'openai-compatible') {
|
|
92
|
+
additionalRiskSignals.push('cloud judge enabled: redacted command text may be sent to an external provider');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
configPresent = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!configPresent) {
|
|
100
|
+
containmentWarnings = ['belay config is missing or unreadable'];
|
|
101
|
+
}
|
|
102
|
+
const scopedPaths = resolveScopedPaths(layout, installScope, repoRoot);
|
|
103
|
+
const hooksPath = scopedPaths.hooksSettingsPath;
|
|
104
|
+
const hooksDir = scopedPaths.hooksDir;
|
|
105
|
+
const corePath = path.join(scopedPaths.runtimeDir, 'core.mjs');
|
|
106
|
+
const skillPath = path.join(scopedPaths.skillsDir, 'SKILL.md');
|
|
107
|
+
const commandsPath = scopedPaths.commandsDir
|
|
108
|
+
? path.join(scopedPaths.commandsDir, 'belay-approve.md')
|
|
109
|
+
: undefined;
|
|
110
|
+
const runtimePresent = existsSync(corePath);
|
|
111
|
+
const runnerPresent = existsSync(path.join(hooksDir, 'belay-runner')) ||
|
|
112
|
+
existsSync(path.join(hooksDir, 'belay-runner.cmd'));
|
|
113
|
+
const hooksInstalled = existsSync(hooksPath) && runnerPresent;
|
|
114
|
+
let managedHooksOk = false;
|
|
115
|
+
if (hooksInstalled) {
|
|
116
|
+
try {
|
|
117
|
+
managedHooksOk = await managedHooksPresent(adapter, hooksPath, hooksDir, repoRoot);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
managedHooksOk = false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const skillInstalled = existsSync(skillPath) ||
|
|
124
|
+
skillCandidates(adapter, repoRoot).some((candidate) => existsSync(candidate));
|
|
125
|
+
const floorInstalled = configPresent && hooksInstalled && managedHooksOk && runtimePresent;
|
|
126
|
+
const skillOnly = skillInstalled && !floorInstalled;
|
|
127
|
+
const missingArtifacts = [];
|
|
128
|
+
if (configPresent) {
|
|
129
|
+
for (const artifact of [
|
|
130
|
+
path.join(hooksDir, 'belay-runner'),
|
|
131
|
+
path.join(hooksDir, 'belay-before-submit.mjs'),
|
|
132
|
+
path.join(hooksDir, 'belay-shell-gate.mjs'),
|
|
133
|
+
path.join(hooksDir, 'belay-tool-gate.mjs'),
|
|
134
|
+
corePath,
|
|
135
|
+
]) {
|
|
136
|
+
if (!existsSync(artifact)) {
|
|
137
|
+
missingArtifacts.push(artifact);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
repoRoot,
|
|
143
|
+
adapter,
|
|
144
|
+
installScope,
|
|
145
|
+
configPath,
|
|
146
|
+
hooksPath,
|
|
147
|
+
skillPath,
|
|
148
|
+
commandsPath,
|
|
149
|
+
configPresent,
|
|
150
|
+
hooksInstalled,
|
|
151
|
+
managedHooksOk,
|
|
152
|
+
runtimePresent,
|
|
153
|
+
skillInstalled,
|
|
154
|
+
skillOnly,
|
|
155
|
+
commandsInstalled: commandsPath ? existsSync(commandsPath) : false,
|
|
156
|
+
floorInstalled,
|
|
157
|
+
missingArtifacts,
|
|
158
|
+
judgeIssues,
|
|
159
|
+
judgeWarnings,
|
|
160
|
+
judgeNotes,
|
|
161
|
+
containmentPosture,
|
|
162
|
+
containmentWarnings,
|
|
163
|
+
additionalRiskSignals,
|
|
164
|
+
l1FullActive,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AdapterName, InitOptions } from '../types.js';
|
|
2
|
+
export interface WizardAnswers {
|
|
3
|
+
adapter: AdapterName;
|
|
4
|
+
scope: 'project' | 'global';
|
|
5
|
+
withSkill: boolean;
|
|
6
|
+
dogfood: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function buildInitOptionsFromWizard(answers: WizardAnswers, targetDir?: string): InitOptions;
|
|
9
|
+
export declare function runInitWizard(options?: {
|
|
10
|
+
targetDir?: string;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
repoRoot: string;
|
|
13
|
+
withSkill: boolean;
|
|
14
|
+
dogfood: boolean;
|
|
15
|
+
adapter: import("../adapters/layouts/types.js").AdapterName;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
function parseAdapter(value) {
|
|
5
|
+
const normalized = (value ?? 'cursor').trim().toLowerCase();
|
|
6
|
+
if (normalized === 'claude' || normalized === 'codex' || normalized === 'cursor') {
|
|
7
|
+
return normalized;
|
|
8
|
+
}
|
|
9
|
+
throw new Error(`Unknown adapter: ${value ?? '(empty)'}`);
|
|
10
|
+
}
|
|
11
|
+
function parseScope(value) {
|
|
12
|
+
const normalized = (value ?? 'project').trim().toLowerCase();
|
|
13
|
+
if (normalized === 'global' || normalized === 'project') {
|
|
14
|
+
return normalized;
|
|
15
|
+
}
|
|
16
|
+
throw new Error(`Unknown scope: ${value ?? '(empty)'}`);
|
|
17
|
+
}
|
|
18
|
+
function parseYesNo(value, defaultValue) {
|
|
19
|
+
const normalized = (value ?? (defaultValue ? 'y' : 'n')).trim().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 buildInitOptionsFromWizard(answers, targetDir) {
|
|
29
|
+
return {
|
|
30
|
+
targetDir,
|
|
31
|
+
adapter: answers.adapter,
|
|
32
|
+
scope: answers.scope,
|
|
33
|
+
withSkill: answers.withSkill,
|
|
34
|
+
dogfood: answers.dogfood,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export async function runInitWizard(options = {}) {
|
|
38
|
+
const rl = readline.createInterface({ input, output });
|
|
39
|
+
try {
|
|
40
|
+
output.write('belay init wizard\n');
|
|
41
|
+
const adapter = parseAdapter(await rl.question('Adapter (cursor/claude/codex) [cursor]: '));
|
|
42
|
+
const scope = parseScope(await rl.question('Install scope (project/global) [project]: '));
|
|
43
|
+
const withSkill = parseYesNo(await rl.question('Install SKILL.md and slash commands? (y/n) [y]: '), true);
|
|
44
|
+
const dogfood = parseYesNo(await rl.question('Start in audit dogfood mode? (y/n) [n]: '), false);
|
|
45
|
+
return initProject(buildInitOptionsFromWizard({ adapter, scope, withSkill, dogfood }, options.targetDir));
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
rl.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AuditMetricsReport } from '../core/audit-metrics.js';
|
|
2
|
+
export interface MetricsOptions {
|
|
3
|
+
targetDir?: string;
|
|
4
|
+
json?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function metricsProject(options?: MetricsOptions): Promise<AuditMetricsReport>;
|
|
7
|
+
export declare function formatMetricsReport(report: AuditMetricsReport): string;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadConfigFile } from '../config-io.js';
|
|
4
|
+
import { computeAuditMetrics, parseAuditNdjson } from '../core/audit-metrics.js';
|
|
5
|
+
export async function metricsProject(options = {}) {
|
|
6
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
7
|
+
const config = await loadConfigFile(repoRoot);
|
|
8
|
+
const auditLogPath = path.join(repoRoot, config.audit.logPath);
|
|
9
|
+
let raw = '';
|
|
10
|
+
try {
|
|
11
|
+
raw = await readFile(auditLogPath, 'utf8');
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
raw = '';
|
|
15
|
+
}
|
|
16
|
+
const records = parseAuditNdjson(raw);
|
|
17
|
+
return computeAuditMetrics(records, {
|
|
18
|
+
auditLogPath: config.audit.logPath,
|
|
19
|
+
mode: config.mode,
|
|
20
|
+
unknownLocalEffect: config.policy.unknownLocalEffect,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function formatMetricsReport(report) {
|
|
24
|
+
const lines = [
|
|
25
|
+
`belay metrics for ${report.auditLogPath}`,
|
|
26
|
+
`Schema: v${report.schemaVersion}`,
|
|
27
|
+
`Gate events: ${report.gateEvents}`,
|
|
28
|
+
`Would-block: ${report.wouldBlockCount} (${(report.wouldBlockRate * 100).toFixed(1)}%)`,
|
|
29
|
+
`Approvals recorded during audit: ${report.approvalRecordedCount}`,
|
|
30
|
+
];
|
|
31
|
+
if (report.approvalLatency.count > 0) {
|
|
32
|
+
lines.push(`Approval latency: median ${report.approvalLatency.medianMs ?? 0}ms, p95 ${report.approvalLatency.p95Ms ?? 0}ms (${report.approvalLatency.count} samples)`);
|
|
33
|
+
}
|
|
34
|
+
if (report.bypassAttemptCount > 0) {
|
|
35
|
+
lines.push(`Bypass attempts detected: ${report.bypassAttemptCount}`);
|
|
36
|
+
}
|
|
37
|
+
if (Object.keys(report.byVerdict).length > 0) {
|
|
38
|
+
lines.push('', 'By verdict:');
|
|
39
|
+
for (const [verdict, count] of Object.entries(report.byVerdict).sort((a, b) => b[1] - a[1])) {
|
|
40
|
+
lines.push(`- ${verdict}: ${count}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const v2Buckets = [
|
|
44
|
+
['location', report.byLocation],
|
|
45
|
+
['opacity', report.byOpacity],
|
|
46
|
+
['effect', report.byEffect],
|
|
47
|
+
['confidence', report.byConfidence],
|
|
48
|
+
];
|
|
49
|
+
for (const [axis, bucket] of v2Buckets) {
|
|
50
|
+
if (Object.keys(bucket).length > 0) {
|
|
51
|
+
lines.push('', `By ${axis}:`);
|
|
52
|
+
for (const [value, count] of Object.entries(bucket).sort((a, b) => b[1] - a[1])) {
|
|
53
|
+
lines.push(`- ${value}: ${count}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (Object.keys(report.gateEventsByDay).length > 0) {
|
|
58
|
+
lines.push('', 'Gate events by day:');
|
|
59
|
+
for (const [day, count] of Object.entries(report.gateEventsByDay).sort()) {
|
|
60
|
+
lines.push(`- ${day}: ${count}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (report.noisyRuleCandidates.length > 0) {
|
|
64
|
+
lines.push('', 'Noisy rule candidates:');
|
|
65
|
+
for (const rule of report.noisyRuleCandidates) {
|
|
66
|
+
lines.push(`- ${rule.reason}: ${(rule.approvalRate * 100).toFixed(0)}% approved after deny (${rule.approvedCount}/${rule.denyCount})`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (Object.keys(report.byReason).length > 0) {
|
|
70
|
+
lines.push('', 'By reason:');
|
|
71
|
+
for (const [reason, count] of Object.entries(report.byReason).sort((a, b) => b[1] - a[1])) {
|
|
72
|
+
lines.push(`- ${reason}: ${count}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (report.topWouldBlockSummaries.length > 0) {
|
|
76
|
+
lines.push('', 'Top would-block summaries:');
|
|
77
|
+
for (const entry of report.topWouldBlockSummaries) {
|
|
78
|
+
lines.push(`- [${entry.reason}] x${entry.count}: ${entry.summary}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (report.dogfood.notes.length > 0) {
|
|
82
|
+
lines.push('', 'Dogfood notes:');
|
|
83
|
+
for (const note of report.dogfood.notes) {
|
|
84
|
+
lines.push(`- ${note}`);
|
|
85
|
+
}
|
|
86
|
+
lines.push('', report.dogfood.readyForEnforce ? 'Ready for enforce: yes' : 'Ready for enforce: not yet');
|
|
87
|
+
}
|
|
88
|
+
return `${lines.join('\n')}\n`;
|
|
89
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { loadConfigFile } from '../config-io.js';
|
|
3
|
+
import { buildRecoverAdvice, RECOVER_DISCLAIMER, } from '../core/recover-advice.js';
|
|
4
|
+
import { probeGitState } from '../core/recover-git-probe.js';
|
|
5
|
+
import { selectRecoverTarget } from '../core/recover-select.js';
|
|
6
|
+
import { loadAuditRecords } from './audit.js';
|
|
7
|
+
import { classifyForReport } from './classify-for-report.js';
|
|
8
|
+
export async function recoverProject(options = {}) {
|
|
9
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
10
|
+
const config = await loadConfigFile(repoRoot);
|
|
11
|
+
let target = null;
|
|
12
|
+
const extraWarnings = [];
|
|
13
|
+
if (options.command) {
|
|
14
|
+
extraWarnings.push('--command re-runs shell classification and may invoke Tier1 judge (classification only — no recovery commands are executed). Prefer audit-based recovery when possible.');
|
|
15
|
+
const classified = await classifyForReport({
|
|
16
|
+
targetDir: repoRoot,
|
|
17
|
+
command: options.command,
|
|
18
|
+
kind: 'shell',
|
|
19
|
+
});
|
|
20
|
+
target = {
|
|
21
|
+
summary: classified.input,
|
|
22
|
+
reason: classified.result.reason,
|
|
23
|
+
effect: classified.result.v2?.effect,
|
|
24
|
+
location: classified.result.v2?.location,
|
|
25
|
+
permission: classified.permission,
|
|
26
|
+
assessment: classified.result.assessment,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const records = await loadAuditRecords(repoRoot);
|
|
31
|
+
target = selectRecoverTarget(records, options);
|
|
32
|
+
}
|
|
33
|
+
const git = await probeGitState(repoRoot);
|
|
34
|
+
if (!target) {
|
|
35
|
+
return {
|
|
36
|
+
repoRoot,
|
|
37
|
+
recoverable: false,
|
|
38
|
+
confidence: 'medium',
|
|
39
|
+
disclaimer: [...RECOVER_DISCLAIMER],
|
|
40
|
+
advice: ['No recoverable audit events found in the selected window.'],
|
|
41
|
+
warnings: ['Specify --fingerprint, --since, or --command to narrow recovery advice.'],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const advice = buildRecoverAdvice({
|
|
45
|
+
repoRoot,
|
|
46
|
+
target,
|
|
47
|
+
git,
|
|
48
|
+
minAssessmentConfidence: config.policy.confidenceThresholds.flag,
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
repoRoot,
|
|
52
|
+
target: {
|
|
53
|
+
timestamp: target.timestamp,
|
|
54
|
+
fingerprint: target.fingerprint,
|
|
55
|
+
summary: target.summary,
|
|
56
|
+
reason: target.reason,
|
|
57
|
+
effect: target.effect,
|
|
58
|
+
location: target.location,
|
|
59
|
+
permission: target.permission,
|
|
60
|
+
},
|
|
61
|
+
...advice,
|
|
62
|
+
warnings: [...extraWarnings, ...advice.warnings],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function formatRecoverReport(report) {
|
|
66
|
+
const lines = [
|
|
67
|
+
`belay recover for ${report.repoRoot}`,
|
|
68
|
+
'',
|
|
69
|
+
'Disclaimer:',
|
|
70
|
+
...report.disclaimer.map((line) => `- ${line}`),
|
|
71
|
+
'',
|
|
72
|
+
];
|
|
73
|
+
if (report.target) {
|
|
74
|
+
lines.push('Target:');
|
|
75
|
+
if (report.target.timestamp) {
|
|
76
|
+
lines.push(`- time: ${report.target.timestamp}`);
|
|
77
|
+
}
|
|
78
|
+
if (report.target.fingerprint) {
|
|
79
|
+
lines.push(`- fingerprint: ${report.target.fingerprint}`);
|
|
80
|
+
}
|
|
81
|
+
lines.push(`- reason: ${report.target.reason}`);
|
|
82
|
+
if (report.target.effect) {
|
|
83
|
+
lines.push(`- effect: ${report.target.effect}`);
|
|
84
|
+
}
|
|
85
|
+
if (report.target.location) {
|
|
86
|
+
lines.push(`- location: ${report.target.location}`);
|
|
87
|
+
}
|
|
88
|
+
lines.push(`- summary: ${report.target.summary}`);
|
|
89
|
+
lines.push('');
|
|
90
|
+
}
|
|
91
|
+
lines.push(`Recoverable: ${report.recoverable ? 'possibly' : 'unlikely'} (confidence=${report.confidence})`);
|
|
92
|
+
lines.push('');
|
|
93
|
+
lines.push('Advice:');
|
|
94
|
+
for (const line of report.advice) {
|
|
95
|
+
lines.push(`- ${line}`);
|
|
96
|
+
}
|
|
97
|
+
if (report.warnings.length > 0) {
|
|
98
|
+
lines.push('');
|
|
99
|
+
lines.push('Warnings:');
|
|
100
|
+
for (const warning of report.warnings) {
|
|
101
|
+
lines.push(`- ${warning}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return `${lines.join('\n')}\n`;
|
|
105
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { loadConfigFile } from '../config-io.js';
|
|
3
|
+
import { detectFenceDrift, formatAskBreakdown, summarizeAuditVisibility, } from '../core/audit-summary.js';
|
|
4
|
+
import { loadAuditRecords } from './audit.js';
|
|
5
|
+
export async function reportProject(options = {}) {
|
|
6
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
7
|
+
const config = await loadConfigFile(repoRoot);
|
|
8
|
+
const auditLogPath = path.join(repoRoot, config.audit.logPath);
|
|
9
|
+
const records = await loadAuditRecords(repoRoot);
|
|
10
|
+
const filter = {
|
|
11
|
+
since: options.since,
|
|
12
|
+
until: options.until,
|
|
13
|
+
};
|
|
14
|
+
const summary = summarizeAuditVisibility(records, filter, {
|
|
15
|
+
recentAskLimit: options.limit ?? 10,
|
|
16
|
+
});
|
|
17
|
+
const drift = detectFenceDrift(summary, {
|
|
18
|
+
threshold: config.policy.fenceWarnThreshold,
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
repoRoot,
|
|
22
|
+
auditLogPath,
|
|
23
|
+
...summary,
|
|
24
|
+
warnings: drift.warnings,
|
|
25
|
+
notes: drift.notes,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function formatReport(report) {
|
|
29
|
+
const lines = [
|
|
30
|
+
`belay report for ${report.repoRoot}`,
|
|
31
|
+
`Audit log: ${report.auditLogPath}`,
|
|
32
|
+
'',
|
|
33
|
+
`Gate events: ${report.gateEvents}`,
|
|
34
|
+
...formatAskBreakdown(report),
|
|
35
|
+
`Flag (allow_flagged): ${report.flagCount}`,
|
|
36
|
+
`Allow (silent pass): ${report.allowCount}`,
|
|
37
|
+
`Silent-pass rate: ${(report.silentPassRate * 100).toFixed(1)}%`,
|
|
38
|
+
'',
|
|
39
|
+
];
|
|
40
|
+
if (report.warnings.length > 0) {
|
|
41
|
+
lines.push('Warnings:');
|
|
42
|
+
for (const warning of report.warnings) {
|
|
43
|
+
lines.push(`- ${warning}`);
|
|
44
|
+
}
|
|
45
|
+
lines.push('');
|
|
46
|
+
}
|
|
47
|
+
if (report.notes.length > 0) {
|
|
48
|
+
lines.push('Notes:');
|
|
49
|
+
for (const note of report.notes) {
|
|
50
|
+
lines.push(`- ${note}`);
|
|
51
|
+
}
|
|
52
|
+
lines.push('');
|
|
53
|
+
}
|
|
54
|
+
if (report.recentAsks.length === 0) {
|
|
55
|
+
lines.push('No recent asks in the selected period.');
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
lines.push('Recent asks:');
|
|
59
|
+
for (const ask of report.recentAsks) {
|
|
60
|
+
const when = ask.timestamp ?? 'unknown-time';
|
|
61
|
+
lines.push(`- [${when}] (${ask.tier}) ${ask.reason} — ${ask.summary}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return `${lines.join('\n')}\n`;
|
|
65
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { loadApprovalState, loadConfigFile, saveApprovalState } from '../config-io.js';
|
|
3
|
+
import { compactApprovals } from '../core/approval.js';
|
|
4
|
+
export async function revokeApproval(options) {
|
|
5
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
6
|
+
const config = await loadConfigFile(repoRoot);
|
|
7
|
+
const pending = await loadApprovalState(repoRoot, 'pending-approvals.json', config);
|
|
8
|
+
const compacted = compactApprovals(pending);
|
|
9
|
+
const index = compacted.approvals.findIndex((approval) => approval.approvalId === options.approvalId);
|
|
10
|
+
if (index === -1) {
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
message: `Pending approval ${options.approvalId} not found or already expired.`,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
compacted.approvals.splice(index, 1);
|
|
17
|
+
await saveApprovalState(repoRoot, 'pending-approvals.json', compacted, config);
|
|
18
|
+
return {
|
|
19
|
+
ok: true,
|
|
20
|
+
message: `Revoked pending approval ${options.approvalId}.`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface SimulateOptions {
|
|
2
|
+
targetDir?: string;
|
|
3
|
+
configPath: string;
|
|
4
|
+
json?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function simulateProject(options: SimulateOptions): Promise<{
|
|
7
|
+
candidateConfigPath: string;
|
|
8
|
+
totalRecords: number;
|
|
9
|
+
changedCount: number;
|
|
10
|
+
allowToDenyCount: number;
|
|
11
|
+
denyToAllowCount: number;
|
|
12
|
+
diffs: import("../core/reclassify.js").ReclassifyDiff[];
|
|
13
|
+
}>;
|
|
14
|
+
export declare function formatSimulateReport(report: Awaited<ReturnType<typeof simulateProject>>): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { loadConfigFile } from '../config-io.js';
|
|
5
|
+
import { parseAuditNdjson, toAuditRecord } from '../core/audit-metrics.js';
|
|
6
|
+
import { mergeConfig } from '../core/config.js';
|
|
7
|
+
import { diffReclassification } from '../core/reclassify.js';
|
|
8
|
+
export async function simulateProject(options) {
|
|
9
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
10
|
+
const currentConfig = await loadConfigFile(repoRoot);
|
|
11
|
+
if (!existsSync(options.configPath)) {
|
|
12
|
+
throw new Error(`Candidate config not found: ${options.configPath}`);
|
|
13
|
+
}
|
|
14
|
+
const candidateRaw = JSON.parse(await readFile(options.configPath, 'utf8'));
|
|
15
|
+
const candidateConfig = mergeConfig(candidateRaw, currentConfig);
|
|
16
|
+
const auditLogPath = path.join(repoRoot, currentConfig.audit.logPath);
|
|
17
|
+
let raw = '';
|
|
18
|
+
try {
|
|
19
|
+
raw = await readFile(auditLogPath, 'utf8');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
raw = '';
|
|
23
|
+
}
|
|
24
|
+
const records = parseAuditNdjson(raw).map(toAuditRecord);
|
|
25
|
+
const diffs = (await Promise.all(records.map((record) => diffReclassification(record, candidateConfig, repoRoot)))).filter((diff) => diff !== null);
|
|
26
|
+
const allowToDeny = diffs.filter((diff) => (diff.previousVerdict === 'allow' || diff.previousVerdict === 'allow_flagged') &&
|
|
27
|
+
diff.nextVerdict === 'deny_pending_approval');
|
|
28
|
+
const denyToAllow = diffs.filter((diff) => diff.previousVerdict === 'deny_pending_approval' &&
|
|
29
|
+
(diff.nextVerdict === 'allow' || diff.nextVerdict === 'allow_flagged'));
|
|
30
|
+
return {
|
|
31
|
+
candidateConfigPath: options.configPath,
|
|
32
|
+
totalRecords: records.length,
|
|
33
|
+
changedCount: diffs.length,
|
|
34
|
+
allowToDenyCount: allowToDeny.length,
|
|
35
|
+
denyToAllowCount: denyToAllow.length,
|
|
36
|
+
diffs,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function formatSimulateReport(report) {
|
|
40
|
+
const lines = [
|
|
41
|
+
`simulate ${report.candidateConfigPath}`,
|
|
42
|
+
`Records scanned: ${report.totalRecords}`,
|
|
43
|
+
`Verdict changes: ${report.changedCount}`,
|
|
44
|
+
`allow/flagged → deny: ${report.allowToDenyCount}`,
|
|
45
|
+
`deny → allow/flagged: ${report.denyToAllowCount}`,
|
|
46
|
+
'',
|
|
47
|
+
];
|
|
48
|
+
for (const diff of report.diffs.slice(0, 30)) {
|
|
49
|
+
lines.push(`- ${diff.summary ?? diff.fingerprint}: ${diff.previousVerdict}/${diff.previousReason} → ${diff.nextVerdict}/${diff.nextReason}`);
|
|
50
|
+
}
|
|
51
|
+
if (report.diffs.length > 30) {
|
|
52
|
+
lines.push(`... ${report.diffs.length - 30} more`);
|
|
53
|
+
}
|
|
54
|
+
return `${lines.join('\n')}\n`;
|
|
55
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { pendingApprovalsPath } from '../config-io.js';
|
|
2
|
+
import type { StatusOptions, StatusReport } from '../types.js';
|
|
3
|
+
export declare function statusProject(options?: StatusOptions): Promise<StatusReport>;
|
|
4
|
+
export declare function formatStatusReport(report: StatusReport): string;
|
|
5
|
+
export { pendingApprovalsPath };
|