mergen-server 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (308) hide show
  1. package/LICENSE +57 -0
  2. package/README.md +96 -0
  3. package/dist/__stubs__/calibration.js +471 -0
  4. package/dist/__stubs__/causal.js +601 -0
  5. package/dist/__stubs__/closed-source.js +150 -0
  6. package/dist/__stubs__/license.js +35 -0
  7. package/dist/__stubs__/plans.js +197 -0
  8. package/dist/app.js +467 -0
  9. package/dist/cli.js +250 -0
  10. package/dist/commands/agent-identity.js +79 -0
  11. package/dist/commands/gate.js +107 -0
  12. package/dist/commands/github.js +632 -0
  13. package/dist/commands/incident.js +644 -0
  14. package/dist/commands/policy.js +710 -0
  15. package/dist/commands/scan.js +243 -0
  16. package/dist/commands/setup.js +1218 -0
  17. package/dist/commands/shared.js +109 -0
  18. package/dist/commands/team.js +809 -0
  19. package/dist/datadog/blame-attribution.js +228 -0
  20. package/dist/datadog/client.js +159 -0
  21. package/dist/datadog/compactor.js +169 -0
  22. package/dist/datadog/fingerprinter.js +34 -0
  23. package/dist/datadog/incident-state.js +21 -0
  24. package/dist/datadog/line-matcher.js +56 -0
  25. package/dist/datadog/memory-store.js +399 -0
  26. package/dist/datadog/otel-trace.js +67 -0
  27. package/dist/index.js +505 -0
  28. package/dist/intelligence/action-risk.js +132 -0
  29. package/dist/intelligence/activity-feed.js +40 -0
  30. package/dist/intelligence/agent-identity.js +115 -0
  31. package/dist/intelligence/agent-pipeline.js +329 -0
  32. package/dist/intelligence/agent-profiles.js +81 -0
  33. package/dist/intelligence/ai-commit.js +16 -0
  34. package/dist/intelligence/approval-analytics.js +42 -0
  35. package/dist/intelligence/approval-events.js +5 -0
  36. package/dist/intelligence/arch-boundaries.js +94 -0
  37. package/dist/intelligence/arch-graph.js +117 -0
  38. package/dist/intelligence/autonomy.js +397 -0
  39. package/dist/intelligence/azure-broker.js +57 -0
  40. package/dist/intelligence/azure-proxy-sessions.js +49 -0
  41. package/dist/intelligence/baseline.js +1 -0
  42. package/dist/intelligence/behavior-baseline.js +134 -0
  43. package/dist/intelligence/billing.js +1 -0
  44. package/dist/intelligence/blast-radius.js +224 -0
  45. package/dist/intelligence/bypass.js +157 -0
  46. package/dist/intelligence/calibration-classifier.js +77 -0
  47. package/dist/intelligence/calibration-git-sync.js +74 -0
  48. package/dist/intelligence/calibration.js +1 -0
  49. package/dist/intelligence/cascade-detector.js +58 -0
  50. package/dist/intelligence/case-study-generator.js +126 -0
  51. package/dist/intelligence/causal-graph.js +126 -0
  52. package/dist/intelligence/causal.js +1 -0
  53. package/dist/intelligence/change-risk.js +102 -0
  54. package/dist/intelligence/compliance-report.js +134 -0
  55. package/dist/intelligence/confidence-report.js +85 -0
  56. package/dist/intelligence/corpus-to-policy.js +140 -0
  57. package/dist/intelligence/credential-broker.js +112 -0
  58. package/dist/intelligence/credential-misuse.js +39 -0
  59. package/dist/intelligence/debug-sessions.js +108 -0
  60. package/dist/intelligence/decision-escalation.js +36 -0
  61. package/dist/intelligence/decision-memory.js +47 -0
  62. package/dist/intelligence/decision-similarity.js +85 -0
  63. package/dist/intelligence/default-safety-tests.js +53 -0
  64. package/dist/intelligence/degradation-watcher.js +108 -0
  65. package/dist/intelligence/detector-plugins.js +70 -0
  66. package/dist/intelligence/detectors.js +1 -0
  67. package/dist/intelligence/device-auth.js +138 -0
  68. package/dist/intelligence/diff-size.js +42 -0
  69. package/dist/intelligence/ebpf-verify.js +45 -0
  70. package/dist/intelligence/enterprise-policy-engine.js +1122 -0
  71. package/dist/intelligence/error-fingerprint.js +1 -0
  72. package/dist/intelligence/execution-gate.js +100 -0
  73. package/dist/intelligence/execution-mode.js +22 -0
  74. package/dist/intelligence/gate-analytics.js +256 -0
  75. package/dist/intelligence/gate-decision.js +64 -0
  76. package/dist/intelligence/gcp-broker.js +50 -0
  77. package/dist/intelligence/git-adr-sync.js +207 -0
  78. package/dist/intelligence/hitl-hold.js +460 -0
  79. package/dist/intelligence/hypothesis-history.js +93 -0
  80. package/dist/intelligence/idp-identity.js +83 -0
  81. package/dist/intelligence/impl-critic.js +216 -0
  82. package/dist/intelligence/incident-autopilot.js +622 -0
  83. package/dist/intelligence/incident-replay.js +162 -0
  84. package/dist/intelligence/incident-result-cache.js +33 -0
  85. package/dist/intelligence/incident-similarity.js +58 -0
  86. package/dist/intelligence/infra-detectors.js +312 -0
  87. package/dist/intelligence/license.js +185 -0
  88. package/dist/intelligence/llm-spokesperson.js +62 -0
  89. package/dist/intelligence/mcp-prompts.js +320 -0
  90. package/dist/intelligence/mcp-resources.js +171 -0
  91. package/dist/intelligence/normalize.js +108 -0
  92. package/dist/intelligence/notifications.js +83 -0
  93. package/dist/intelligence/oidc-issuer.js +87 -0
  94. package/dist/intelligence/override-corpus.js +597 -0
  95. package/dist/intelligence/planning-gate.js +84 -0
  96. package/dist/intelligence/plans.js +223 -0
  97. package/dist/intelligence/platt-scaling.js +164 -0
  98. package/dist/intelligence/policy-proposals.js +84 -0
  99. package/dist/intelligence/policy-suggester.js +161 -0
  100. package/dist/intelligence/policy-sync.js +103 -0
  101. package/dist/intelligence/policy-test-runner.js +35 -0
  102. package/dist/intelligence/postmortem-parser.js +181 -0
  103. package/dist/intelligence/postmortem-retrieval.js +224 -0
  104. package/dist/intelligence/postmortem-store.js +596 -0
  105. package/dist/intelligence/pr-commenter.js +81 -0
  106. package/dist/intelligence/pr-policy-gate.js +38 -0
  107. package/dist/intelligence/pr-shadow-analyzer.js +229 -0
  108. package/dist/intelligence/process-tree.js +108 -0
  109. package/dist/intelligence/prompts.js +1 -0
  110. package/dist/intelligence/replay.js +38 -0
  111. package/dist/intelligence/repro-steps.js +1 -0
  112. package/dist/intelligence/resource-extractors.js +0 -0
  113. package/dist/intelligence/resource-file-context.js +138 -0
  114. package/dist/intelligence/risk-score.js +22 -0
  115. package/dist/intelligence/rollback.js +137 -0
  116. package/dist/intelligence/runbook-updater.js +143 -0
  117. package/dist/intelligence/sandbox.js +194 -0
  118. package/dist/intelligence/script-trust.js +104 -0
  119. package/dist/intelligence/session-metrics.js +67 -0
  120. package/dist/intelligence/session-threat-tracker.js +231 -0
  121. package/dist/intelligence/shadow-digest-cron.js +81 -0
  122. package/dist/intelligence/shadow-log.js +284 -0
  123. package/dist/intelligence/shell-ast.js +145 -0
  124. package/dist/intelligence/siem-forward.js +104 -0
  125. package/dist/intelligence/slack-digest.js +151 -0
  126. package/dist/intelligence/slack-override-loop.js +217 -0
  127. package/dist/intelligence/slack-routing.js +67 -0
  128. package/dist/intelligence/slack.js +900 -0
  129. package/dist/intelligence/sql-ast.js +31 -0
  130. package/dist/intelligence/team.js +1 -0
  131. package/dist/intelligence/telemetry.js +76 -0
  132. package/dist/intelligence/threshold-optimizer.js +75 -0
  133. package/dist/intelligence/token-budget.js +66 -0
  134. package/dist/intelligence/tool-guard.js +708 -0
  135. package/dist/intelligence/tool-manifest 2.js +74 -0
  136. package/dist/intelligence/tool-manifest.js +125 -0
  137. package/dist/intelligence/tools-analysis.js +857 -0
  138. package/dist/intelligence/tools-arch.js +152 -0
  139. package/dist/intelligence/tools-autonomy.js +495 -0
  140. package/dist/intelligence/tools-blast-radius.js +140 -0
  141. package/dist/intelligence/tools-browser.js +431 -0
  142. package/dist/intelligence/tools-change-timeline.js +154 -0
  143. package/dist/intelligence/tools-credentials.js +117 -0
  144. package/dist/intelligence/tools-datadog.js +233 -0
  145. package/dist/intelligence/tools-debug-sessions.js +209 -0
  146. package/dist/intelligence/tools-discovery.js +284 -0
  147. package/dist/intelligence/tools-infra.js +612 -0
  148. package/dist/intelligence/tools-intent.js +128 -0
  149. package/dist/intelligence/tools-memory.js +514 -0
  150. package/dist/intelligence/tools-runbook.js +951 -0
  151. package/dist/intelligence/tools-sessions.js +107 -0
  152. package/dist/intelligence/tools-state.js +232 -0
  153. package/dist/intelligence/tools-utility.js +430 -0
  154. package/dist/intelligence/tools-validate.js +215 -0
  155. package/dist/intelligence/tools.js +1 -0
  156. package/dist/intelligence/trace-context.js +11 -0
  157. package/dist/intelligence/unclassified-clusters.js +87 -0
  158. package/dist/intelligence/usage.js +195 -0
  159. package/dist/routes/active-authors.js +63 -0
  160. package/dist/routes/activity-feed.js +21 -0
  161. package/dist/routes/adr.js +117 -0
  162. package/dist/routes/agent-activity.js +184 -0
  163. package/dist/routes/agent-blunders.js +163 -0
  164. package/dist/routes/agents.js +175 -0
  165. package/dist/routes/api-keys.js +65 -0
  166. package/dist/routes/arch.js +75 -0
  167. package/dist/routes/audit-export.js +93 -0
  168. package/dist/routes/billing-dashboard.js +158 -0
  169. package/dist/routes/billing-outcome.js +77 -0
  170. package/dist/routes/calibration.js +305 -0
  171. package/dist/routes/ci-gate-history.js +20 -0
  172. package/dist/routes/ci-gate.js +232 -0
  173. package/dist/routes/ci.js +293 -0
  174. package/dist/routes/compliance-report.js +24 -0
  175. package/dist/routes/confidence.js +30 -0
  176. package/dist/routes/credentials.js +119 -0
  177. package/dist/routes/dashboard.js +1407 -0
  178. package/dist/routes/decision-memory.js +42 -0
  179. package/dist/routes/demo.js +844 -0
  180. package/dist/routes/device-auth-stub.js +173 -0
  181. package/dist/routes/explain-why.js +39 -0
  182. package/dist/routes/gate-analytics.js +213 -0
  183. package/dist/routes/gate.js +28 -0
  184. package/dist/routes/github-webhook.js +364 -0
  185. package/dist/routes/habituation.js +30 -0
  186. package/dist/routes/health-integrations.js +243 -0
  187. package/dist/routes/heartbeats.js +45 -0
  188. package/dist/routes/hitl.js +364 -0
  189. package/dist/routes/impact-report.js +645 -0
  190. package/dist/routes/incident-webhook.js +63 -0
  191. package/dist/routes/incidents.js +366 -0
  192. package/dist/routes/layers.js +62 -0
  193. package/dist/routes/license.js +187 -0
  194. package/dist/routes/oidc-issuer.js +30 -0
  195. package/dist/routes/onboarding.js +170 -0
  196. package/dist/routes/otel.js +67 -0
  197. package/dist/routes/otlp-receiver.js +179 -0
  198. package/dist/routes/overrides.js +241 -0
  199. package/dist/routes/pagerduty.js +258 -0
  200. package/dist/routes/policies.js +809 -0
  201. package/dist/routes/policy-nl.js +169 -0
  202. package/dist/routes/postmortem.js +102 -0
  203. package/dist/routes/pr-shadow.js +47 -0
  204. package/dist/routes/rbac.js +61 -0
  205. package/dist/routes/replay.js +17 -0
  206. package/dist/routes/risk-report.js +164 -0
  207. package/dist/routes/runbooks.js +125 -0
  208. package/dist/routes/safety-test.js +174 -0
  209. package/dist/routes/sdk.js +222 -0
  210. package/dist/routes/sensor.js +819 -0
  211. package/dist/routes/sentry.js +140 -0
  212. package/dist/routes/sessions.js +43 -0
  213. package/dist/routes/setup-ui.js +594 -0
  214. package/dist/routes/shadow-report.js +107 -0
  215. package/dist/routes/slack-routing.js +171 -0
  216. package/dist/routes/team-usage.js +71 -0
  217. package/dist/routes/telemetry.js +28 -0
  218. package/dist/routes/tenants.js +199 -0
  219. package/dist/routes/tickets.js +167 -0
  220. package/dist/routes/validate.js +13 -0
  221. package/dist/routes/war-room.js +113 -0
  222. package/dist/scripts/check-arch.js +22 -0
  223. package/dist/seeds/community-corpus.js +176 -0
  224. package/dist/seeds/corpus.js +1224 -0
  225. package/dist/sensor/action-ledger.js +327 -0
  226. package/dist/sensor/adr-store.js +140 -0
  227. package/dist/sensor/agent-blunder-store.js +370 -0
  228. package/dist/sensor/agent-context-store.js +225 -0
  229. package/dist/sensor/agent-memory-store.js +231 -0
  230. package/dist/sensor/audit-fetch.js +23 -0
  231. package/dist/sensor/audit-log.js +273 -0
  232. package/dist/sensor/buffer-schemas.js +225 -0
  233. package/dist/sensor/buffer.js +599 -0
  234. package/dist/sensor/bypass-tracker.js +93 -0
  235. package/dist/sensor/ci-gate-history.js +70 -0
  236. package/dist/sensor/cloud-auth.js +183 -0
  237. package/dist/sensor/commit-context-store.js +235 -0
  238. package/dist/sensor/docker-log-stream.js +164 -0
  239. package/dist/sensor/docker-monitor.js +122 -0
  240. package/dist/sensor/extended-buffer.js +46 -0
  241. package/dist/sensor/feedback-token.js +41 -0
  242. package/dist/sensor/file-lock.js +6 -0
  243. package/dist/sensor/fs-watcher.js +106 -0
  244. package/dist/sensor/gate-heartbeat.js +122 -0
  245. package/dist/sensor/git-suspect.js +136 -0
  246. package/dist/sensor/habituation-store.js +77 -0
  247. package/dist/sensor/heartbeat-monitor.js +133 -0
  248. package/dist/sensor/incident-store.js +340 -0
  249. package/dist/sensor/infra-normalizer.js +513 -0
  250. package/dist/sensor/ingest.js +339 -0
  251. package/dist/sensor/jest-reporter.js +52 -0
  252. package/dist/sensor/k8s-events.js +132 -0
  253. package/dist/sensor/layer2-store.js +111 -0
  254. package/dist/sensor/layer3-store.js +230 -0
  255. package/dist/sensor/layer4-store.js +147 -0
  256. package/dist/sensor/logger.js +13 -0
  257. package/dist/sensor/otel-exporter.js +161 -0
  258. package/dist/sensor/paths.js +71 -0
  259. package/dist/sensor/policy-history.js +147 -0
  260. package/dist/sensor/pr-shadow-store.js +83 -0
  261. package/dist/sensor/process-watcher.js +162 -0
  262. package/dist/sensor/rbac.js +92 -0
  263. package/dist/sensor/redact.js +114 -0
  264. package/dist/sensor/redis-store.js +191 -0
  265. package/dist/sensor/route-reachability.js +64 -0
  266. package/dist/sensor/security-utils.js +18 -0
  267. package/dist/sensor/service-graph.js +170 -0
  268. package/dist/sensor/service-topology.js +227 -0
  269. package/dist/sensor/session-history.js +74 -0
  270. package/dist/sensor/session-persist.js +38 -0
  271. package/dist/sensor/shadow-promote.js +122 -0
  272. package/dist/sensor/sourcemap.js +281 -0
  273. package/dist/sensor/sqlite-store.js +205 -0
  274. package/dist/sensor/sso.js +164 -0
  275. package/dist/sensor/vitest-reporter.js +65 -0
  276. package/dist/sensor/watcher.js +48 -0
  277. package/dist/storage/interfaces.js +0 -0
  278. package/dist/storage/pg/pg-action-ledger.js +138 -0
  279. package/dist/storage/pg/pg-approval-store.js +107 -0
  280. package/dist/storage/pg/pg-blunder-store.js +193 -0
  281. package/dist/storage/pg/pg-ci-gate-history.js +83 -0
  282. package/dist/storage/pg/pg-client.js +24 -0
  283. package/dist/storage/pg/pg-event-store.js +63 -0
  284. package/dist/storage/pg/pg-incident-store.js +190 -0
  285. package/dist/storage/pg/pg-migrations.js +26 -0
  286. package/dist/storage/pg/pg-override-corpus.js +447 -0
  287. package/dist/storage/pg/pg-shadow-log.js +129 -0
  288. package/dist/storage/sqlite/sqlite-action-ledger.js +18 -0
  289. package/dist/storage/sqlite/sqlite-approval-store.js +37 -0
  290. package/dist/storage/sqlite/sqlite-blunder-store.js +27 -0
  291. package/dist/storage/sqlite/sqlite-ci-gate-history.js +19 -0
  292. package/dist/storage/sqlite/sqlite-event-store.js +29 -0
  293. package/dist/storage/sqlite/sqlite-incident-store.js +34 -0
  294. package/dist/storage/sqlite/sqlite-override-corpus.js +75 -0
  295. package/dist/storage/sqlite/sqlite-shadow-log.js +36 -0
  296. package/dist/storage/store-factory.js +55 -0
  297. package/dist/storage/store-registry.js +31 -0
  298. package/dist/update-checker.js +107 -0
  299. package/dist/workers/autopilot-worker.js +28 -0
  300. package/dist/workers/notification-worker.js +28 -0
  301. package/dist/workers/queues.js +58 -0
  302. package/dist/workers/validation-worker.js +24 -0
  303. package/dist/workers/worker-registry.js +18 -0
  304. package/package.json +123 -0
  305. package/sdk/mergen-inject.js +260 -0
  306. package/sdk/node.js +313 -0
  307. package/sdk/vite-plugin.ts +57 -0
  308. package/sdk/webpack-plugin.js +106 -0
