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,612 @@
1
+ import { z } from "zod";
2
+ import { store } from "../sensor/buffer.js";
3
+ import { truncateToTokenBudget } from "./token-budget.js";
4
+ import { hypothesisHistory } from "./hypothesis-history.js";
5
+ import { getActivePlanId } from "./license.js";
6
+ import { getPlan } from "./plans.js";
7
+ import { trackCall, withTierGate } from "./tools-state.js";
8
+ import { getTierForTool } from "./tool-manifest.js";
9
+ import { serviceGraph } from "../sensor/service-graph.js";
10
+ import { incidentStore } from "../sensor/incident-store.js";
11
+ import logger from "../sensor/logger.js";
12
+ function registerInfraTools(server) {
13
+ server.registerTool(
14
+ "get_unified_timeline",
15
+ {
16
+ description: "Returns the complete cross-signal causal timeline with confidence labels on every event. EXACT = deterministic traceId join (browser request \u2194 backend log line, same request). LINKED = structural SHA join (git blame / CI run / deployment matches browser build). ~CORR = timestamp proximity only (within 2s, statistical). OBS = event captured, no cross-signal link found. Start with EXACT rows \u2014 they are the verified causal chain. Use LINKED rows to find the responsible commit. Includes root cause hypothesis at top when confidence \u2265 45%.",
17
+ inputSchema: {
18
+ seconds: z.number().int().min(60).max(3600).optional().describe("Time window in seconds (default 300 = 5 minutes, max 3600 = 1 hour)"),
19
+ limit: z.number().int().min(1).max(200).optional().describe("Max events to return (default 50)")
20
+ }
21
+ },
22
+ withTierGate(getTierForTool("get_unified_timeline"), async ({ seconds = 300, limit = 50 }) => {
23
+ trackCall("get_unified_timeline");
24
+ const since = Date.now() - seconds * 1e3;
25
+ const BACKEND_ERROR_RE = /error|exception|traceback|panic|fatal|killed|oom/i;
26
+ const backendTraceSet = /* @__PURE__ */ new Set();
27
+ for (const t of store.getTerminalOutput(200, void 0, since)) {
28
+ if (t.traceId) backendTraceSet.add(t.traceId);
29
+ }
30
+ const browserTraceMap = /* @__PURE__ */ new Map();
31
+ for (const n of store.getNetwork(limit, void 0, since)) {
32
+ if (n.traceId) browserTraceMap.set(n.traceId, `${n.method} ${n.url}`);
33
+ }
34
+ const browserBuildShas = /* @__PURE__ */ new Set();
35
+ for (const e of store.getLogs(limit, void 0, since)) {
36
+ if (e.buildSha) browserBuildShas.add(e.buildSha.toLowerCase());
37
+ }
38
+ for (const n of store.getNetwork(limit, void 0, since)) {
39
+ if (n.buildSha) browserBuildShas.add(n.buildSha.toLowerCase());
40
+ }
41
+ const ciAndDeployShas = /* @__PURE__ */ new Set();
42
+ for (const c of store.getCIEvents(50, void 0, since)) {
43
+ ciAndDeployShas.add(c.sha.toLowerCase());
44
+ if (c.shortSha) ciAndDeployShas.add(c.shortSha.toLowerCase());
45
+ }
46
+ for (const d of store.getDeployments(20, void 0, since)) {
47
+ ciAndDeployShas.add(d.sha.toLowerCase());
48
+ if (d.shortSha) ciAndDeployShas.add(d.shortSha.toLowerCase());
49
+ }
50
+ const backendErrTimes = store.getTerminalOutput(100, void 0, since).filter((t) => BACKEND_ERROR_RE.test(t.data)).map((t) => t.timestamp);
51
+ function nearBackend(ts) {
52
+ return backendErrTimes.some((t) => Math.abs(t - ts) <= 2e3);
53
+ }
54
+ function badge(c) {
55
+ switch (c) {
56
+ case "exact":
57
+ return "`EXACT `";
58
+ case "linked":
59
+ return "`LINKED `";
60
+ case "temporal":
61
+ return "`~CORR `";
62
+ case "observed":
63
+ return "`OBS `";
64
+ }
65
+ }
66
+ const rows = [];
67
+ for (const e of store.getLogs(limit, void 0, since)) {
68
+ if (e.level === "log") continue;
69
+ const msg = e.args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ").slice(0, 150);
70
+ const blame = e.gitSuspect ? ` [${e.gitSuspect.sha.slice(0, 7)} \xB7 ${e.gitSuspect.author}]` : "";
71
+ const confidence = e.gitSuspect ? "linked" : nearBackend(e.timestamp) ? "temporal" : "observed";
72
+ rows.push({ ts: e.timestamp, icon: e.level === "error" ? "\u{1F534}" : "\u{1F7E1}", label: msg + blame, source: "browser", confidence });
73
+ }
74
+ for (const n of store.getNetwork(limit, void 0, since)) {
75
+ if (n.status < 400 && n.status !== 0 && !n.error) continue;
76
+ const traceJoin = n.traceId ? backendTraceSet.has(n.traceId) : false;
77
+ const shaJoin = n.buildSha ? ciAndDeployShas.has(n.buildSha.toLowerCase()) : false;
78
+ const confidence = traceJoin ? "exact" : shaJoin ? "linked" : nearBackend(n.timestamp) ? "temporal" : "observed";
79
+ const traceNote = traceJoin ? " \u2194 backend" : "";
80
+ rows.push({ ts: n.timestamp, icon: "\u{1F534}", label: `${n.method} ${n.url} \u2192 ${n.status || "ERR"} (${n.duration}ms)${traceNote}`, source: "browser", confidence });
81
+ }
82
+ for (const t of store.getTerminalOutput(100, void 0, since)) {
83
+ if (!BACKEND_ERROR_RE.test(t.data)) continue;
84
+ const traceJoin = t.traceId ? browserTraceMap.has(t.traceId) : false;
85
+ const confidence = traceJoin ? "exact" : "observed";
86
+ const traceNote = traceJoin ? ` \u2194 ${browserTraceMap.get(t.traceId)}` : "";
87
+ rows.push({ ts: t.timestamp, icon: "\u{1F4BB}", label: `[${t.terminalName}] ${t.data.slice(0, 150)}${traceNote}`, source: "backend", confidence });
88
+ }
89
+ for (const p of store.getProcessExits(20, void 0, since)) {
90
+ if (p.reason === "normal") continue;
91
+ rows.push({ ts: p.timestamp, icon: "\u{1F4A5}", label: `[${p.process}] crashed (exit ${p.exitCode})`, source: "backend", confidence: "observed" });
92
+ }
93
+ for (const c of store.getCIEvents(50, void 0, since)) {
94
+ const icon = c.status === "failure" ? "\u274C" : c.status === "success" ? "\u2705" : "\u23ED";
95
+ const tests = c.failedTests && c.failedTests.length > 0 ? ` \u2014 ${c.failedTests.slice(0, 2).map((t) => t.name).join(", ")}` : "";
96
+ const sha7 = c.shortSha ?? c.sha.slice(0, 7);
97
+ const shaJoin = browserBuildShas.has(c.sha.toLowerCase()) || browserBuildShas.has(sha7.toLowerCase());
98
+ rows.push({ ts: c.timestamp, icon, label: `CI ${c.status}: ${c.job}${tests}`, source: "ci", sha: sha7, confidence: shaJoin ? "linked" : "observed" });
99
+ }
100
+ for (const d of store.getDeployments(20, void 0, since)) {
101
+ const icon = d.status === "success" ? "\u{1F680}" : d.status === "failure" ? "\u{1F4A5}" : d.status === "rollback" ? "\u23EA" : "\u23F3";
102
+ const sha7 = d.shortSha ?? d.sha.slice(0, 7);
103
+ const shaJoin = browserBuildShas.has(d.sha.toLowerCase()) || browserBuildShas.has(sha7.toLowerCase());
104
+ rows.push({ ts: d.timestamp, icon, label: `Deploy to ${d.environment}: ${d.status}${d.actor ? " by " + d.actor : ""}`, source: "deploy", sha: sha7, confidence: shaJoin ? "linked" : "observed" });
105
+ }
106
+ rows.sort((a, b) => a.ts - b.ts);
107
+ const trimmed = rows.slice(-limit);
108
+ const latest = hypothesisHistory.latest();
109
+ const top = latest?.topHypothesis ?? null;
110
+ const lines = [];
111
+ if (top && top.confidenceScore >= 0.45) {
112
+ const pct = Math.round(top.confidenceScore * 100);
113
+ lines.push(`## Root Cause \u2014 ${pct}% confidence`, `**${top.summary}**`);
114
+ if (top.fixHint) lines.push(`Fix: ${top.fixHint}`);
115
+ lines.push("", "---", "");
116
+ }
117
+ const exactCount = trimmed.filter((r) => r.confidence === "exact").length;
118
+ const linkedCount = trimmed.filter((r) => r.confidence === "linked").length;
119
+ const temporalCount = trimmed.filter((r) => r.confidence === "temporal").length;
120
+ lines.push(`## Unified Timeline (last ${Math.round(seconds / 60)}m \xB7 ${trimmed.length} events)`);
121
+ if (exactCount > 0 || linkedCount > 0 || temporalCount > 0) {
122
+ const parts = [];
123
+ if (exactCount > 0) parts.push(`${exactCount} exact trace join${exactCount > 1 ? "s" : ""}`);
124
+ if (linkedCount > 0) parts.push(`${linkedCount} SHA-linked`);
125
+ if (temporalCount > 0) parts.push(`${temporalCount} temporal`);
126
+ lines.push(`*Causal joins: ${parts.join(" \xB7 ")}*`);
127
+ }
128
+ lines.push("");
129
+ for (const r of trimmed) {
130
+ const time = new Date(r.ts).toISOString().slice(11, 19);
131
+ const sha = r.sha ? ` [${r.sha}]` : "";
132
+ lines.push(`\`${time}\` ${r.icon} ${badge(r.confidence)} **[${r.source.toUpperCase()}]** ${r.label}${sha}`);
133
+ }
134
+ if (trimmed.length === 0) {
135
+ lines.push("*No significant events in the last " + Math.round(seconds / 60) + " minutes.*", "");
136
+ lines.push("Connect your CI pipeline: `POST /ci/github` or `POST /ci/generic`");
137
+ lines.push("Notify Mergen of deploys: `POST /deployments`");
138
+ lines.push("Stream backend logs: `mergen-server watch npm start`");
139
+ } else {
140
+ lines.push("", "> `EXACT` = traceId join \xB7 `LINKED` = SHA/git join \xB7 `~CORR` = timestamp proximity \xB7 `OBS` = no cross-signal link");
141
+ }
142
+ return { content: [{ type: "text", text: lines.join("\n") }] };
143
+ })
144
+ );
145
+ server.registerTool(
146
+ "get_ci_results",
147
+ {
148
+ description: 'Returns CI/CD run results (GitHub Actions, GitLab CI, etc.) captured by Mergen. Use this when a browser error appeared after a deploy to ask: "Did the CI run for this commit have failures? Which tests?" The commit SHA links CI failures directly to browser errors.',
149
+ inputSchema: {
150
+ limit: z.number().int().min(1).max(100).optional().describe("Max results to return (default 20)"),
151
+ status: z.enum(["success", "failure", "cancelled", "skipped"]).optional().describe('Filter by status \u2014 use "failure" to see only broken builds'),
152
+ since: z.number().int().optional().describe("Only return CI runs after this Unix timestamp in ms"),
153
+ sha: z.string().optional().describe("Filter to a specific commit SHA (full or short)")
154
+ }
155
+ },
156
+ async ({ limit, status, since, sha }) => {
157
+ trackCall("get_ci_results");
158
+ let events = store.getCIEvents(limit ?? 20, status, since);
159
+ if (sha) {
160
+ const s = sha.toLowerCase();
161
+ events = events.filter((e) => e.sha.startsWith(s) || s.startsWith(e.sha.slice(0, 7)));
162
+ }
163
+ if (events.length === 0) {
164
+ return {
165
+ content: [{
166
+ type: "text",
167
+ text: 'No CI events in buffer.\n\nTo send CI results to Mergen, add to your GitHub Actions workflow:\n\n - name: Report to Mergen\n if: always()\n run: |\n curl -s -X POST $MERGEN_URL/ci/generic \\\n -H "Content-Type: application/json" \\\n -d \'{"sha":"${{ github.sha }}","branch":"${{ github.ref_name }}",\' \\\n \'"status":"${{ job.status }}","job":"${{ github.job }}"}\'\n\nOr use: POST /ci/github, /ci/gitlab, /ci/generic, /deployments'
168
+ }]
169
+ };
170
+ }
171
+ const failures = events.filter((e) => e.status === "failure");
172
+ const lines = [`## CI Results (${events.length} runs, ${failures.length} failure${failures.length !== 1 ? "s" : ""})`, ""];
173
+ for (const e of events) {
174
+ const ts = new Date(e.timestamp).toISOString();
175
+ const icon = e.status === "success" ? "\u2705" : e.status === "failure" ? "\u274C" : "\u23ED";
176
+ const dur = e.durationMs ? ` (${Math.round(e.durationMs / 1e3)}s)` : "";
177
+ lines.push(`${icon} **${e.job}** \xB7 ${e.shortSha ?? e.sha.slice(0, 7)} \xB7 ${e.branch ?? ""} \xB7 ${ts}${dur}`);
178
+ if (e.workflow) lines.push(` Workflow: ${e.workflow} (${e.provider})`);
179
+ if (e.url) lines.push(` URL: ${e.url}`);
180
+ if (e.failedTests && e.failedTests.length > 0) {
181
+ lines.push(` Failed tests (${e.failedTests.length}):`);
182
+ for (const t of e.failedTests.slice(0, 10))
183
+ lines.push(` \u2022 ${t.name}${t.error ? ` \u2014 ${t.error.slice(0, 100)}` : ""}`);
184
+ }
185
+ lines.push("");
186
+ }
187
+ return { content: [{ type: "text", text: lines.join("\n") }] };
188
+ }
189
+ );
190
+ server.registerTool(
191
+ "get_deployments",
192
+ {
193
+ description: 'Returns deployment events captured by Mergen. Use this to answer "when was this version deployed and who deployed it?" and to correlate browser errors with the specific deploy that introduced them.',
194
+ inputSchema: {
195
+ limit: z.number().int().min(1).max(100).optional().describe("Max results to return (default 10)"),
196
+ environment: z.string().optional().describe('Filter by environment name (e.g. "staging", "production")'),
197
+ since: z.number().int().optional().describe("Only return deployments after this Unix timestamp in ms")
198
+ }
199
+ },
200
+ async ({ limit, environment, since }) => {
201
+ trackCall("get_deployments");
202
+ const events = store.getDeployments(limit ?? 10, environment, since);
203
+ if (events.length === 0) {
204
+ return {
205
+ content: [{
206
+ type: "text",
207
+ text: `No deployment events in buffer.
208
+
209
+ To notify Mergen of a deploy:
210
+
211
+ curl -X POST $MERGEN_URL/deployments \\
212
+ -H "Content-Type: application/json" \\
213
+ -d '{"sha":"abc1234","environment":"staging","status":"success"}'
214
+
215
+ Supported status values: started | success | failure | rollback`
216
+ }]
217
+ };
218
+ }
219
+ const lines = [`## Deployments (${events.length})`, ""];
220
+ for (const d of events) {
221
+ const ts = new Date(d.timestamp).toISOString();
222
+ const icon = d.status === "success" ? "\u{1F680}" : d.status === "failure" ? "\u{1F4A5}" : d.status === "rollback" ? "\u23EA" : "\u23F3";
223
+ lines.push(`${icon} **${d.environment}** \xB7 ${d.shortSha ?? d.sha.slice(0, 7)} \xB7 ${d.status} \xB7 ${ts}`);
224
+ if (d.service) lines.push(` Service: ${d.service}`);
225
+ if (d.version) lines.push(` Version: ${d.version}`);
226
+ if (d.actor) lines.push(` Actor: ${d.actor}`);
227
+ if (d.url) lines.push(` URL: ${d.url}`);
228
+ lines.push("");
229
+ }
230
+ return { content: [{ type: "text", text: lines.join("\n") }] };
231
+ }
232
+ );
233
+ server.registerTool(
234
+ "get_process_logs",
235
+ {
236
+ description: 'Returns stdout/stderr lines captured from local dev servers and containers. Use this when a browser error (e.g. fetch \u2192 500) needs to be correlated with backend output. Filter by process_name to narrow to a specific service (e.g. "backend", "docker:api").',
237
+ inputSchema: {
238
+ limit: z.number().int().min(1).max(500).optional().describe("Max lines to return (default 100)"),
239
+ process_name: z.string().optional().describe('Filter to a specific process or container name (partial match, e.g. "api", "docker:web")'),
240
+ since: z.number().int().optional().describe("Only return lines after this Unix timestamp in ms"),
241
+ errors_only: z.boolean().optional().describe("If true, only return lines matching error/exception/panic/fatal patterns"),
242
+ max_tokens: z.number().int().min(100).max(1e4).optional().describe("Soft token limit for response")
243
+ }
244
+ },
245
+ async ({ limit, process_name, since, errors_only, max_tokens }) => {
246
+ trackCall("get_process_logs");
247
+ let events = store.getTerminalOutput(limit ?? 100, void 0, since);
248
+ if (process_name) {
249
+ const pat = process_name.toLowerCase();
250
+ events = events.filter((e) => e.terminalName.toLowerCase().includes(pat));
251
+ }
252
+ if (errors_only) {
253
+ const ERROR_RE = /error|exception|traceback|panic|fatal|unhandled|crash|killed|oom/i;
254
+ events = events.filter((e) => ERROR_RE.test(e.data));
255
+ }
256
+ const processExits = store.getProcessExits(20, void 0, since);
257
+ if (events.length === 0 && processExits.length === 0) {
258
+ return {
259
+ content: [{
260
+ type: "text",
261
+ text: "No process logs in buffer.\n\nTo stream a dev server into Mergen:\n mergen-server watch npm start\n mergen-server watch python manage.py runserver\n\nOr enable Docker log streaming: MERGEN_DOCKER_LOGS=true"
262
+ }]
263
+ };
264
+ }
265
+ const sources = [...new Set(events.map((e) => e.terminalName))];
266
+ const lines = events.map((e) => `[${new Date(e.timestamp).toISOString()}] [${e.terminalName}] ${e.data}`);
267
+ for (const p of processExits) {
268
+ lines.push(`[${new Date(p.timestamp).toISOString()}] [${p.process}] EXIT code=${p.exitCode} reason=${p.reason}${p.signal ? " signal=" + p.signal : ""}`);
269
+ }
270
+ const header = `Process logs from: ${sources.join(", ")} (${events.length} lines)
271
+
272
+ `;
273
+ const { result, truncated, omitted } = truncateToTokenBudget(lines, max_tokens, "\n");
274
+ if (truncated) logger.info({ tool: "get_process_logs", omitted }, "response truncated");
275
+ return { content: [{ type: "text", text: header + result }] };
276
+ }
277
+ );
278
+ server.registerTool(
279
+ "get_code_owners",
280
+ {
281
+ description: "Looks up the CODEOWNERS entry for a given file path. Use this after finding a suspect commit to identify which team owns the file and who to page or assign the bug to. Reads .github/CODEOWNERS, CODEOWNERS, or docs/CODEOWNERS from the working directory.",
282
+ inputSchema: {
283
+ file_path: z.string().describe('Relative or absolute file path to look up (e.g. "src/auth/token.ts")'),
284
+ cwd: z.string().optional().describe("Working directory to search for CODEOWNERS (defaults to process.cwd())")
285
+ }
286
+ },
287
+ withTierGate(getTierForTool("get_code_owners"), async ({ file_path, cwd }) => {
288
+ trackCall("get_code_owners");
289
+ const { findCodeOwners } = await import("../sensor/git-suspect.js");
290
+ const result = findCodeOwners(file_path, cwd ?? process.cwd());
291
+ if (!result) {
292
+ return {
293
+ content: [{
294
+ type: "text",
295
+ text: `No CODEOWNERS entry found for \`${file_path}\`.
296
+
297
+ Either no CODEOWNERS file exists in this repo, or the file doesn't match any pattern.
298
+ CODEOWNERS should be at .github/CODEOWNERS, CODEOWNERS, or docs/CODEOWNERS.`
299
+ }]
300
+ };
301
+ }
302
+ return {
303
+ content: [{
304
+ type: "text",
305
+ text: [
306
+ `## Code Owners for \`${file_path}\``,
307
+ "",
308
+ `**Owners:** ${result.owners.join(", ")}`,
309
+ `**Matched pattern:** \`${result.pattern}\``,
310
+ `**Source:** \`${result.source}\``,
311
+ "",
312
+ `These are the teams/individuals responsible for this file.`,
313
+ `Tag them in the bug report or assign the fix to their queue.`
314
+ ].join("\n")
315
+ }]
316
+ };
317
+ })
318
+ );
319
+ server.registerTool(
320
+ "get_backend_logs",
321
+ {
322
+ description: "Returns structured log events from Node.js or Python backend SDKs. Events come from services instrumented with mergen-node or mergen-python. Use service to filter to a specific backend service by name. Use sdk to filter to node or python events only.",
323
+ inputSchema: {
324
+ service: z.string().optional().describe("Filter by service name (MERGEN_NAME env var)"),
325
+ sdk: z.enum(["node", "python"]).optional().describe("Filter to a specific SDK"),
326
+ level: z.enum(["log", "warn", "error"]).optional().describe("Filter by log level"),
327
+ since: z.number().int().optional().describe("Only return events after this Unix ms timestamp"),
328
+ limit: z.number().int().min(1).max(200).optional().describe("Max events (default 50)")
329
+ }
330
+ },
331
+ withTierGate(getTierForTool("get_backend_logs"), async ({ service, sdk, level, since, limit }) => {
332
+ trackCall("get_backend_logs");
333
+ let events = store.getLogs(limit ?? 50, level, since);
334
+ events = events.filter(
335
+ (e) => e.url?.startsWith("mergen://node/") || e.url?.startsWith("mergen://python/")
336
+ );
337
+ if (service) events = events.filter((e) => e.url?.includes(`/${service}`));
338
+ if (sdk === "node") events = events.filter((e) => e.url?.startsWith("mergen://node/"));
339
+ if (sdk === "python") events = events.filter((e) => e.url?.startsWith("mergen://python/"));
340
+ if (events.length === 0) {
341
+ return { content: [{ type: "text", text: "No backend log events. Instrument your service with mergen-node or mergen-python." }] };
342
+ }
343
+ const lines = events.map((e) => {
344
+ const ts = new Date(e.timestamp).toISOString();
345
+ const svcLabel = e.url?.split("/").slice(2).join("/") ?? "backend";
346
+ const args = e.args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
347
+ const stack = e.stack ? `
348
+ Stack: ${e.stack}` : "";
349
+ return `[${ts}] [${e.level.toUpperCase()}] [${svcLabel}] ${args}${stack}`;
350
+ });
351
+ return { content: [{ type: "text", text: `${events.length} backend log event(s):
352
+
353
+ ${lines.join("\n")}` }] };
354
+ })
355
+ );
356
+ server.registerTool(
357
+ "get_backend_spans",
358
+ {
359
+ description: "Returns server-side request spans from Node.js or Python SDK middleware. Each span covers one inbound HTTP request: route, method, status, duration, traceId. The traceId links this span to the matching browser network event for end-to-end tracing. Use get_correlated_trace after finding a traceId to see the full round-trip.",
360
+ inputSchema: {
361
+ service: z.string().optional().describe("Filter by service name"),
362
+ trace_id: z.string().optional().describe("Filter to a specific 32-char hex traceId"),
363
+ status_filter: z.number().int().optional().describe("Filter to a specific HTTP status code (e.g. 500)"),
364
+ since: z.number().int().optional().describe("Only return spans after this Unix ms timestamp"),
365
+ limit: z.number().int().min(1).max(200).optional().describe("Max spans (default 50)")
366
+ }
367
+ },
368
+ async ({ service, trace_id, status_filter, since, limit }) => {
369
+ trackCall("get_backend_spans");
370
+ if (!getPlan(getActivePlanId()).backendObservability) {
371
+ return {
372
+ content: [{
373
+ type: "text",
374
+ text: `\u26D4 **Backend span tracing** is a Pro feature.
375
+
376
+ Upgrade to **Pro ($29/mo)** for OTLP export, backend spans, and end-to-end trace correlation.
377
+
378
+ \u2192 https://mergen.app/pricing`
379
+ }],
380
+ isError: true
381
+ };
382
+ }
383
+ let spans = store.getBackendSpans(limit ?? 50, service, since);
384
+ if (trace_id) spans = spans.filter((s) => s.traceId.toLowerCase() === trace_id.toLowerCase());
385
+ if (status_filter !== void 0) spans = spans.filter((s) => s.statusCode === status_filter);
386
+ if (spans.length === 0) {
387
+ return { content: [{ type: "text", text: "No backend spans. Add mergen-node Express middleware or mergen-python Django/FastAPI middleware to capture server spans." }] };
388
+ }
389
+ const browserTraceIds = new Set(store.getNetwork(200).filter((n) => n.traceId).map((n) => n.traceId));
390
+ const lines = spans.map((s) => {
391
+ const ts = new Date(s.timestamp).toISOString();
392
+ const flag = s.statusCode >= 500 ? " [CRITICAL]" : s.statusCode >= 400 ? " [ERROR]" : "";
393
+ const joined = browserTraceIds.has(s.traceId) ? " [JOINED\u2192browser]" : "";
394
+ const err = s.error ? ` \u2014 ${s.error}` : "";
395
+ return `[${ts}] [${s.sdk}:${s.service}] ${s.method} ${s.route} \u2192 ${s.statusCode} (${s.durationMs}ms)${flag}${joined}${err}
396
+ traceId: ${s.traceId}`;
397
+ });
398
+ return { content: [{ type: "text", text: `${spans.length} backend span(s):
399
+
400
+ ${lines.join("\n\n")}` }] };
401
+ }
402
+ );
403
+ server.registerTool(
404
+ "get_correlated_trace",
405
+ {
406
+ description: "Given a traceId, returns ALL events that share it: the browser fetch/XHR event, the matching backend span, and any backend log lines that logged the same traceId. This gives a deterministic end-to-end view of one request as it traveled from browser to server. Use after get_backend_spans or get_network_activity shows a traceId of interest.",
407
+ inputSchema: {
408
+ trace_id: z.string().describe("32-char hex traceId from a network event or backend span")
409
+ }
410
+ },
411
+ async ({ trace_id }) => {
412
+ trackCall("get_correlated_trace");
413
+ if (!getPlan(getActivePlanId()).backendObservability) {
414
+ return {
415
+ content: [{
416
+ type: "text",
417
+ text: `\u26D4 **End-to-end trace correlation** is a Pro feature.
418
+
419
+ Upgrade to **Pro ($29/mo)** for backend spans and browser-to-server trace correlation.
420
+
421
+ \u2192 https://mergen.app/pricing`
422
+ }],
423
+ isError: true
424
+ };
425
+ }
426
+ const traceId = trace_id.toLowerCase().replace(/-/g, "");
427
+ if (!/^[0-9a-f]{32}$/.test(traceId)) {
428
+ return { content: [{ type: "text", text: "Invalid traceId \u2014 must be a 32-char hex string." }] };
429
+ }
430
+ const browserNet = store.getNetwork(200).filter((n) => n.traceId?.toLowerCase() === traceId);
431
+ const backendSpans = store.getBackendSpans(50).filter((s) => s.traceId.toLowerCase() === traceId);
432
+ const backendLogs = store.getTerminalOutput(200).filter((t) => t.traceId?.toLowerCase() === traceId);
433
+ if (browserNet.length === 0 && backendSpans.length === 0 && backendLogs.length === 0) {
434
+ return { content: [{ type: "text", text: `No events found for traceId ${traceId}. The trace may have expired from the buffer.` }] };
435
+ }
436
+ const lines = [`Correlated trace: ${traceId}
437
+ `];
438
+ if (browserNet.length > 0) {
439
+ lines.push("\u2500\u2500 Browser (fetch/XHR) \u2500\u2500");
440
+ for (const n of browserNet) {
441
+ lines.push(`[${new Date(n.timestamp).toISOString()}] ${n.method} ${n.url} \u2192 ${n.status} ${n.statusText} (${n.duration}ms)`);
442
+ if (n.error) lines.push(` Error: ${n.error}`);
443
+ if (n.responseBody) lines.push(` Response: ${JSON.stringify(n.responseBody).slice(0, 300)}`);
444
+ }
445
+ lines.push("");
446
+ }
447
+ if (backendSpans.length > 0) {
448
+ lines.push("\u2500\u2500 Backend spans \u2500\u2500");
449
+ for (const s of backendSpans) {
450
+ lines.push(`[${new Date(s.timestamp).toISOString()}] [${s.sdk}:${s.service}] ${s.method} ${s.route} \u2192 ${s.statusCode} (${s.durationMs}ms)`);
451
+ if (s.error) lines.push(` Error: ${s.error}`);
452
+ if (s.userId) lines.push(` UserId: ${s.userId}`);
453
+ }
454
+ lines.push("");
455
+ }
456
+ if (backendLogs.length > 0) {
457
+ lines.push("\u2500\u2500 Backend log lines (traceId extracted from stdout) \u2500\u2500");
458
+ for (const t of backendLogs)
459
+ lines.push(`[${new Date(t.timestamp).toISOString()}] [${t.terminalName}] ${t.data.slice(0, 300)}`);
460
+ }
461
+ const joinStatus = browserNet.length > 0 && backendSpans.length > 0 ? "\u2705 EXACT JOIN \u2014 browser request matched to backend span" : browserNet.length > 0 ? "\u26A0\uFE0F PARTIAL \u2014 browser event found, no backend span (SDK not instrumented?)" : "\u26A0\uFE0F PARTIAL \u2014 backend span found, no browser event";
462
+ lines.push("", joinStatus);
463
+ return { content: [{ type: "text", text: lines.join("\n") }] };
464
+ }
465
+ );
466
+ server.registerTool(
467
+ "get_service_graph",
468
+ {
469
+ description: "Returns the live service dependency graph \u2014 which services call which, blast risk for each service (how many upstream callers would be affected by a fix), and historical co-occurrence edges from incident data (which services tend to fail together). Use this BEFORE proposing a fix to understand blast radius: a service with 6+ upstream callers has HIGH blast risk and requires manual approval for any autonomous fix. Also use this to identify cascading failure patterns: if A \u2192 B \u2192 C all show errors simultaneously, fix B first. The OTLP graph is built from live traces; the incident graph accumulates from past incidents. Both are shown \u2014 they complement each other.",
470
+ inputSchema: {
471
+ service: z.string().optional().describe(
472
+ "Focus on a single service. Returns its direct callers, callees, blast risk tier, transitive upstream impact count, and co-incident service history. Omit to see the full cross-service topology."
473
+ ),
474
+ include_incidents: z.boolean().optional().describe("Include incident co-occurrence graph (default: true).")
475
+ }
476
+ },
477
+ async ({ service, include_incidents = true }) => {
478
+ trackCall("get_service_graph");
479
+ if (service) {
480
+ const callers = serviceGraph.getCallers(service);
481
+ const callees = serviceGraph.getCallees(service);
482
+ const blastRisk = serviceGraph.getBlastRisk(service);
483
+ const upstream = serviceGraph.getUpstreamImpact(service);
484
+ const hasOtlpData2 = serviceGraph.size > 0;
485
+ const coIncident = include_incidents ? incidentStore.coOccurringServices(service, 10 * 6e4, 8) : [];
486
+ const riskEmoji = blastRisk === "high" ? "\u{1F534}" : blastRisk === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
487
+ const lines2 = [
488
+ `## Service Graph \u2014 \`${service}\``,
489
+ "",
490
+ `**Blast risk:** ${riskEmoji} ${blastRisk.toUpperCase()} \u2014 ${upstream.length} transitive upstream service${upstream.length !== 1 ? "s" : ""} affected by a fix`,
491
+ ""
492
+ ];
493
+ if (!hasOtlpData2) {
494
+ lines2.push(
495
+ "_OTLP graph is empty \u2014 no spans received yet. Instrument your services with the OpenTelemetry SDK and point them at `:4318/v1/traces` or `:3000/v1/traces` to populate the live topology._",
496
+ ""
497
+ );
498
+ } else {
499
+ if (callers.length > 0) {
500
+ lines2.push(
501
+ `**Callers** (services that call \`${service}\`):`,
502
+ ...callers.map((s) => `- \`${s}\``),
503
+ ""
504
+ );
505
+ } else {
506
+ lines2.push(`**Callers:** none observed in OTLP stream`, "");
507
+ }
508
+ if (callees.length > 0) {
509
+ lines2.push(
510
+ `**Callees** (services \`${service}\` calls):`,
511
+ ...callees.map((s) => `- \`${s}\``),
512
+ ""
513
+ );
514
+ } else {
515
+ lines2.push(`**Callees:** none observed in OTLP stream`, "");
516
+ }
517
+ if (upstream.length > 0) {
518
+ lines2.push(
519
+ `**Transitive upstream impact** (BFS depth \u2264 3):`,
520
+ upstream.map((s) => `\`${s}\``).join(", "),
521
+ ""
522
+ );
523
+ }
524
+ }
525
+ if (include_incidents) {
526
+ if (coIncident.length > 0) {
527
+ lines2.push(
528
+ `**Co-incident services** (failed together within 10 min in incident history):`,
529
+ "",
530
+ "| Service | Co-occurrences |",
531
+ "|---------|---------------|",
532
+ ...coIncident.map(({ service: s, count }) => `| \`${s}\` | ${count} |`),
533
+ "",
534
+ "_High co-occurrence suggests a shared dependency or cascade pattern._"
535
+ );
536
+ } else {
537
+ lines2.push(`**Co-incident services:** no co-occurrence data yet`);
538
+ }
539
+ }
540
+ lines2.push(
541
+ "",
542
+ "---",
543
+ `_Blast risk: LOW = 0\u20132 upstream, MEDIUM = 3\u20135, HIGH = 6+. HIGH blast risk raises the autonomous execution threshold by 10 percentage points._`
544
+ );
545
+ return { content: [{ type: "text", text: lines2.join("\n") }] };
546
+ }
547
+ const hasOtlpData = serviceGraph.size > 0;
548
+ const graphJson = serviceGraph.toJSON();
549
+ const coEdges = include_incidents ? incidentStore.getInteractionGraph() : [];
550
+ const lines = ["## Service Graph \u2014 Full Topology", ""];
551
+ if (!hasOtlpData) {
552
+ lines.push(
553
+ `**OTLP graph:** empty`,
554
+ "",
555
+ "_No OTLP spans received. To populate the live service graph:_",
556
+ "```",
557
+ "# Point your services at Mergen:",
558
+ "OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:3000 node your-app.js",
559
+ "```",
560
+ ""
561
+ );
562
+ } else {
563
+ const lastUpdated = graphJson.lastUpdated ? `${Math.round((Date.now() - graphJson.lastUpdated) / 1e3)}s ago` : "unknown";
564
+ lines.push(
565
+ `**OTLP graph:** ${graphJson.services.length} service${graphJson.services.length !== 1 ? "s" : ""}, ${graphJson.edges.length} edge${graphJson.edges.length !== 1 ? "s" : ""} \xB7 last updated ${lastUpdated}`,
566
+ "",
567
+ "**Services:**",
568
+ graphJson.services.map((s) => {
569
+ const risk = serviceGraph.getBlastRisk(s);
570
+ const emoji = risk === "high" ? "\u{1F534}" : risk === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
571
+ return `${emoji} \`${s}\``;
572
+ }).join(" "),
573
+ ""
574
+ );
575
+ const topEdges = [...graphJson.edges].sort((a, b) => b.callCount - a.callCount).slice(0, 5);
576
+ if (topEdges.length > 0) {
577
+ lines.push(
578
+ "**Top call paths (by volume):**",
579
+ "",
580
+ "| From | To | Calls | Errors |",
581
+ "|------|----|-------|--------|",
582
+ ...topEdges.map((e) => `| \`${e.source}\` | \`${e.target}\` | ${e.callCount} | ${e.errorCount} |`),
583
+ ""
584
+ );
585
+ }
586
+ }
587
+ if (include_incidents && coEdges.length > 0) {
588
+ const topCoEdges = coEdges.slice(0, 5);
589
+ lines.push(
590
+ "**Top co-incident pairs (from incident history):**",
591
+ "",
592
+ "| Service A | Service B | Co-occurrences |",
593
+ "|-----------|-----------|---------------|",
594
+ ...topCoEdges.map((e) => `| \`${e.source}\` | \`${e.target}\` | ${e.weight} |`),
595
+ "",
596
+ "_Co-incident pairs often share a common dependency. When both are paging, fix the shared upstream first._"
597
+ );
598
+ } else if (include_incidents) {
599
+ lines.push("**Co-incident pairs:** no incident history yet");
600
+ }
601
+ lines.push(
602
+ "",
603
+ "---",
604
+ `_Call \`get_service_graph(service: "name")\` for per-service blast risk and callers/callees._`
605
+ );
606
+ return { content: [{ type: "text", text: lines.join("\n") }] };
607
+ }
608
+ );
609
+ }
610
+ export {
611
+ registerInfraTools
612
+ };