@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.
Files changed (266) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +268 -0
  3. package/agent-belay-logo.png +0 -0
  4. package/dist/adapters/claude/adapter.d.ts +7 -0
  5. package/dist/adapters/claude/adapter.js +114 -0
  6. package/dist/adapters/claude/hooks.d.ts +13 -0
  7. package/dist/adapters/claude/hooks.js +49 -0
  8. package/dist/adapters/claude/runtime-entry.d.ts +4 -0
  9. package/dist/adapters/claude/runtime-entry.js +260 -0
  10. package/dist/adapters/codex/adapter.d.ts +7 -0
  11. package/dist/adapters/codex/adapter.js +73 -0
  12. package/dist/adapters/codex/hooks.d.ts +21 -0
  13. package/dist/adapters/codex/hooks.js +78 -0
  14. package/dist/adapters/codex/runtime-entry.d.ts +4 -0
  15. package/dist/adapters/codex/runtime-entry.js +237 -0
  16. package/dist/adapters/cursor/adapter.d.ts +7 -0
  17. package/dist/adapters/cursor/adapter.js +29 -0
  18. package/dist/adapters/cursor/hooks.d.ts +2 -0
  19. package/dist/adapters/cursor/hooks.js +26 -0
  20. package/dist/adapters/cursor/runtime-entry.d.ts +4 -0
  21. package/dist/adapters/cursor/runtime-entry.js +143 -0
  22. package/dist/adapters/layouts/claude.d.ts +2 -0
  23. package/dist/adapters/layouts/claude.js +40 -0
  24. package/dist/adapters/layouts/codex.d.ts +2 -0
  25. package/dist/adapters/layouts/codex.js +43 -0
  26. package/dist/adapters/layouts/cursor.d.ts +2 -0
  27. package/dist/adapters/layouts/cursor.js +40 -0
  28. package/dist/adapters/layouts/index.d.ts +7 -0
  29. package/dist/adapters/layouts/index.js +23 -0
  30. package/dist/adapters/layouts/protected-paths.d.ts +3 -0
  31. package/dist/adapters/layouts/protected-paths.js +15 -0
  32. package/dist/adapters/layouts/scope.d.ts +19 -0
  33. package/dist/adapters/layouts/scope.js +86 -0
  34. package/dist/adapters/layouts/types.d.ts +14 -0
  35. package/dist/adapters/layouts/types.js +1 -0
  36. package/dist/adapters/registry.d.ts +4 -0
  37. package/dist/adapters/registry.js +14 -0
  38. package/dist/adapters/shared/gate-runtime.d.ts +51 -0
  39. package/dist/adapters/shared/gate-runtime.js +518 -0
  40. package/dist/adapters/shared/repo-root.d.ts +2 -0
  41. package/dist/adapters/shared/repo-root.js +17 -0
  42. package/dist/adapters/types.d.ts +19 -0
  43. package/dist/adapters/types.js +1 -0
  44. package/dist/branding.d.ts +3 -0
  45. package/dist/branding.js +3 -0
  46. package/dist/bundle/claude-runtime.mjs +5323 -0
  47. package/dist/bundle/codex-runtime.mjs +5310 -0
  48. package/dist/bundle/cursor-runtime.mjs +5208 -0
  49. package/dist/cleanup-orphans.d.ts +7 -0
  50. package/dist/cleanup-orphans.js +59 -0
  51. package/dist/cli.d.ts +2 -0
  52. package/dist/cli.js +631 -0
  53. package/dist/commands/approve.d.ts +14 -0
  54. package/dist/commands/approve.js +65 -0
  55. package/dist/commands/audit.d.ts +59 -0
  56. package/dist/commands/audit.js +132 -0
  57. package/dist/commands/classify-for-report.d.ts +9 -0
  58. package/dist/commands/classify-for-report.js +85 -0
  59. package/dist/commands/doctor.d.ts +3 -0
  60. package/dist/commands/doctor.js +366 -0
  61. package/dist/commands/dogfood.d.ts +5 -0
  62. package/dist/commands/dogfood.js +71 -0
  63. package/dist/commands/explain.d.ts +3 -0
  64. package/dist/commands/explain.js +133 -0
  65. package/dist/commands/health-snapshot.d.ts +2 -0
  66. package/dist/commands/health-snapshot.js +166 -0
  67. package/dist/commands/init-wizard.d.ts +16 -0
  68. package/dist/commands/init-wizard.js +50 -0
  69. package/dist/commands/metrics.d.ts +7 -0
  70. package/dist/commands/metrics.js +89 -0
  71. package/dist/commands/recover.d.ts +3 -0
  72. package/dist/commands/recover.js +105 -0
  73. package/dist/commands/report.d.ts +3 -0
  74. package/dist/commands/report.js +65 -0
  75. package/dist/commands/revoke.d.ts +5 -0
  76. package/dist/commands/revoke.js +22 -0
  77. package/dist/commands/simulate.d.ts +14 -0
  78. package/dist/commands/simulate.js +55 -0
  79. package/dist/commands/status.d.ts +5 -0
  80. package/dist/commands/status.js +107 -0
  81. package/dist/config-io.d.ts +23 -0
  82. package/dist/config-io.js +180 -0
  83. package/dist/conformance/guarantee-table.d.ts +14 -0
  84. package/dist/conformance/guarantee-table.js +95 -0
  85. package/dist/conformance/types.d.ts +6 -0
  86. package/dist/conformance/types.js +1 -0
  87. package/dist/core/approval-service.d.ts +26 -0
  88. package/dist/core/approval-service.js +41 -0
  89. package/dist/core/approval-token.d.ts +11 -0
  90. package/dist/core/approval-token.js +61 -0
  91. package/dist/core/approval.d.ts +19 -0
  92. package/dist/core/approval.js +58 -0
  93. package/dist/core/audit-analysis.d.ts +10 -0
  94. package/dist/core/audit-analysis.js +147 -0
  95. package/dist/core/audit-metrics.d.ts +51 -0
  96. package/dist/core/audit-metrics.js +155 -0
  97. package/dist/core/audit-query.d.ts +11 -0
  98. package/dist/core/audit-query.js +142 -0
  99. package/dist/core/audit-summary.d.ts +33 -0
  100. package/dist/core/audit-summary.js +111 -0
  101. package/dist/core/audit-types.d.ts +65 -0
  102. package/dist/core/audit-types.js +2 -0
  103. package/dist/core/capability/allowlist.d.ts +10 -0
  104. package/dist/core/capability/allowlist.js +53 -0
  105. package/dist/core/capability/broker.d.ts +17 -0
  106. package/dist/core/capability/broker.js +29 -0
  107. package/dist/core/capability/index.d.ts +5 -0
  108. package/dist/core/capability/index.js +4 -0
  109. package/dist/core/capability/paths.d.ts +1 -0
  110. package/dist/core/capability/paths.js +20 -0
  111. package/dist/core/capability/reasons.d.ts +2 -0
  112. package/dist/core/capability/reasons.js +4 -0
  113. package/dist/core/capability/types.d.ts +10 -0
  114. package/dist/core/capability/types.js +1 -0
  115. package/dist/core/capability-approval.d.ts +28 -0
  116. package/dist/core/capability-approval.js +43 -0
  117. package/dist/core/classify-subagent.d.ts +2 -0
  118. package/dist/core/classify-subagent.js +69 -0
  119. package/dist/core/classify-tool.d.ts +3 -0
  120. package/dist/core/classify-tool.js +311 -0
  121. package/dist/core/config-layers.d.ts +23 -0
  122. package/dist/core/config-layers.js +59 -0
  123. package/dist/core/config.d.ts +219 -0
  124. package/dist/core/config.js +720 -0
  125. package/dist/core/control-plane-isolation.d.ts +10 -0
  126. package/dist/core/control-plane-isolation.js +83 -0
  127. package/dist/core/custom-command-match.d.ts +2 -0
  128. package/dist/core/custom-command-match.js +8 -0
  129. package/dist/core/egress/allowlist.d.ts +7 -0
  130. package/dist/core/egress/allowlist.js +33 -0
  131. package/dist/core/egress/env.d.ts +3 -0
  132. package/dist/core/egress/env.js +17 -0
  133. package/dist/core/egress/fingerprint.d.ts +3 -0
  134. package/dist/core/egress/fingerprint.js +35 -0
  135. package/dist/core/egress/policy.d.ts +8 -0
  136. package/dist/core/egress/policy.js +47 -0
  137. package/dist/core/egress/proxy-server.d.ts +21 -0
  138. package/dist/core/egress/proxy-server.js +263 -0
  139. package/dist/core/egress/types.d.ts +25 -0
  140. package/dist/core/egress/types.js +1 -0
  141. package/dist/core/egress-approval.d.ts +48 -0
  142. package/dist/core/egress-approval.js +129 -0
  143. package/dist/core/fingerprint.d.ts +6 -0
  144. package/dist/core/fingerprint.js +24 -0
  145. package/dist/core/gate-contract.d.ts +48 -0
  146. package/dist/core/gate-contract.js +50 -0
  147. package/dist/core/gate-engine.d.ts +20 -0
  148. package/dist/core/gate-engine.js +172 -0
  149. package/dist/core/glob.d.ts +1 -0
  150. package/dist/core/glob.js +39 -0
  151. package/dist/core/index.d.ts +19 -0
  152. package/dist/core/index.js +15 -0
  153. package/dist/core/integrity.d.ts +15 -0
  154. package/dist/core/integrity.js +68 -0
  155. package/dist/core/judge-api-key.d.ts +4 -0
  156. package/dist/core/judge-api-key.js +11 -0
  157. package/dist/core/judge-config.d.ts +29 -0
  158. package/dist/core/judge-config.js +85 -0
  159. package/dist/core/judge-doctor.d.ts +7 -0
  160. package/dist/core/judge-doctor.js +124 -0
  161. package/dist/core/judgment.d.ts +6 -0
  162. package/dist/core/judgment.js +38 -0
  163. package/dist/core/notify.d.ts +13 -0
  164. package/dist/core/notify.js +44 -0
  165. package/dist/core/path-utils.d.ts +12 -0
  166. package/dist/core/path-utils.js +107 -0
  167. package/dist/core/reclassify.d.ts +15 -0
  168. package/dist/core/reclassify.js +82 -0
  169. package/dist/core/recover-advice.d.ts +30 -0
  170. package/dist/core/recover-advice.js +177 -0
  171. package/dist/core/recover-git-probe.d.ts +8 -0
  172. package/dist/core/recover-git-probe.js +50 -0
  173. package/dist/core/recover-select.d.ts +10 -0
  174. package/dist/core/recover-select.js +60 -0
  175. package/dist/core/scrub.d.ts +3 -0
  176. package/dist/core/scrub.js +87 -0
  177. package/dist/core/shell-substitution.d.ts +6 -0
  178. package/dist/core/shell-substitution.js +130 -0
  179. package/dist/core/shell-tokenizer.d.ts +5 -0
  180. package/dist/core/shell-tokenizer.js +129 -0
  181. package/dist/core/shell-unparseable.d.ts +4 -0
  182. package/dist/core/shell-unparseable.js +96 -0
  183. package/dist/core/transactional/diff-evaluator.d.ts +2 -0
  184. package/dist/core/transactional/diff-evaluator.js +84 -0
  185. package/dist/core/transactional/eligibility.d.ts +4 -0
  186. package/dist/core/transactional/eligibility.js +44 -0
  187. package/dist/core/transactional/git-worktree.d.ts +13 -0
  188. package/dist/core/transactional/git-worktree.js +189 -0
  189. package/dist/core/transactional/index.d.ts +5 -0
  190. package/dist/core/transactional/index.js +4 -0
  191. package/dist/core/transactional/reasons.d.ts +4 -0
  192. package/dist/core/transactional/reasons.js +8 -0
  193. package/dist/core/transactional/runner.d.ts +2 -0
  194. package/dist/core/transactional/runner.js +113 -0
  195. package/dist/core/transactional/types.d.ts +46 -0
  196. package/dist/core/transactional/types.js +1 -0
  197. package/dist/core/types.d.ts +90 -0
  198. package/dist/core/types.js +1 -0
  199. package/dist/core/v2/adapter.d.ts +14 -0
  200. package/dist/core/v2/adapter.js +118 -0
  201. package/dist/core/v2/containment.d.ts +19 -0
  202. package/dist/core/v2/containment.js +91 -0
  203. package/dist/core/v2/egress-classify.d.ts +7 -0
  204. package/dist/core/v2/egress-classify.js +216 -0
  205. package/dist/core/v2/fingerprint.d.ts +1 -0
  206. package/dist/core/v2/fingerprint.js +4 -0
  207. package/dist/core/v2/index.d.ts +12 -0
  208. package/dist/core/v2/index.js +10 -0
  209. package/dist/core/v2/judge-audit.d.ts +2 -0
  210. package/dist/core/v2/judge-audit.js +15 -0
  211. package/dist/core/v2/judge-factory.d.ts +25 -0
  212. package/dist/core/v2/judge-factory.js +75 -0
  213. package/dist/core/v2/judge-outbound.d.ts +12 -0
  214. package/dist/core/v2/judge-outbound.js +73 -0
  215. package/dist/core/v2/judge.d.ts +47 -0
  216. package/dist/core/v2/judge.js +264 -0
  217. package/dist/core/v2/launcher-resolve.d.ts +12 -0
  218. package/dist/core/v2/launcher-resolve.js +190 -0
  219. package/dist/core/v2/overrides.d.ts +7 -0
  220. package/dist/core/v2/overrides.js +37 -0
  221. package/dist/core/v2/parser.d.ts +21 -0
  222. package/dist/core/v2/parser.js +213 -0
  223. package/dist/core/v2/types.d.ts +67 -0
  224. package/dist/core/v2/types.js +1 -0
  225. package/dist/core/v2/verdict.d.ts +2 -0
  226. package/dist/core/v2/verdict.js +699 -0
  227. package/dist/corpus/evaluate.d.ts +24 -0
  228. package/dist/corpus/evaluate.js +69 -0
  229. package/dist/defaults.d.ts +18 -0
  230. package/dist/defaults.js +155 -0
  231. package/dist/egress-daemon.d.ts +1 -0
  232. package/dist/egress-daemon.js +52 -0
  233. package/dist/index.d.ts +17 -0
  234. package/dist/index.js +15 -0
  235. package/dist/installer/bootstrap.d.ts +5 -0
  236. package/dist/installer/bootstrap.js +61 -0
  237. package/dist/installer/runtime-artifacts.d.ts +3 -0
  238. package/dist/installer/runtime-artifacts.js +23 -0
  239. package/dist/installer/scope-config.d.ts +8 -0
  240. package/dist/installer/scope-config.js +25 -0
  241. package/dist/installer.d.ts +22 -0
  242. package/dist/installer.js +169 -0
  243. package/dist/node-resolution.d.ts +8 -0
  244. package/dist/node-resolution.js +237 -0
  245. package/dist/operational-insights.d.ts +19 -0
  246. package/dist/operational-insights.js +24 -0
  247. package/dist/presets.d.ts +4 -0
  248. package/dist/presets.js +95 -0
  249. package/dist/services/egress-service.d.ts +57 -0
  250. package/dist/services/egress-service.js +334 -0
  251. package/dist/services/sandbox-service.d.ts +38 -0
  252. package/dist/services/sandbox-service.js +95 -0
  253. package/dist/templates.d.ts +7 -0
  254. package/dist/templates.js +56 -0
  255. package/dist/types.d.ts +230 -0
  256. package/dist/types.js +1 -0
  257. package/dist/version.d.ts +1 -0
  258. package/dist/version.js +1 -0
  259. package/package.json +65 -0
  260. package/skills/belay/SKILL.md +52 -0
  261. package/skills/belay/belay-approve.md +7 -0
  262. package/skills/belay/belay-explain.md +11 -0
  263. package/skills/belay/belay-recover.md +13 -0
  264. package/skills/belay/belay-report.md +7 -0
  265. package/skills/belay/belay-status.md +9 -0
  266. package/skills/belay/belay-why.md +11 -0