@@ -0,0 +1,710 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "fs";
2
+ import { resolve, join } from "path";
3
+ import { homedir } from "os";
4
+ import { log, success, error, hr, findPort } from "./shared.js";
5
+ async function approveCommand(cmdArgs) {
6
+ const token = cmdArgs[0]?.trim();
7
+ if (!token) {
8
+ error("Token required. Usage: mergen approve <id>");
9
+ process.exit(1);
10
+ }
11
+ let secret = "";
12
+ const secretPath = join(homedir(), ".mergen", "secret");
13
+ if (existsSync(secretPath)) {
14
+ try {
15
+ secret = readFileSync(secretPath, "utf8").trim();
16
+ } catch {
17
+ }
18
+ }
19
+ log("Locating running Mergen gateway server...");
20
+ let port = 3e3;
21
+ let discovered = false;
22
+ for (let p = 3e3; p <= 3010; p++) {
23
+ try {
24
+ const r = await fetch(`http://127.0.0.1:${p}/health`, { signal: AbortSignal.timeout(300) });
25
+ if (r.ok) {
26
+ port = p;
27
+ discovered = true;
28
+ break;
29
+ }
30
+ } catch {
31
+ }
32
+ }
33
+ if (!discovered) {
34
+ error("No active Mergen server detected on local ports 3000-3010.");
35
+ log("Make sure the server is running: mergen-server start");
36
+ process.exit(1);
37
+ }
38
+ log(`Connecting to server on port ${port}...`);
39
+ try {
40
+ const headers = { "Content-Type": "application/json" };
41
+ if (secret) {
42
+ headers["x-mergen-secret"] = secret;
43
+ }
44
+ const res = await fetch(`http://127.0.0.1:${port}/hitl/approve`, {
45
+ method: "POST",
46
+ headers,
47
+ body: JSON.stringify({ token })
48
+ });
49
+ const body = await res.json();
50
+ if (res.ok && body.ok) {
51
+ success(`Approved! The blocked tool call will be allowed to proceed.`);
52
+ } else {
53
+ error(`Approval failed: ${body.error ?? "Unknown error"}`);
54
+ process.exit(1);
55
+ }
56
+ } catch (err) {
57
+ error(`Failed to reach server: ${err instanceof Error ? err.message : String(err)}`);
58
+ process.exit(1);
59
+ }
60
+ }
61
+ async function shadowReportCommand() {
62
+ let port = 3e3;
63
+ let serverFound = false;
64
+ for (let p = 3e3; p <= 3010; p++) {
65
+ try {
66
+ const r = await fetch(`http://127.0.0.1:${p}/health`, { signal: AbortSignal.timeout(800) });
67
+ if (r.ok) {
68
+ port = p;
69
+ serverFound = true;
70
+ break;
71
+ }
72
+ } catch {
73
+ }
74
+ }
75
+ if (!serverFound) {
76
+ error("Server not running. Start with: mergen-server start");
77
+ process.exit(1);
78
+ }
79
+ const secret = (() => {
80
+ try {
81
+ return readFileSync(join(homedir(), ".mergen", "secret"), "utf8").trim();
82
+ } catch {
83
+ return "";
84
+ }
85
+ })();
86
+ const headers = { "content-type": "application/json" };
87
+ if (secret) headers["x-mergen-secret"] = secret;
88
+ const BASE = `http://127.0.0.1:${port}`;
89
+ const [summaryRes, entriesRes] = await Promise.all([
90
+ fetch(`${BASE}/shadow-report`, { headers, signal: AbortSignal.timeout(5e3) }),
91
+ fetch(`${BASE}/shadow-report/entries?limit=20`, { headers, signal: AbortSignal.timeout(5e3) })
92
+ ]);
93
+ const summary = summaryRes.ok ? await summaryRes.json() : null;
94
+ const entriesData = entriesRes.ok ? await entriesRes.json() : null;
95
+ const W = 60;
96
+ const div = "\u2500".repeat(W);
97
+ const days = summary?.windowDays ?? 30;
98
+ console.log(`
99
+ Shadow Mode Track Record \u2014 last ${days} days`);
100
+ console.log(div);
101
+ const entries = entriesData?.entries ?? [];
102
+ if (entries.length === 0) {
103
+ console.log(" No shadow entries yet.");
104
+ console.log(" Shadow mode posts diagnoses without executing fixes.");
105
+ console.log(" Set MERGEN_SHADOW_MODE=true and connect PagerDuty to start.\n");
106
+ } else {
107
+ console.log(` ${"Date".padEnd(12)}${"Service".padEnd(12)}${"Tag".padEnd(26)}${"Conf".padEnd(6)}Verdict`);
108
+ console.log(div);
109
+ for (const e of entries) {
110
+ const date = new Date(e.recordedAt).toISOString().slice(0, 10);
111
+ const svc = (e.service ?? "unknown").slice(0, 10).padEnd(12);
112
+ const tag = (e.incidentTag ?? "").replace(/^infra_/, "").slice(0, 24).padEnd(26);
113
+ const conf = `${e.diagnosisConfidence}%`.padEnd(6);
114
+ const verd = e.humanVerdict === "would-approve" ? "\u2705 approve" : e.humanVerdict === "would-override" ? "\u270B override" : "(unreviewed)";
115
+ console.log(` ${date.padEnd(12)}${svc}${tag}${conf}${verd}`);
116
+ }
117
+ console.log(div);
118
+ const total = summary?.total ?? entries.length;
119
+ const reviewed = summary?.humanReviewed ?? 0;
120
+ const fpNote = reviewed >= 5 && summary?.approvalRate != null ? `false positive rate: ${Math.round((1 - summary.approvalRate) * 100)}%` : `false positive rate: pending (${reviewed} of 5 reviews needed)`;
121
+ console.log(` Total: ${total} entries \xB7 ${reviewed} reviewed \xB7 ${fpNote}`);
122
+ if (summary?.recommendation) console.log(` ${summary.recommendation}`);
123
+ }
124
+ console.log(`
125
+ Run: mergen-server impact-report for the full Day-30 deck summary
126
+ `);
127
+ }
128
+ async function allowCommand(args) {
129
+ const { loadEnterprisePolicy, saveEnterprisePolicy } = await import("../intelligence/enterprise-policy-engine.js");
130
+ const fileIdx = args.indexOf("--file");
131
+ const actorIdx = args.indexOf("--actor");
132
+ const svcIdx = args.indexOf("--service");
133
+ const fileGlob = fileIdx !== -1 ? args[fileIdx + 1] : null;
134
+ const actorFlag = actorIdx !== -1 ? args[actorIdx + 1] : "all";
135
+ const svcFlag = svcIdx !== -1 ? args[svcIdx + 1] : void 0;
136
+ const positional = args.filter((a, i) => !a.startsWith("--") && (fileIdx === -1 || i !== fileIdx + 1) && (actorIdx === -1 || i !== actorIdx + 1) && (svcIdx === -1 || i !== svcIdx + 1));
137
+ const cmdPattern = positional[0] ?? null;
138
+ if (!cmdPattern && !fileGlob) {
139
+ console.log('Usage: mergen-server allow "<command pattern>"');
140
+ console.log(' mergen-server allow --file "<file glob>"');
141
+ console.log(' mergen-server allow "npm install" --actor ai --service api');
142
+ process.exit(1);
143
+ }
144
+ const actor = actorFlag === "ai" || actorFlag === "human" ? actorFlag : "all";
145
+ const label = cmdPattern ?? fileGlob;
146
+ const rule = {
147
+ id: `allowlist_${Date.now()}`,
148
+ name: `Allowlist: ${label}`,
149
+ description: `Added via mergen-server allow on ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`,
150
+ action: "pass",
151
+ reason: `Explicitly allowed: ${label}`,
152
+ conditions: {
153
+ ...cmdPattern ? { commands: [cmdPattern] } : {},
154
+ ...fileGlob ? { files: [fileGlob] } : {},
155
+ ...actor !== "all" ? { actorType: actor } : {},
156
+ ...svcFlag ? { services: [svcFlag] } : {}
157
+ }
158
+ };
159
+ const config = loadEnterprisePolicy();
160
+ config.rules.push(rule);
161
+ saveEnterprisePolicy(config);
162
+ success(`Allowlisted: ${label}`);
163
+ if (actor !== "all") log(`Actor: ${actor}`, "\u2139");
164
+ if (svcFlag) log(`Service: ${svcFlag}`, "\u2139");
165
+ console.log(`
166
+ Rule id: ${rule.id}`);
167
+ console.log(` Policy file: ~/.mergen/enterprise-policy.json`);
168
+ console.log(`
169
+ To remove it: edit ~/.mergen/enterprise-policy.json and delete the rule with this id.
170
+ `);
171
+ }
172
+ async function verifyLogCommand() {
173
+ let port = 3e3;
174
+ let result = null;
175
+ for (let p = 3e3; p <= 3010; p++) {
176
+ try {
177
+ const r = await fetch(`http://127.0.0.1:${p}/agent-blunders/verify`, { signal: AbortSignal.timeout(2e3) });
178
+ if (r.ok) {
179
+ result = await r.json();
180
+ port = p;
181
+ break;
182
+ }
183
+ } catch {
184
+ }
185
+ }
186
+ hr();
187
+ console.log("\u2B21 Mergen \u2014 Agent Blunder Log integrity check\n");
188
+ if (!result) {
189
+ log("Server not running \u2014 cannot verify log. Start with: mergen-server start", "\u26A0");
190
+ process.exit(1);
191
+ }
192
+ if (result.valid) {
193
+ const count = result.verified ?? 0;
194
+ if (count === 0) {
195
+ success(`Log integrity: PASS \u2014 ${result.note ?? "Log is empty"}`);
196
+ } else {
197
+ success(`Log integrity: PASS \u2014 ${count} entries verified`);
198
+ if (result.truncated) {
199
+ log(`Chain is partial (ring buffer wrapped) \u2014 pre-eviction entries cannot be verified. This is expected, not a tamper signal.`, "\u2B21");
200
+ }
201
+ if (result.verifiedFrom) {
202
+ log(`Verified from entry: ${result.verifiedFrom}`, "\u2B21");
203
+ }
204
+ }
205
+ hr();
206
+ console.log(`
207
+ API: GET http://127.0.0.1:${port}/agent-blunders/verify
208
+ `);
209
+ process.exit(0);
210
+ } else {
211
+ hr();
212
+ error(`Log integrity: FAIL \u2014 hash chain broken at entry index ${result.firstInvalidIdx ?? "?"}`);
213
+ if (result.reason) console.error(` Reason: ${result.reason}`);
214
+ console.error("\n The Agent Blunder Log may have been tampered with or corrupted.");
215
+ console.error(` Full details: GET http://127.0.0.1:${port}/agent-blunders/verify`);
216
+ hr();
217
+ process.exit(1);
218
+ }
219
+ }
220
+ async function guardCommand(args) {
221
+ const strict = args.includes("--strict");
222
+ const install = args.includes("--install");
223
+ if (install) {
224
+ const hookPath = resolve(process.cwd(), ".git", "hooks", "pre-commit");
225
+ if (!existsSync(resolve(process.cwd(), ".git"))) {
226
+ error("Not a git repository. Run from project root.");
227
+ process.exit(1);
228
+ }
229
+ const script = `#!/bin/sh
230
+ # Mergen guard \u2014 checks for runtime errors before commit
231
+ mergen-server guard
232
+ `;
233
+ writeFileSync(hookPath, script, { encoding: "utf8", mode: 493 });
234
+ success(`Pre-commit hook installed: ${hookPath}`);
235
+ log("Mergen will now report runtime errors before each commit.");
236
+ return;
237
+ }
238
+ let serverPort = 0;
239
+ let health = null;
240
+ for (let p = 3e3; p <= 3010; p++) {
241
+ try {
242
+ const r = await fetch(`http://127.0.0.1:${p}/health`, { signal: AbortSignal.timeout(800) });
243
+ if (r.ok) {
244
+ health = await r.json();
245
+ serverPort = p;
246
+ break;
247
+ }
248
+ } catch {
249
+ }
250
+ }
251
+ if (!health) {
252
+ log("Mergen: server not running \u2014 skipping runtime check", "\u2B21");
253
+ process.exit(0);
254
+ }
255
+ let incidentHits = [];
256
+ try {
257
+ const { execSync: _exec } = await import("child_process");
258
+ const staged = _exec("git diff --cached --name-only 2>/dev/null", { encoding: "utf8" }).trim();
259
+ if (staged) {
260
+ const stagedServices = /* @__PURE__ */ new Set();
261
+ for (const f of staged.split("\n")) {
262
+ const m = f.match(/^(?:services|apps|packages|src)\/([^/]+)\//);
263
+ if (m) stagedServices.add(m[1].toLowerCase());
264
+ const top = f.split("/")[0];
265
+ if (top && !["src", "test", "tests", "docs", ".github", "scripts", "config"].includes(top)) {
266
+ stagedServices.add(top.toLowerCase());
267
+ }
268
+ }
269
+ if (stagedServices.size > 0) {
270
+ const r = await fetch(`http://127.0.0.1:${serverPort}/override-corpus`, {
271
+ signal: AbortSignal.timeout(1e3)
272
+ });
273
+ if (r.ok) {
274
+ const body = await r.json();
275
+ for (const entry of body.corpus ?? []) {
276
+ const matchedService = entry.services.find(
277
+ (s) => stagedServices.has(s.toLowerCase())
278
+ );
279
+ if (matchedService) {
280
+ incidentHits.push({
281
+ tag: entry.tag,
282
+ service: matchedService,
283
+ reason: entry.dominantReason,
284
+ timePattern: entry.timePattern,
285
+ total: entry.total
286
+ });
287
+ }
288
+ }
289
+ }
290
+ }
291
+ }
292
+ } catch {
293
+ }
294
+ const errors = health.errors ?? 0;
295
+ const warns = health.warnings ?? 0;
296
+ const netErr = health.networkErrors ?? 0;
297
+ const sigs = health.signals ?? [];
298
+ const highSig = sigs.filter((s) => s.confidence >= 0.8);
299
+ hr();
300
+ console.log("\u2B21 Mergen \u2014 pre-commit runtime check\n");
301
+ if (errors === 0 && warns === 0 && netErr === 0 && sigs.length === 0) {
302
+ success("Buffer clean \u2014 no errors, warnings, or signals");
303
+ } else {
304
+ if (errors > 0) console.log(` \u26A0 ${errors} console error(s) in buffer`);
305
+ if (warns > 0) console.log(` \u26A0 ${warns} console warning(s) in buffer`);
306
+ if (netErr > 0) console.log(` \u26A0 ${netErr} network error(s) in buffer`);
307
+ for (const s of sigs.slice(0, 3)) {
308
+ const pct = Math.round(s.confidence * 100);
309
+ console.log(` \u{1F50D} [${pct}%] ${s.message}`);
310
+ }
311
+ if (strict && (errors > 0 || highSig.length > 0)) {
312
+ console.log('\n Ask your AI: "analyze_runtime" before committing');
313
+ hr();
314
+ error("Commit blocked (--strict mode). Fix runtime errors first.");
315
+ process.exit(1);
316
+ }
317
+ log('\nCommit allowed. Run "analyze_runtime" to investigate.');
318
+ }
319
+ if (incidentHits.length > 0) {
320
+ console.log("");
321
+ console.log(" \u2B21 Incident history for staged services:");
322
+ for (const hit of incidentHits.slice(0, 5)) {
323
+ const reason = hit.reason ? ` \xB7 ${hit.reason}` : "";
324
+ const pattern = hit.timePattern ? ` \xB7 ${hit.timePattern}` : "";
325
+ console.log(` \u2022 ${hit.service} \u2014 ${hit.tag}${reason}${pattern} (${hit.total}\xD7)`);
326
+ }
327
+ console.log(` This is informational. Review corpus: GET /override-corpus`);
328
+ }
329
+ hr();
330
+ process.exit(0);
331
+ }
332
+ async function policyPushCommand() {
333
+ const port = await findPort();
334
+ if (!port) {
335
+ error("Server not running. Start it first: mergen-server start");
336
+ process.exit(1);
337
+ }
338
+ const resp = await fetch(`http://127.0.0.1:${port}/policies/export`, { signal: AbortSignal.timeout(5e3) });
339
+ if (!resp.ok) {
340
+ error(`Policy fetch failed: ${resp.status}`);
341
+ process.exit(1);
342
+ }
343
+ const policy = await resp.json();
344
+ const { mkdirSync: mkd, writeFileSync: wf } = await import("fs");
345
+ const dir = resolve(process.cwd(), ".mergen");
346
+ mkd(dir, { recursive: true });
347
+ const file = resolve(dir, "policy.json");
348
+ wf(file, JSON.stringify(policy, null, 2) + "\n", "utf8");
349
+ success(`Policy saved to .mergen/policy.json`);
350
+ log("Commit this file to track policy changes in git:");
351
+ log(' git add .mergen/policy.json && git commit -m "chore: update mergen policy"');
352
+ }
353
+ async function policyPullCommand(args) {
354
+ const mode = args.includes("--merge") ? "merge" : "replace";
355
+ const { readFileSync: rf, existsSync: ex } = await import("fs");
356
+ const file = resolve(process.cwd(), ".mergen", "policy.json");
357
+ if (!ex(file)) {
358
+ error("No .mergen/policy.json found. Run: mergen-server policy-push first.");
359
+ process.exit(1);
360
+ }
361
+ let policy;
362
+ try {
363
+ policy = JSON.parse(rf(file, "utf8"));
364
+ } catch {
365
+ error("Failed to parse .mergen/policy.json");
366
+ process.exit(1);
367
+ }
368
+ const port = await findPort();
369
+ if (!port) {
370
+ error("Server not running. Start it first: mergen-server start");
371
+ process.exit(1);
372
+ }
373
+ const resp = await fetch(`http://127.0.0.1:${port}/policies/import`, {
374
+ method: "POST",
375
+ headers: { "Content-Type": "application/json" },
376
+ body: JSON.stringify({ policy, mode }),
377
+ signal: AbortSignal.timeout(5e3)
378
+ });
379
+ const data = await resp.json();
380
+ if (!resp.ok || !data.ok) {
381
+ error(`Policy import failed: ${JSON.stringify(data.error)}`);
382
+ process.exit(1);
383
+ }
384
+ success(`Policy applied (${mode}): ${data.ruleCount} rules active`);
385
+ }
386
+ async function policyDiffCommand() {
387
+ const { readFileSync: rf, existsSync: ex } = await import("fs");
388
+ const file = resolve(process.cwd(), ".mergen", "policy.json");
389
+ const port = await findPort();
390
+ if (!port) {
391
+ error("Server not running.");
392
+ process.exit(1);
393
+ }
394
+ const resp = await fetch(`http://127.0.0.1:${port}/policies/export`, { signal: AbortSignal.timeout(5e3) });
395
+ if (!resp.ok) {
396
+ error("Could not fetch live policy.");
397
+ process.exit(1);
398
+ }
399
+ const live = await resp.json();
400
+ const liveIds = new Set((live.rules ?? []).map((r) => r.id));
401
+ if (!ex(file)) {
402
+ log("No .mergen/policy.json \u2014 run mergen-server policy-push to create it");
403
+ process.exit(0);
404
+ }
405
+ let disk;
406
+ try {
407
+ disk = JSON.parse(rf(file, "utf8"));
408
+ } catch {
409
+ error("Cannot parse .mergen/policy.json");
410
+ process.exit(1);
411
+ return;
412
+ }
413
+ const diskIds = new Set((disk.rules ?? []).map((r) => r.id));
414
+ hr();
415
+ console.log("\u2B21 Mergen \u2014 Policy Diff (.mergen/policy.json vs live server)\n");
416
+ const onlyLive = [...liveIds].filter((id) => !diskIds.has(id));
417
+ const onlyDisk = [...diskIds].filter((id) => !liveIds.has(id));
418
+ const inBoth = [...liveIds].filter((id) => diskIds.has(id));
419
+ if (onlyLive.length === 0 && onlyDisk.length === 0) {
420
+ success("Policy files are in sync \u2014 no differences");
421
+ } else {
422
+ if (onlyLive.length > 0) console.log(` + Live only (not in file): ${onlyLive.join(", ")}`);
423
+ if (onlyDisk.length > 0) console.log(` - File only (not in live): ${onlyDisk.join(", ")}`);
424
+ console.log(` = Matching rules: ${inBoth.length}`);
425
+ log("\nTo sync file \u2192 server: mergen-server policy-pull");
426
+ log("To sync server \u2192 file: mergen-server policy-push");
427
+ }
428
+ try {
429
+ const impactResp = await _authedFetch(port, "/policies/diff-impact", {
430
+ method: "POST",
431
+ body: JSON.stringify(disk)
432
+ });
433
+ if (impactResp.ok) {
434
+ const impact = await impactResp.json();
435
+ if (impact.ok && impact.eventsInWindow > 0) {
436
+ console.log(`
437
+ Impact if applied (replayed against ${impact.eventsInWindow} recent live calls):`);
438
+ console.log(` would newly block: ${impact.newlyBlocked} would newly hold: ${impact.newlyHeld}`);
439
+ console.log(` would no longer block: ${impact.noLongerBlocked} would no longer hold: ${impact.noLongerHeld}`);
440
+ const families = Object.entries(impact.byToolFamily);
441
+ if (families.length > 0) {
442
+ console.log(` affected tools: ${families.map(([t, n]) => `${t} (${n})`).join(", ")}`);
443
+ }
444
+ }
445
+ }
446
+ } catch {
447
+ }
448
+ hr();
449
+ }
450
+ async function policySimulateCommand(args) {
451
+ const filePath = args.find((a) => !a.startsWith("--"));
452
+ if (!filePath) {
453
+ error("Usage: mergen-server policy simulate <policyFile> [--window-days=N]");
454
+ process.exit(1);
455
+ }
456
+ const resolved = resolve(process.cwd(), filePath);
457
+ if (!existsSync(resolved)) {
458
+ error(`File not found: ${resolved}`);
459
+ process.exit(1);
460
+ }
461
+ let policy;
462
+ try {
463
+ policy = JSON.parse(readFileSync(resolved, "utf8"));
464
+ } catch {
465
+ error(`Failed to parse ${resolved} as JSON`);
466
+ process.exit(1);
467
+ }
468
+ const windowFlag = args.find((a) => a.startsWith("--window-days="));
469
+ const windowDays = windowFlag ? Number(windowFlag.split("=")[1]) : 30;
470
+ const port = await findPort();
471
+ if (!port) {
472
+ error("Server not running. Start it first: mergen-server start");
473
+ process.exit(1);
474
+ }
475
+ let secret = "";
476
+ const secretPath = join(homedir(), ".mergen", "secret");
477
+ if (existsSync(secretPath)) {
478
+ try {
479
+ secret = readFileSync(secretPath, "utf8").trim();
480
+ } catch {
481
+ }
482
+ }
483
+ const resp = await fetch(`http://127.0.0.1:${port}/policies/preview?windowDays=${windowDays}`, {
484
+ method: "POST",
485
+ headers: { "Content-Type": "application/json", ...secret ? { "x-mergen-secret": secret } : {} },
486
+ body: JSON.stringify(policy),
487
+ signal: AbortSignal.timeout(1e4)
488
+ });
489
+ const data = await resp.json();
490
+ if (!resp.ok || !data.ok) {
491
+ error(`Simulation failed: ${JSON.stringify(data.error ?? data)}`);
492
+ process.exit(1);
493
+ }
494
+ hr();
495
+ console.log(`\u2B21 Mergen \u2014 Policy Simulation (${filePath}, last ${data.windowDays} days)
496
+ `);
497
+ console.log(` ${data.blundersEvaluated} historical events replayed`);
498
+ console.log(` would block: ${data.wouldBlock} (${data.blockRate}%) would hold: ${data.wouldWarn} (${data.warnRate}%) would pass: ${data.wouldPass} (${data.passRate}%)`);
499
+ console.log(` agents affected: ${data.agentsAffected.length > 0 ? data.agentsAffected.join(", ") : "none"}`);
500
+ console.log(` services affected: ${data.servicesAffected.length > 0 ? data.servicesAffected.join(", ") : "none"}`);
501
+ if (data.changedVerdicts.length > 0) {
502
+ console.log(`
503
+ Sample of changed outcomes (previously blocked, would now ${data.changedVerdicts[0].preview}):`);
504
+ for (const c of data.changedVerdicts) {
505
+ console.log(` - [${c.service ?? "unknown"}] ${c.command ?? c.id} \u2192 ${c.preview}`);
506
+ }
507
+ }
508
+ console.log(`
509
+ ${data.note}`);
510
+ hr();
511
+ }
512
+ async function _authedFetch(port, urlPath, init = {}) {
513
+ let secret = "";
514
+ const secretPath = join(homedir(), ".mergen", "secret");
515
+ if (existsSync(secretPath)) {
516
+ try {
517
+ secret = readFileSync(secretPath, "utf8").trim();
518
+ } catch {
519
+ }
520
+ }
521
+ return fetch(`http://127.0.0.1:${port}${urlPath}`, {
522
+ ...init,
523
+ headers: { "Content-Type": "application/json", ...secret ? { "x-mergen-secret": secret } : {}, ...init.headers ?? {} },
524
+ signal: AbortSignal.timeout(1e4)
525
+ });
526
+ }
527
+ async function policyVersionsCommand() {
528
+ const port = await findPort();
529
+ if (!port) {
530
+ error("Server not running. Start it first: mergen-server start");
531
+ process.exit(1);
532
+ }
533
+ const resp = await _authedFetch(port, "/policies/versions");
534
+ const data = await resp.json();
535
+ if (!resp.ok || !data.ok) {
536
+ error(`Failed to list versions: ${JSON.stringify(data.error ?? data)}`);
537
+ process.exit(1);
538
+ }
539
+ hr();
540
+ console.log("\u2B21 Mergen \u2014 Policy Versions (newest first)\n");
541
+ if (data.versions.length === 0) {
542
+ log("No policy versions recorded yet \u2014 versions are created automatically on every policy save.");
543
+ } else {
544
+ for (const v of data.versions) console.log(` ${v}`);
545
+ log(`
546
+ Roll back with: mergen-server policy-rollback <versionId>`);
547
+ }
548
+ hr();
549
+ }
550
+ async function policyRollbackCommand(args) {
551
+ const versionId = args[0]?.trim();
552
+ if (!versionId) {
553
+ error("Usage: mergen-server policy-rollback <versionId>");
554
+ log("List available versions with: mergen-server policy-versions");
555
+ process.exit(1);
556
+ }
557
+ const port = await findPort();
558
+ if (!port) {
559
+ error("Server not running. Start it first: mergen-server start");
560
+ process.exit(1);
561
+ }
562
+ const resp = await _authedFetch(port, `/policies/versions/${encodeURIComponent(versionId)}/revert`, { method: "POST" });
563
+ const data = await resp.json();
564
+ if (!resp.ok || !data.ok) {
565
+ error(`Rollback failed: ${JSON.stringify(data.error ?? data)}`);
566
+ process.exit(1);
567
+ }
568
+ success(`Policy rolled back to version ${data.revertedTo} (${data.ruleCount} rules active).`);
569
+ }
570
+ async function traceReplayCommand(args) {
571
+ const traceId = args[0]?.trim();
572
+ if (!traceId) {
573
+ error("Usage: mergen-server trace-replay <traceId>");
574
+ log("Find a trace id from a gate-event/blunder-log entry, or MERGEN_TRACE_ID if you set one explicitly.");
575
+ process.exit(1);
576
+ }
577
+ const port = await findPort();
578
+ if (!port) {
579
+ error("Server not running. Start it first: mergen-server start");
580
+ process.exit(1);
581
+ }
582
+ const resp = await _authedFetch(port, `/replay/${encodeURIComponent(traceId)}`);
583
+ const data = await resp.json();
584
+ if (!resp.ok || !data.ok) {
585
+ error(`Replay failed: ${JSON.stringify(data.error ?? data)}`);
586
+ process.exit(1);
587
+ }
588
+ hr();
589
+ console.log(`\u2B21 Mergen \u2014 Runtime Replay (trace ${traceId})
590
+ `);
591
+ for (const e of data.entries) {
592
+ const when = new Date(e.ts).toISOString();
593
+ if (e.source === "gate-event") {
594
+ console.log(` [${when}] GATE ${e.toolName ?? ""} \u2192 ${e.verdict} ${e.command ?? ""}`);
595
+ } else {
596
+ console.log(` [${when}] BLUNDER ${e.blunderType} ${e.command ?? ""}`);
597
+ }
598
+ if (e.triggeredRules && e.triggeredRules.length > 0) {
599
+ console.log(` rules: ${e.triggeredRules.join(", ")}`);
600
+ }
601
+ }
602
+ console.log(`
603
+ ${data.note}`);
604
+ hr();
605
+ }
606
+ async function testSafetyCommand() {
607
+ const port = await findPort();
608
+ let results;
609
+ if (port) {
610
+ try {
611
+ const resp = await fetch(`http://127.0.0.1:${port}/safety-test?verbose=1`, {
612
+ signal: AbortSignal.timeout(5e3)
613
+ });
614
+ const data = await resp.json();
615
+ hr();
616
+ console.log("\u2B21 Mergen \u2014 Safety Test Suite\n");
617
+ console.log(` ${data.passed}/${data.total} tests passed`);
618
+ if (data.cases) {
619
+ for (const c of data.cases) {
620
+ const icon = c.passed ? "\u2713" : "\u2717";
621
+ console.log(` ${icon} ${c.name}`);
622
+ if (!c.passed) console.log(` expected=${c.expected} got=${c.got}`);
623
+ }
624
+ }
625
+ hr();
626
+ process.exit(data.allPassed ? 0 : 1);
627
+ return;
628
+ } catch {
629
+ }
630
+ }
631
+ log("Server not running \u2014 running safety tests locally");
632
+ const { loadEnterprisePolicy, DEFAULT_ENTERPRISE_POLICY, _resetPolicyCacheForTesting } = await import("../intelligence/enterprise-policy-engine.js");
633
+ const { runPolicyTestCases } = await import("../intelligence/policy-test-runner.js");
634
+ const { DEFAULT_SAFETY_TESTS } = await import("../intelligence/default-safety-tests.js");
635
+ const saved = loadEnterprisePolicy();
636
+ _resetPolicyCacheForTesting(DEFAULT_ENTERPRISE_POLICY);
637
+ const summary = runPolicyTestCases(DEFAULT_SAFETY_TESTS);
638
+ hr();
639
+ console.log("\u2B21 Mergen \u2014 Safety Test Suite\n");
640
+ for (const r of summary.results) {
641
+ const icon = r.passed ? "\u2713" : "\u2717";
642
+ console.log(` ${icon} ${r.id}`);
643
+ if (!r.passed) console.log(` expected=${r.expected} got=${r.got}`);
644
+ }
645
+ console.log(`
646
+ ${summary.passed}/${summary.total} tests passed`);
647
+ hr();
648
+ _resetPolicyCacheForTesting(saved);
649
+ process.exit(summary.allPassed ? 0 : 1);
650
+ }
651
+ async function policyTestCommand(args) {
652
+ const { runPolicyTestCases, parsePolicyTestFile } = await import("../intelligence/policy-test-runner.js");
653
+ const filePath = args.find((a) => !a.startsWith("--"));
654
+ let cases;
655
+ let sourceLabel;
656
+ if (filePath) {
657
+ const resolved = resolve(process.cwd(), filePath);
658
+ if (!existsSync(resolved)) {
659
+ error(`File not found: ${resolved}`);
660
+ process.exit(1);
661
+ }
662
+ let raw;
663
+ try {
664
+ raw = JSON.parse(readFileSync(resolved, "utf8"));
665
+ } catch {
666
+ error(`Failed to parse ${resolved} as JSON`);
667
+ process.exit(1);
668
+ }
669
+ try {
670
+ cases = parsePolicyTestFile(raw);
671
+ } catch (err) {
672
+ error(err instanceof Error ? err.message : String(err));
673
+ process.exit(1);
674
+ }
675
+ sourceLabel = filePath;
676
+ } else {
677
+ const { DEFAULT_SAFETY_TESTS } = await import("../intelligence/default-safety-tests.js");
678
+ cases = DEFAULT_SAFETY_TESTS;
679
+ sourceLabel = "built-in default suite (8 cases)";
680
+ }
681
+ const summary = runPolicyTestCases(cases);
682
+ hr();
683
+ console.log(`\u2B21 Mergen \u2014 Policy Test (${sourceLabel})
684
+ `);
685
+ for (const r of summary.results) {
686
+ const icon = r.passed ? "\u2713" : "\u2717";
687
+ console.log(` ${icon} ${r.id}${r.description ? ` \u2014 ${r.description}` : ""}`);
688
+ if (!r.passed) console.log(` expected=${r.expected} got=${r.got} reasons=[${r.reasons.join("; ")}]`);
689
+ }
690
+ console.log(`
691
+ ${summary.passed}/${summary.total} tests passed`);
692
+ hr();
693
+ process.exit(summary.allPassed ? 0 : 1);
694
+ }
695
+ export {
696
+ allowCommand,
697
+ approveCommand,
698
+ guardCommand,
699
+ policyDiffCommand,
700
+ policyPullCommand,
701
+ policyPushCommand,
702
+ policyRollbackCommand,
703
+ policySimulateCommand,
704
+ policyTestCommand,
705
+ policyVersionsCommand,
706
+ shadowReportCommand,
707
+ testSafetyCommand,
708
+ traceReplayCommand,
709
+ verifyLogCommand
710
+ };