@@ -0,0 +1,366 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { getClaudeManagedHookEntries } from '../adapters/claude/hooks.js';
5
+ import { getCodexManagedHookEntries } from '../adapters/codex/hooks.js';
6
+ import { getAdapterLayout } from '../adapters/layouts/index.js';
7
+ import { resolveScopedPaths } from '../adapters/layouts/scope.js';
8
+ import { cleanupOrphanApprovalState } from '../cleanup-orphans.js';
9
+ import { approvedApprovalsPath, belayStateDir, detectAdapterName, loadLayeredConfig, pendingApprovalsPath, repoLocalStateDirFor, } from '../config-io.js';
10
+ import { detectFenceDrift, summarizeAuditVisibility } from '../core/audit-summary.js';
11
+ import { defaultControlPlaneDir } from '../core/config.js';
12
+ import { verifyIntegrityManifest } from '../core/integrity.js';
13
+ import { diagnoseJudge } from '../core/judge-doctor.js';
14
+ import { getManagedHookEntries } from '../defaults.js';
15
+ import { resolveNodeBinary } from '../node-resolution.js';
16
+ import { egressStatus } from '../services/egress-service.js';
17
+ import { sandboxStatus } from '../services/sandbox-service.js';
18
+ import { PACKAGE_VERSION } from '../version.js';
19
+ import { loadAuditRecords } from './audit.js';
20
+ import { collectHealthSnapshot } from './health-snapshot.js';
21
+ import { metricsProject } from './metrics.js';
22
+ async function readRuntimeVersion(corePath) {
23
+ try {
24
+ const content = await readFile(corePath, 'utf8');
25
+ const stampMatch = content.match(/RUNTIME_BUILD_STAMP\s*=\s*"([^"]+)"/);
26
+ const versionMatch = content.match(/RUNTIME_PACKAGE_VERSION\s*=\s*"([^"]+)"/);
27
+ return {
28
+ stamp: stampMatch?.[1],
29
+ version: versionMatch?.[1],
30
+ };
31
+ }
32
+ catch {
33
+ return {};
34
+ }
35
+ }
36
+ function resolveDoctorAdapter(options, configAdapter) {
37
+ if (options.adapter) {
38
+ return options.adapter;
39
+ }
40
+ if (configAdapter === 'claude' || configAdapter === 'codex') {
41
+ return configAdapter;
42
+ }
43
+ return 'cursor';
44
+ }
45
+ export async function doctorProject(options = {}) {
46
+ const repoRoot = path.resolve(options.targetDir ?? process.cwd());
47
+ const issues = [];
48
+ const notes = [];
49
+ const warnings = [];
50
+ let loadedConfig = null;
51
+ let configProvenance = [];
52
+ let adapterName = options.adapter ?? detectAdapterName(repoRoot);
53
+ let activeLayout = getAdapterLayout(adapterName);
54
+ let configPath = activeLayout.configPath(repoRoot);
55
+ let hooksPath = activeLayout.hooksSettingsPath(repoRoot);
56
+ let corePath = path.join(activeLayout.runtimeDir(repoRoot), 'core.mjs');
57
+ if (!existsSync(configPath)) {
58
+ issues.push(`Missing config: ${configPath}`);
59
+ }
60
+ else {
61
+ try {
62
+ const rawConfig = JSON.parse(await readFile(configPath, 'utf8'));
63
+ adapterName = resolveDoctorAdapter(options, rawConfig.adapter);
64
+ activeLayout = getAdapterLayout(adapterName);
65
+ configPath = activeLayout.configPath(repoRoot);
66
+ hooksPath = activeLayout.hooksSettingsPath(repoRoot);
67
+ corePath = path.join(activeLayout.runtimeDir(repoRoot), 'core.mjs');
68
+ if (rawConfig.version === undefined) {
69
+ warnings.push('Config is missing "version". Set "version": 3 explicitly to avoid ambiguous migration.');
70
+ }
71
+ const layered = await loadLayeredConfig(repoRoot, adapterName);
72
+ loadedConfig = layered.config;
73
+ configProvenance = layered.provenance;
74
+ for (const entry of layered.provenance) {
75
+ notes.push(`Config layer [${entry.source}]: ${entry.path}`);
76
+ }
77
+ if (loadedConfig.version !== 4) {
78
+ warnings.push(`Config version is ${loadedConfig.version}; expected 4. Run belay upgrade to migrate.`);
79
+ }
80
+ const judgeDoctor = await diagnoseJudge(loadedConfig);
81
+ issues.push(...judgeDoctor.issues);
82
+ warnings.push(...judgeDoctor.warnings);
83
+ notes.push(...judgeDoctor.notes);
84
+ notes.push(`Adapter: ${adapterName}`);
85
+ const installScope = loadedConfig.installScope === 'global' ? 'global' : 'project';
86
+ const scopedPaths = resolveScopedPaths(activeLayout, installScope, repoRoot);
87
+ hooksPath = scopedPaths.hooksSettingsPath;
88
+ corePath = path.join(scopedPaths.runtimeDir, 'core.mjs');
89
+ notes.push(installScope === 'global'
90
+ ? `Install scope: global (hooks/runtime at ${scopedPaths.hooksDir})`
91
+ : 'Install scope: project');
92
+ notes.push(`Config mode: ${loadedConfig.mode}`);
93
+ notes.push('Verdict engine: v2 (location × opacity × effect × confidence). Shell gates use the v2 classifier; audit records include schemaVersion 2 axes when available.');
94
+ const repoLocalDir = repoLocalStateDirFor(repoRoot, loadedConfig);
95
+ if (loadedConfig.controlPlane.enabled) {
96
+ notes.push(`Control plane: ${belayStateDir(loadedConfig, repoLocalDir)}`);
97
+ const repoLocalPending = path.join(repoLocalDir, 'pending-approvals.json');
98
+ const repoLocalApproved = path.join(repoLocalDir, 'approved-approvals.json');
99
+ if (existsSync(repoLocalPending) || existsSync(repoLocalApproved)) {
100
+ warnings.push('Repo-local approval files remain while control plane is enabled. Run belay doctor --fix to archive them.');
101
+ }
102
+ }
103
+ else {
104
+ const controlPlaneDirs = new Set([defaultControlPlaneDir()]);
105
+ if (loadedConfig.controlPlane.configDir) {
106
+ controlPlaneDirs.add(loadedConfig.controlPlane.configDir);
107
+ }
108
+ for (const controlPlaneDir of controlPlaneDirs) {
109
+ const hasApprovalFiles = existsSync(path.join(controlPlaneDir, 'pending-approvals.json')) ||
110
+ existsSync(path.join(controlPlaneDir, 'approved-approvals.json'));
111
+ if (hasApprovalFiles) {
112
+ warnings.push(`Control plane is disabled but approval files still exist at ${controlPlaneDir}. Run belay doctor --fix to migrate and archive them.`);
113
+ }
114
+ }
115
+ }
116
+ if (loadedConfig.controlPlane.integrity === 'hash-pinned') {
117
+ notes.push('Integrity: hash-pinned (verify with belay upgrade after runtime changes).');
118
+ const integrity = await verifyIntegrityManifest(repoRoot, activeLayout);
119
+ if (!integrity.ok) {
120
+ issues.push(`Integrity verification failed: ${integrity.mismatches.slice(0, 3).join(', ')}`);
121
+ }
122
+ }
123
+ }
124
+ catch (error) {
125
+ issues.push(error instanceof Error ? error.message : 'Failed to parse belay.config.json');
126
+ }
127
+ }
128
+ const installScope = loadedConfig?.installScope === 'global' ? 'global' : 'project';
129
+ const scopedPaths = resolveScopedPaths(activeLayout, installScope, repoRoot);
130
+ hooksPath = scopedPaths.hooksSettingsPath;
131
+ corePath = path.join(scopedPaths.runtimeDir, 'core.mjs');
132
+ const hooksDir = scopedPaths.hooksDir;
133
+ const repoLocalDir = loadedConfig
134
+ ? repoLocalStateDirFor(repoRoot, loadedConfig)
135
+ : activeLayout.repoLocalStateDir(repoRoot);
136
+ const requiredPaths = [
137
+ path.join(hooksDir, 'belay-runner'),
138
+ path.join(hooksDir, 'belay-runner.cmd'),
139
+ path.join(hooksDir, 'belay-before-submit.mjs'),
140
+ path.join(hooksDir, 'belay-shell-gate.mjs'),
141
+ path.join(hooksDir, 'belay-tool-gate.mjs'),
142
+ path.join(hooksDir, 'belay-audit.mjs'),
143
+ corePath,
144
+ loadedConfig
145
+ ? pendingApprovalsPath(repoRoot, loadedConfig)
146
+ : path.join(repoLocalDir, 'pending-approvals.json'),
147
+ loadedConfig
148
+ ? approvedApprovalsPath(repoRoot, loadedConfig)
149
+ : path.join(repoLocalDir, 'approved-approvals.json'),
150
+ path.join(repoRoot, loadedConfig?.audit.logPath ?? activeLayout.defaultAuditLogPath(repoRoot)),
151
+ ];
152
+ for (const requiredPath of requiredPaths) {
153
+ if (!existsSync(requiredPath)) {
154
+ issues.push(`Missing generated file: ${requiredPath}`);
155
+ }
156
+ }
157
+ let hooksOk = true;
158
+ try {
159
+ const managedEntries = adapterName === 'cursor'
160
+ ? getManagedHookEntries(process.platform, hooksDir, repoRoot)
161
+ : adapterName === 'claude'
162
+ ? getClaudeManagedHookEntries(process.platform, hooksDir, repoRoot)
163
+ : getCodexManagedHookEntries(process.platform, hooksDir, repoRoot);
164
+ if (adapterName === 'cursor') {
165
+ const { loadHooksFile } = await import('../installer.js');
166
+ const hooksFile = await loadHooksFile(hooksPath);
167
+ for (const { event, definition } of managedEntries) {
168
+ const entries = hooksFile.hooks[event] ?? [];
169
+ const present = entries.some((entry) => entry.command === definition.command && entry.matcher === definition.matcher);
170
+ if (!present) {
171
+ hooksOk = false;
172
+ const matcherSuffix = definition.matcher ? ` (matcher: ${definition.matcher})` : '';
173
+ issues.push(`Missing managed hook for ${event}: ${definition.command}${matcherSuffix}`);
174
+ }
175
+ }
176
+ }
177
+ else if (adapterName === 'codex') {
178
+ // Codex hooks live in TOML (.codex/config.toml). Verify belay's managed command strings
179
+ // are present in the rendered TOML block.
180
+ const toml = await readFile(hooksPath, 'utf8');
181
+ for (const { event, definition } of managedEntries) {
182
+ if (!toml.includes(definition.command)) {
183
+ hooksOk = false;
184
+ issues.push(`Missing Codex managed hook for ${event}: ${definition.command}`);
185
+ }
186
+ }
187
+ // Codex adapter: shell gating VERIFIED end-to-end on Codex TUI (PreToolUse deny honored;
188
+ // Codex TUI smoke / G-B2). Surface only the residual caveats so users know the boundary.
189
+ warnings.push('Codex adapter: shell gating verified on Codex TUI (PreToolUse deny honored). Residual ' +
190
+ 'caveats — only the shell (Bash) tool is confirmed; non-shell tool names (apply_patch ' +
191
+ 'etc.) are best-guess mappings; unmapped tools ask with pending approval (R39); managed ' +
192
+ '(pre-trusted) deployment is not yet available; non-managed hooks require /hooks trust. ' +
193
+ 'See docs/adapter-sdk.md and docs/gates/G-B1-cursor-skill-ux.md.');
194
+ }
195
+ else {
196
+ const settings = JSON.parse(await readFile(hooksPath, 'utf8'));
197
+ for (const { event, definition } of managedEntries) {
198
+ const eventHooks = settings.hooks?.[event] ?? [];
199
+ const present = eventHooks.some((entry) => entry.matcher === definition.matcher &&
200
+ entry.hooks?.some((hook) => hook.command === definition.command));
201
+ if (!present) {
202
+ hooksOk = false;
203
+ issues.push(`Missing Claude managed hook for ${event}: ${definition.command}`);
204
+ }
205
+ }
206
+ }
207
+ }
208
+ catch (error) {
209
+ hooksOk = false;
210
+ issues.push(error instanceof Error ? error.message : 'Failed to parse hook settings');
211
+ }
212
+ const nodeResolution = resolveNodeBinary();
213
+ if (!nodeResolution.ok) {
214
+ issues.push(nodeResolution.detail);
215
+ }
216
+ else {
217
+ notes.push(`Node resolved at ${nodeResolution.path}`);
218
+ }
219
+ if (existsSync(corePath)) {
220
+ const runtimeVersions = await readRuntimeVersion(corePath);
221
+ if (runtimeVersions.stamp && !runtimeVersions.stamp.startsWith(`${PACKAGE_VERSION}@`)) {
222
+ warnings.push(`Installed runtime stamp (${runtimeVersions.stamp}) differs from package (${PACKAGE_VERSION}). Run belay upgrade.`);
223
+ }
224
+ if (runtimeVersions.version && runtimeVersions.version !== PACKAGE_VERSION) {
225
+ warnings.push(`Installed runtime version (${runtimeVersions.version}) differs from package (${PACKAGE_VERSION}). Run belay upgrade.`);
226
+ }
227
+ if (runtimeVersions.stamp?.startsWith(`${PACKAGE_VERSION}@`)) {
228
+ notes.push(`Runtime version matches package (${PACKAGE_VERSION}).`);
229
+ }
230
+ }
231
+ if (options.fix && loadedConfig) {
232
+ const cleanup = await cleanupOrphanApprovalState(repoRoot, loadedConfig, {
233
+ dryRun: options.dryRun === true,
234
+ });
235
+ if (cleanup.actions.length > 0) {
236
+ notes.push(...cleanup.actions);
237
+ }
238
+ else {
239
+ notes.push('No orphan approval cleanup actions were needed.');
240
+ }
241
+ }
242
+ let dogfood = null;
243
+ if (loadedConfig) {
244
+ const auditRecords = await loadAuditRecords(repoRoot);
245
+ const auditVisibility = summarizeAuditVisibility(auditRecords);
246
+ const drift = detectFenceDrift(auditVisibility, {
247
+ threshold: loadedConfig.policy.fenceWarnThreshold,
248
+ });
249
+ warnings.push(...drift.warnings);
250
+ notes.push(...drift.notes);
251
+ const metrics = await metricsProject({ targetDir: repoRoot });
252
+ dogfood = {
253
+ active: loadedConfig.mode === 'audit' && loadedConfig.policy.unknownLocalEffect === 'deny',
254
+ mode: loadedConfig.mode,
255
+ unknownLocalEffect: loadedConfig.policy.unknownLocalEffect,
256
+ readyForEnforce: metrics.dogfood.readyForEnforce,
257
+ gateEvents: metrics.gateEvents,
258
+ wouldBlockCount: metrics.wouldBlockCount,
259
+ wouldBlockRate: metrics.wouldBlockRate,
260
+ notes: metrics.dogfood.notes,
261
+ };
262
+ if (dogfood.active) {
263
+ notes.push(`Dogfood active: ${dogfood.gateEvents} gate events, ${dogfood.wouldBlockCount} would-block (${(dogfood.wouldBlockRate * 100).toFixed(1)}%).`);
264
+ if (dogfood.readyForEnforce) {
265
+ notes.push('Dogfood metrics suggest enforce mode is ready (belay dogfood --enforce).');
266
+ }
267
+ }
268
+ else if (dogfood.unknownLocalEffect === 'deny' && dogfood.mode !== 'audit') {
269
+ notes.push('Fail-closed policy is enabled in enforce mode.');
270
+ }
271
+ if (loadedConfig.policy.transactional.enabled) {
272
+ notes.push('Transactional execution: enabled — low-confidence shell mutations run in an isolated git worktree; observed-safe effects are applied once and the hook denies re-execution.');
273
+ if (!existsSync(path.join(repoRoot, '.git'))) {
274
+ warnings.push('Transactional execution is enabled but this directory is not a git repository. Transactional worktrees will be skipped until git is available.');
275
+ }
276
+ }
277
+ if (loadedConfig.sandbox.enabled) {
278
+ const sandbox = await sandboxStatus({ targetDir: repoRoot });
279
+ notes.push(`Sandbox capability broker: enabled (runtime=${loadedConfig.sandbox.runtime}, fs-scope entries=${sandbox.fsScopeAllowlistCount}, full-isolation=${sandbox.l1FullActive}).`);
280
+ if (loadedConfig.sandbox.runtime === 'none') {
281
+ warnings.push('sandbox.enabled is true but sandbox.runtime is none.');
282
+ }
283
+ for (const issue of sandbox.issues) {
284
+ warnings.push(issue);
285
+ }
286
+ }
287
+ if (loadedConfig.egress.enabled) {
288
+ const egress = await egressStatus({ targetDir: repoRoot });
289
+ notes.push(`Egress proxy: enabled — read/mutate action class enforced at proxy layer (listen ${egress.host}:${egress.port}; demoteL3External config is legacy and not applied to shell classifier).`);
290
+ if (!egress.running) {
291
+ warnings.push('Egress is enabled in config but the local proxy is not running. Run belay egress start.');
292
+ }
293
+ else {
294
+ notes.push(`Egress proxy running (pid ${egress.pid}).`);
295
+ if (egress.foreignProxy) {
296
+ warnings.push(`Egress listen port ${egress.host}:${egress.port} is occupied by another proxy${egress.boundRepoRoot ? ` for ${egress.boundRepoRoot}` : ''}. Do not use belay egress env for this repository.`);
297
+ }
298
+ else if (egress.repoRootMismatch) {
299
+ warnings.push(`Egress proxy is bound to ${egress.boundRepoRoot} but this repo is ${repoRoot}. Stop and restart egress for this repository.`);
300
+ }
301
+ }
302
+ }
303
+ }
304
+ const health = await collectHealthSnapshot({ targetDir: repoRoot, adapter: adapterName });
305
+ if (health.containmentPosture !== 'l1-full') {
306
+ warnings.push(`Containment posture is ${health.containmentPosture}: ${health.containmentWarnings.join('; ')}`);
307
+ }
308
+ for (const signal of health.additionalRiskSignals) {
309
+ warnings.push(`Additional risk signal: ${signal}`);
310
+ }
311
+ if (health.skillOnly) {
312
+ warnings.push('Skill-only install detected: belay SKILL.md is present but hook floor is missing or incomplete. ' +
313
+ 'This is advisory only — enforcement requires hooks. Run `npx @guilz-dev/belay init` (or `belay init-wizard`) ' +
314
+ 'then `belay doctor` to verify the floor.');
315
+ notes.push(`Skill path: ${health.skillPath}`);
316
+ }
317
+ if (health.skillInstalled && !health.commandsInstalled && adapterName === 'cursor') {
318
+ notes.push('Optional: install Cursor slash commands with `belay init --with-skill` for /belay-approve routing.');
319
+ }
320
+ const report = {
321
+ ok: issues.length === 0 && hooksOk,
322
+ repoRoot,
323
+ configPath,
324
+ hooksPath,
325
+ nodeResolution,
326
+ issues,
327
+ notes,
328
+ warnings,
329
+ configProvenance,
330
+ dogfood,
331
+ };
332
+ return report;
333
+ }
334
+ export function formatDoctorReport(report) {
335
+ const lines = [
336
+ `belay doctor for ${report.repoRoot}`,
337
+ `Config: ${report.configPath}`,
338
+ `Hooks: ${report.hooksPath}`,
339
+ `Node: ${report.nodeResolution.ok ? report.nodeResolution.path : 'unresolved'}`,
340
+ ];
341
+ if (report.notes.length > 0) {
342
+ lines.push('', 'Notes:');
343
+ for (const note of report.notes) {
344
+ lines.push(`- ${note}`);
345
+ }
346
+ }
347
+ if (report.warnings.length > 0) {
348
+ lines.push('', 'Warnings:');
349
+ for (const warning of report.warnings) {
350
+ lines.push(`- ${warning}`);
351
+ }
352
+ }
353
+ if (report.dogfood) {
354
+ lines.push('', `Dogfood: ${report.dogfood.active ? 'active' : 'inactive'} | enforce ready: ${report.dogfood.readyForEnforce ? 'yes' : 'no'}`);
355
+ }
356
+ if (report.issues.length > 0) {
357
+ lines.push('', 'Issues:');
358
+ for (const issue of report.issues) {
359
+ lines.push(`- ${issue}`);
360
+ }
361
+ }
362
+ else {
363
+ lines.push('', 'No issues detected.');
364
+ }
365
+ return `${lines.join('\n')}\n`;
366
+ }
@@ -0,0 +1,5 @@
1
+ import { isDogfoodConfig, loadOperationalInsights } from '../operational-insights.js';
2
+ import type { DogfoodOptions, DogfoodResult } from '../types.js';
3
+ export declare function dogfoodProject(options?: DogfoodOptions): Promise<DogfoodResult>;
4
+ export declare function formatDogfoodResult(result: DogfoodResult): string;
5
+ export { isDogfoodConfig, loadOperationalInsights };
@@ -0,0 +1,71 @@
1
+ import path from 'node:path';
2
+ import { configPathFor, loadConfigFile, writeConfigFile } from '../config-io.js';
3
+ import { mergeConfig } from '../core/config.js';
4
+ import { isDogfoodConfig, loadOperationalInsights } from '../operational-insights.js';
5
+ import { metricsProject } from './metrics.js';
6
+ export async function dogfoodProject(options = {}) {
7
+ const repoRoot = path.resolve(options.targetDir ?? process.cwd());
8
+ const adapter = options.adapter ?? 'cursor';
9
+ const configPath = configPathFor(repoRoot, adapter);
10
+ if (options.enforce) {
11
+ return promoteDogfoodToEnforce(repoRoot, configPath, options.force === true, adapter);
12
+ }
13
+ const existing = await loadConfigFile(repoRoot, adapter);
14
+ const updated = mergeConfig({
15
+ ...existing,
16
+ mode: 'audit',
17
+ policy: {
18
+ ...existing.policy,
19
+ unknownLocalEffect: 'deny',
20
+ },
21
+ });
22
+ await writeConfigFile(repoRoot, updated, adapter);
23
+ return {
24
+ ok: true,
25
+ repoRoot,
26
+ configPath,
27
+ mode: updated.mode,
28
+ unknownLocalEffect: updated.policy.unknownLocalEffect,
29
+ message: [
30
+ 'Dogfood mode enabled: audit + policy.unknownLocalEffect deny.',
31
+ 'Run belay metrics after normal agent work, then belay dogfood --enforce when ready.',
32
+ ].join(' '),
33
+ };
34
+ }
35
+ async function promoteDogfoodToEnforce(repoRoot, configPath, force, adapter = 'cursor') {
36
+ const existing = await loadConfigFile(repoRoot, adapter);
37
+ const metrics = await metricsProject({ targetDir: repoRoot });
38
+ if (!force && !metrics.dogfood.readyForEnforce) {
39
+ return {
40
+ ok: false,
41
+ repoRoot,
42
+ configPath,
43
+ mode: existing.mode,
44
+ unknownLocalEffect: existing.policy.unknownLocalEffect,
45
+ message: [
46
+ 'Dogfood metrics do not recommend enforce yet.',
47
+ ...metrics.dogfood.notes,
48
+ 'Re-run belay metrics, tune overrides.allow, or pass --force to override.',
49
+ ].join(' '),
50
+ };
51
+ }
52
+ const updated = mergeConfig({
53
+ ...existing,
54
+ mode: 'enforce',
55
+ });
56
+ await writeConfigFile(repoRoot, updated, adapter);
57
+ return {
58
+ ok: true,
59
+ repoRoot,
60
+ configPath,
61
+ mode: updated.mode,
62
+ unknownLocalEffect: updated.policy.unknownLocalEffect,
63
+ message: force
64
+ ? 'Switched to enforce mode (forced). Fail-closed shell policy remains active via policy.unknownLocalEffect deny.'
65
+ : 'Switched to enforce mode. Fail-closed shell policy remains active via policy.unknownLocalEffect deny.',
66
+ };
67
+ }
68
+ export function formatDogfoodResult(result) {
69
+ return `${result.message}\n`;
70
+ }
71
+ export { isDogfoodConfig, loadOperationalInsights };
@@ -0,0 +1,3 @@
1
+ import type { ExplainOptions, ExplainReport } from '../types.js';
2
+ export declare function explainCommand(options: ExplainOptions): Promise<ExplainReport>;
3
+ export declare function formatExplainReport(report: ExplainReport): string;
@@ -0,0 +1,133 @@
1
+ import path from 'node:path';
2
+ import { loadApprovalState, loadConfigFile } from '../config-io.js';
3
+ import { compactApprovals } from '../core/approval.js';
4
+ import { classifyForReport } from './classify-for-report.js';
5
+ export async function explainCommand(options) {
6
+ const repoRoot = path.resolve(options.targetDir ?? process.cwd());
7
+ if (!options.command && !options.payload && options.explainLastPending !== false) {
8
+ const config = await loadConfigFile(repoRoot);
9
+ const pending = compactApprovals(await loadApprovalState(repoRoot, 'pending-approvals.json', config));
10
+ if (pending.approvals.length > 0) {
11
+ const latest = [...pending.approvals].sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))[0];
12
+ const inputKind = latest.inputKind ??
13
+ (latest.kind === 'egress' || latest.kind === 'capability' ? 'shell' : latest.kind);
14
+ const classified = await classifyForReport({
15
+ targetDir: repoRoot,
16
+ cwd: options.cwd,
17
+ kind: inputKind,
18
+ command: latest.input ?? latest.summary,
19
+ });
20
+ return {
21
+ repoRoot: classified.repoRoot,
22
+ kind: classified.kind,
23
+ command: classified.input,
24
+ cwd: classified.cwd,
25
+ policy: classified.policy,
26
+ overrides: classified.overrides,
27
+ egress: classified.egress,
28
+ egressProxyRunning: classified.egressProxyRunning,
29
+ egressL3DemotionActive: false,
30
+ sandbox: classified.sandbox,
31
+ sandboxBrokerActive: classified.sandboxBrokerActive,
32
+ l1FullActive: classified.l1FullActive,
33
+ transactionalEligible: classified.transactionalEligible,
34
+ permission: classified.permission,
35
+ tier: classified.tier,
36
+ approvalId: latest.approvalId,
37
+ result: classified.result,
38
+ };
39
+ }
40
+ }
41
+ if (!options.command && !options.payload) {
42
+ throw new Error('explain requires --command, --payload-json, or a pending approval to explain.');
43
+ }
44
+ const classified = await classifyForReport({
45
+ targetDir: repoRoot,
46
+ cwd: options.cwd,
47
+ kind: options.kind,
48
+ command: options.command,
49
+ toolName: options.toolName,
50
+ payload: options.payload,
51
+ });
52
+ return {
53
+ repoRoot: classified.repoRoot,
54
+ kind: classified.kind,
55
+ command: classified.input,
56
+ cwd: classified.cwd,
57
+ policy: classified.policy,
58
+ overrides: classified.overrides,
59
+ egress: classified.egress,
60
+ egressProxyRunning: classified.egressProxyRunning,
61
+ egressL3DemotionActive: false,
62
+ sandbox: classified.sandbox,
63
+ sandboxBrokerActive: classified.sandboxBrokerActive,
64
+ l1FullActive: classified.l1FullActive,
65
+ transactionalEligible: classified.transactionalEligible,
66
+ permission: classified.permission,
67
+ tier: classified.tier,
68
+ result: classified.result,
69
+ };
70
+ }
71
+ export function formatExplainReport(report) {
72
+ const { result } = report;
73
+ const judgeFields = result.v2
74
+ ? [
75
+ result.v2.judgeProvider ? ` judgeProvider: ${result.v2.judgeProvider}` : null,
76
+ result.v2.judgeModelResolved ? ` judgeModel: ${result.v2.judgeModelResolved}` : null,
77
+ result.v2.judgeLatencyMs !== undefined
78
+ ? ` judgeLatencyMs: ${result.v2.judgeLatencyMs}`
79
+ : null,
80
+ result.v2.judgeFallbackReason
81
+ ? ` judgeFallbackReason: ${result.v2.judgeFallbackReason}`
82
+ : null,
83
+ ].filter((line) => line !== null)
84
+ : [];
85
+ const lines = [
86
+ `belay explain for ${report.repoRoot}`,
87
+ ...(report.approvalId ? [`Pending approval: ${report.approvalId}`] : []),
88
+ `Kind: ${report.kind}`,
89
+ `Input: ${report.command}`,
90
+ `CWD: ${report.cwd}`,
91
+ `Permission: ${report.permission}`,
92
+ `Tier: ${report.tier}`,
93
+ `Policy unknownLocalEffect: ${report.policy.unknownLocalEffect}`,
94
+ `Egress (partial L1): ${report.egress.enabled ? 'enabled' : 'disabled'} (proxy running=${report.egressProxyRunning}; shell L3 demotion inactive — read/mutate enforced at proxy layer per R36)`,
95
+ report.egress.enabled
96
+ ? `Egress proxy: ${report.egress.listenHost}:${report.egress.listenPort}`
97
+ : 'Egress proxy: not configured',
98
+ `Sandbox (L1 broker): ${report.sandbox.enabled ? 'enabled' : 'disabled'} (runtime=${report.sandbox.runtime}, fs broker active=${report.sandboxBrokerActive}, L1-full=${report.l1FullActive})`,
99
+ `Transactional (L2): ${report.policy.transactional.enabled ? 'enabled' : 'disabled'} (eligible for this command=${report.transactionalEligible})`,
100
+ report.policy.transactional.enabled
101
+ ? `Transactional band: [${report.policy.transactional.minConfidence}, ${report.policy.transactional.maxConfidence})`
102
+ : 'Transactional band: not configured',
103
+ `Overrides allow: ${report.overrides.allow.join(', ') || '(none)'}`,
104
+ `Overrides external: ${report.overrides.external.join(', ') || '(none)'}`,
105
+ '',
106
+ `Verdict: ${result.verdict}`,
107
+ `Reason: ${result.reason}`,
108
+ `Fingerprint: ${result.fingerprint}`,
109
+ ...(result.v2
110
+ ? [
111
+ '',
112
+ 'v2 axes:',
113
+ ` location: ${result.v2.location}`,
114
+ ` opacity: ${result.v2.opacity}`,
115
+ ` effect: ${result.v2.effect}`,
116
+ ` confidence: ${result.v2.confidence}`,
117
+ ` would: ${result.v2.would}`,
118
+ ...(judgeFields.length > 0 ? ['judgeTrace:', ...judgeFields] : []),
119
+ ]
120
+ : []),
121
+ '',
122
+ 'Predicted assessment:',
123
+ ` reversibility: ${result.assessment.reversibility}`,
124
+ ` external: ${result.assessment.external}`,
125
+ ` blastRadius: ${result.assessment.blastRadius}`,
126
+ ` confidence: ${result.assessment.confidence}`,
127
+ ` signals: ${result.assessment.signals.join(', ') || '(none)'}`,
128
+ report.transactionalEligible
129
+ ? 'Observed assessment: measured in an isolated git worktree at gate time. Observed-safe commands are applied once and the hook denies re-execution (transactional_already_applied).'
130
+ : 'Observed assessment: not applicable (transactional path not eligible).',
131
+ ];
132
+ return `${lines.join('\n')}\n`;
133
+ }
@@ -0,0 +1,2 @@
1
+ import type { HealthSnapshot, HealthSnapshotOptions } from '../types.js';
2
+ export declare function collectHealthSnapshot(options?: HealthSnapshotOptions): Promise<HealthSnapshot>;