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.
- package/LICENSE +57 -0
- package/README.md +96 -0
- package/dist/__stubs__/calibration.js +471 -0
- package/dist/__stubs__/causal.js +601 -0
- package/dist/__stubs__/closed-source.js +150 -0
- package/dist/__stubs__/license.js +35 -0
- package/dist/__stubs__/plans.js +197 -0
- package/dist/app.js +467 -0
- package/dist/cli.js +250 -0
- package/dist/commands/agent-identity.js +79 -0
- package/dist/commands/gate.js +107 -0
- package/dist/commands/github.js +632 -0
- package/dist/commands/incident.js +644 -0
- package/dist/commands/policy.js +710 -0
- package/dist/commands/scan.js +243 -0
- package/dist/commands/setup.js +1218 -0
- package/dist/commands/shared.js +109 -0
- package/dist/commands/team.js +809 -0
- package/dist/datadog/blame-attribution.js +228 -0
- package/dist/datadog/client.js +159 -0
- package/dist/datadog/compactor.js +169 -0
- package/dist/datadog/fingerprinter.js +34 -0
- package/dist/datadog/incident-state.js +21 -0
- package/dist/datadog/line-matcher.js +56 -0
- package/dist/datadog/memory-store.js +399 -0
- package/dist/datadog/otel-trace.js +67 -0
- package/dist/index.js +505 -0
- package/dist/intelligence/action-risk.js +132 -0
- package/dist/intelligence/activity-feed.js +40 -0
- package/dist/intelligence/agent-identity.js +115 -0
- package/dist/intelligence/agent-pipeline.js +329 -0
- package/dist/intelligence/agent-profiles.js +81 -0
- package/dist/intelligence/ai-commit.js +16 -0
- package/dist/intelligence/approval-analytics.js +42 -0
- package/dist/intelligence/approval-events.js +5 -0
- package/dist/intelligence/arch-boundaries.js +94 -0
- package/dist/intelligence/arch-graph.js +117 -0
- package/dist/intelligence/autonomy.js +397 -0
- package/dist/intelligence/azure-broker.js +57 -0
- package/dist/intelligence/azure-proxy-sessions.js +49 -0
- package/dist/intelligence/baseline.js +1 -0
- package/dist/intelligence/behavior-baseline.js +134 -0
- package/dist/intelligence/billing.js +1 -0
- package/dist/intelligence/blast-radius.js +224 -0
- package/dist/intelligence/bypass.js +157 -0
- package/dist/intelligence/calibration-classifier.js +77 -0
- package/dist/intelligence/calibration-git-sync.js +74 -0
- package/dist/intelligence/calibration.js +1 -0
- package/dist/intelligence/cascade-detector.js +58 -0
- package/dist/intelligence/case-study-generator.js +126 -0
- package/dist/intelligence/causal-graph.js +126 -0
- package/dist/intelligence/causal.js +1 -0
- package/dist/intelligence/change-risk.js +102 -0
- package/dist/intelligence/compliance-report.js +134 -0
- package/dist/intelligence/confidence-report.js +85 -0
- package/dist/intelligence/corpus-to-policy.js +140 -0
- package/dist/intelligence/credential-broker.js +112 -0
- package/dist/intelligence/credential-misuse.js +39 -0
- package/dist/intelligence/debug-sessions.js +108 -0
- package/dist/intelligence/decision-escalation.js +36 -0
- package/dist/intelligence/decision-memory.js +47 -0
- package/dist/intelligence/decision-similarity.js +85 -0
- package/dist/intelligence/default-safety-tests.js +53 -0
- package/dist/intelligence/degradation-watcher.js +108 -0
- package/dist/intelligence/detector-plugins.js +70 -0
- package/dist/intelligence/detectors.js +1 -0
- package/dist/intelligence/device-auth.js +138 -0
- package/dist/intelligence/diff-size.js +42 -0
- package/dist/intelligence/ebpf-verify.js +45 -0
- package/dist/intelligence/enterprise-policy-engine.js +1122 -0
- package/dist/intelligence/error-fingerprint.js +1 -0
- package/dist/intelligence/execution-gate.js +100 -0
- package/dist/intelligence/execution-mode.js +22 -0
- package/dist/intelligence/gate-analytics.js +256 -0
- package/dist/intelligence/gate-decision.js +64 -0
- package/dist/intelligence/gcp-broker.js +50 -0
- package/dist/intelligence/git-adr-sync.js +207 -0
- package/dist/intelligence/hitl-hold.js +460 -0
- package/dist/intelligence/hypothesis-history.js +93 -0
- package/dist/intelligence/idp-identity.js +83 -0
- package/dist/intelligence/impl-critic.js +216 -0
- package/dist/intelligence/incident-autopilot.js +622 -0
- package/dist/intelligence/incident-replay.js +162 -0
- package/dist/intelligence/incident-result-cache.js +33 -0
- package/dist/intelligence/incident-similarity.js +58 -0
- package/dist/intelligence/infra-detectors.js +312 -0
- package/dist/intelligence/license.js +185 -0
- package/dist/intelligence/llm-spokesperson.js +62 -0
- package/dist/intelligence/mcp-prompts.js +320 -0
- package/dist/intelligence/mcp-resources.js +171 -0
- package/dist/intelligence/normalize.js +108 -0
- package/dist/intelligence/notifications.js +83 -0
- package/dist/intelligence/oidc-issuer.js +87 -0
- package/dist/intelligence/override-corpus.js +597 -0
- package/dist/intelligence/planning-gate.js +84 -0
- package/dist/intelligence/plans.js +223 -0
- package/dist/intelligence/platt-scaling.js +164 -0
- package/dist/intelligence/policy-proposals.js +84 -0
- package/dist/intelligence/policy-suggester.js +161 -0
- package/dist/intelligence/policy-sync.js +103 -0
- package/dist/intelligence/policy-test-runner.js +35 -0
- package/dist/intelligence/postmortem-parser.js +181 -0
- package/dist/intelligence/postmortem-retrieval.js +224 -0
- package/dist/intelligence/postmortem-store.js +596 -0
- package/dist/intelligence/pr-commenter.js +81 -0
- package/dist/intelligence/pr-policy-gate.js +38 -0
- package/dist/intelligence/pr-shadow-analyzer.js +229 -0
- package/dist/intelligence/process-tree.js +108 -0
- package/dist/intelligence/prompts.js +1 -0
- package/dist/intelligence/replay.js +38 -0
- package/dist/intelligence/repro-steps.js +1 -0
- package/dist/intelligence/resource-extractors.js +0 -0
- package/dist/intelligence/resource-file-context.js +138 -0
- package/dist/intelligence/risk-score.js +22 -0
- package/dist/intelligence/rollback.js +137 -0
- package/dist/intelligence/runbook-updater.js +143 -0
- package/dist/intelligence/sandbox.js +194 -0
- package/dist/intelligence/script-trust.js +104 -0
- package/dist/intelligence/session-metrics.js +67 -0
- package/dist/intelligence/session-threat-tracker.js +231 -0
- package/dist/intelligence/shadow-digest-cron.js +81 -0
- package/dist/intelligence/shadow-log.js +284 -0
- package/dist/intelligence/shell-ast.js +145 -0
- package/dist/intelligence/siem-forward.js +104 -0
- package/dist/intelligence/slack-digest.js +151 -0
- package/dist/intelligence/slack-override-loop.js +217 -0
- package/dist/intelligence/slack-routing.js +67 -0
- package/dist/intelligence/slack.js +900 -0
- package/dist/intelligence/sql-ast.js +31 -0
- package/dist/intelligence/team.js +1 -0
- package/dist/intelligence/telemetry.js +76 -0
- package/dist/intelligence/threshold-optimizer.js +75 -0
- package/dist/intelligence/token-budget.js +66 -0
- package/dist/intelligence/tool-guard.js +708 -0
- package/dist/intelligence/tool-manifest 2.js +74 -0
- package/dist/intelligence/tool-manifest.js +125 -0
- package/dist/intelligence/tools-analysis.js +857 -0
- package/dist/intelligence/tools-arch.js +152 -0
- package/dist/intelligence/tools-autonomy.js +495 -0
- package/dist/intelligence/tools-blast-radius.js +140 -0
- package/dist/intelligence/tools-browser.js +431 -0
- package/dist/intelligence/tools-change-timeline.js +154 -0
- package/dist/intelligence/tools-credentials.js +117 -0
- package/dist/intelligence/tools-datadog.js +233 -0
- package/dist/intelligence/tools-debug-sessions.js +209 -0
- package/dist/intelligence/tools-discovery.js +284 -0
- package/dist/intelligence/tools-infra.js +612 -0
- package/dist/intelligence/tools-intent.js +128 -0
- package/dist/intelligence/tools-memory.js +514 -0
- package/dist/intelligence/tools-runbook.js +951 -0
- package/dist/intelligence/tools-sessions.js +107 -0
- package/dist/intelligence/tools-state.js +232 -0
- package/dist/intelligence/tools-utility.js +430 -0
- package/dist/intelligence/tools-validate.js +215 -0
- package/dist/intelligence/tools.js +1 -0
- package/dist/intelligence/trace-context.js +11 -0
- package/dist/intelligence/unclassified-clusters.js +87 -0
- package/dist/intelligence/usage.js +195 -0
- package/dist/routes/active-authors.js +63 -0
- package/dist/routes/activity-feed.js +21 -0
- package/dist/routes/adr.js +117 -0
- package/dist/routes/agent-activity.js +184 -0
- package/dist/routes/agent-blunders.js +163 -0
- package/dist/routes/agents.js +175 -0
- package/dist/routes/api-keys.js +65 -0
- package/dist/routes/arch.js +75 -0
- package/dist/routes/audit-export.js +93 -0
- package/dist/routes/billing-dashboard.js +158 -0
- package/dist/routes/billing-outcome.js +77 -0
- package/dist/routes/calibration.js +305 -0
- package/dist/routes/ci-gate-history.js +20 -0
- package/dist/routes/ci-gate.js +232 -0
- package/dist/routes/ci.js +293 -0
- package/dist/routes/compliance-report.js +24 -0
- package/dist/routes/confidence.js +30 -0
- package/dist/routes/credentials.js +119 -0
- package/dist/routes/dashboard.js +1407 -0
- package/dist/routes/decision-memory.js +42 -0
- package/dist/routes/demo.js +844 -0
- package/dist/routes/device-auth-stub.js +173 -0
- package/dist/routes/explain-why.js +39 -0
- package/dist/routes/gate-analytics.js +213 -0
- package/dist/routes/gate.js +28 -0
- package/dist/routes/github-webhook.js +364 -0
- package/dist/routes/habituation.js +30 -0
- package/dist/routes/health-integrations.js +243 -0
- package/dist/routes/heartbeats.js +45 -0
- package/dist/routes/hitl.js +364 -0
- package/dist/routes/impact-report.js +645 -0
- package/dist/routes/incident-webhook.js +63 -0
- package/dist/routes/incidents.js +366 -0
- package/dist/routes/layers.js +62 -0
- package/dist/routes/license.js +187 -0
- package/dist/routes/oidc-issuer.js +30 -0
- package/dist/routes/onboarding.js +170 -0
- package/dist/routes/otel.js +67 -0
- package/dist/routes/otlp-receiver.js +179 -0
- package/dist/routes/overrides.js +241 -0
- package/dist/routes/pagerduty.js +258 -0
- package/dist/routes/policies.js +809 -0
- package/dist/routes/policy-nl.js +169 -0
- package/dist/routes/postmortem.js +102 -0
- package/dist/routes/pr-shadow.js +47 -0
- package/dist/routes/rbac.js +61 -0
- package/dist/routes/replay.js +17 -0
- package/dist/routes/risk-report.js +164 -0
- package/dist/routes/runbooks.js +125 -0
- package/dist/routes/safety-test.js +174 -0
- package/dist/routes/sdk.js +222 -0
- package/dist/routes/sensor.js +819 -0
- package/dist/routes/sentry.js +140 -0
- package/dist/routes/sessions.js +43 -0
- package/dist/routes/setup-ui.js +594 -0
- package/dist/routes/shadow-report.js +107 -0
- package/dist/routes/slack-routing.js +171 -0
- package/dist/routes/team-usage.js +71 -0
- package/dist/routes/telemetry.js +28 -0
- package/dist/routes/tenants.js +199 -0
- package/dist/routes/tickets.js +167 -0
- package/dist/routes/validate.js +13 -0
- package/dist/routes/war-room.js +113 -0
- package/dist/scripts/check-arch.js +22 -0
- package/dist/seeds/community-corpus.js +176 -0
- package/dist/seeds/corpus.js +1224 -0
- package/dist/sensor/action-ledger.js +327 -0
- package/dist/sensor/adr-store.js +140 -0
- package/dist/sensor/agent-blunder-store.js +370 -0
- package/dist/sensor/agent-context-store.js +225 -0
- package/dist/sensor/agent-memory-store.js +231 -0
- package/dist/sensor/audit-fetch.js +23 -0
- package/dist/sensor/audit-log.js +273 -0
- package/dist/sensor/buffer-schemas.js +225 -0
- package/dist/sensor/buffer.js +599 -0
- package/dist/sensor/bypass-tracker.js +93 -0
- package/dist/sensor/ci-gate-history.js +70 -0
- package/dist/sensor/cloud-auth.js +183 -0
- package/dist/sensor/commit-context-store.js +235 -0
- package/dist/sensor/docker-log-stream.js +164 -0
- package/dist/sensor/docker-monitor.js +122 -0
- package/dist/sensor/extended-buffer.js +46 -0
- package/dist/sensor/feedback-token.js +41 -0
- package/dist/sensor/file-lock.js +6 -0
- package/dist/sensor/fs-watcher.js +106 -0
- package/dist/sensor/gate-heartbeat.js +122 -0
- package/dist/sensor/git-suspect.js +136 -0
- package/dist/sensor/habituation-store.js +77 -0
- package/dist/sensor/heartbeat-monitor.js +133 -0
- package/dist/sensor/incident-store.js +340 -0
- package/dist/sensor/infra-normalizer.js +513 -0
- package/dist/sensor/ingest.js +339 -0
- package/dist/sensor/jest-reporter.js +52 -0
- package/dist/sensor/k8s-events.js +132 -0
- package/dist/sensor/layer2-store.js +111 -0
- package/dist/sensor/layer3-store.js +230 -0
- package/dist/sensor/layer4-store.js +147 -0
- package/dist/sensor/logger.js +13 -0
- package/dist/sensor/otel-exporter.js +161 -0
- package/dist/sensor/paths.js +71 -0
- package/dist/sensor/policy-history.js +147 -0
- package/dist/sensor/pr-shadow-store.js +83 -0
- package/dist/sensor/process-watcher.js +162 -0
- package/dist/sensor/rbac.js +92 -0
- package/dist/sensor/redact.js +114 -0
- package/dist/sensor/redis-store.js +191 -0
- package/dist/sensor/route-reachability.js +64 -0
- package/dist/sensor/security-utils.js +18 -0
- package/dist/sensor/service-graph.js +170 -0
- package/dist/sensor/service-topology.js +227 -0
- package/dist/sensor/session-history.js +74 -0
- package/dist/sensor/session-persist.js +38 -0
- package/dist/sensor/shadow-promote.js +122 -0
- package/dist/sensor/sourcemap.js +281 -0
- package/dist/sensor/sqlite-store.js +205 -0
- package/dist/sensor/sso.js +164 -0
- package/dist/sensor/vitest-reporter.js +65 -0
- package/dist/sensor/watcher.js +48 -0
- package/dist/storage/interfaces.js +0 -0
- package/dist/storage/pg/pg-action-ledger.js +138 -0
- package/dist/storage/pg/pg-approval-store.js +107 -0
- package/dist/storage/pg/pg-blunder-store.js +193 -0
- package/dist/storage/pg/pg-ci-gate-history.js +83 -0
- package/dist/storage/pg/pg-client.js +24 -0
- package/dist/storage/pg/pg-event-store.js +63 -0
- package/dist/storage/pg/pg-incident-store.js +190 -0
- package/dist/storage/pg/pg-migrations.js +26 -0
- package/dist/storage/pg/pg-override-corpus.js +447 -0
- package/dist/storage/pg/pg-shadow-log.js +129 -0
- package/dist/storage/sqlite/sqlite-action-ledger.js +18 -0
- package/dist/storage/sqlite/sqlite-approval-store.js +37 -0
- package/dist/storage/sqlite/sqlite-blunder-store.js +27 -0
- package/dist/storage/sqlite/sqlite-ci-gate-history.js +19 -0
- package/dist/storage/sqlite/sqlite-event-store.js +29 -0
- package/dist/storage/sqlite/sqlite-incident-store.js +34 -0
- package/dist/storage/sqlite/sqlite-override-corpus.js +75 -0
- package/dist/storage/sqlite/sqlite-shadow-log.js +36 -0
- package/dist/storage/store-factory.js +55 -0
- package/dist/storage/store-registry.js +31 -0
- package/dist/update-checker.js +107 -0
- package/dist/workers/autopilot-worker.js +28 -0
- package/dist/workers/notification-worker.js +28 -0
- package/dist/workers/queues.js +58 -0
- package/dist/workers/validation-worker.js +24 -0
- package/dist/workers/worker-registry.js +18 -0
- package/package.json +123 -0
- package/sdk/mergen-inject.js +260 -0
- package/sdk/node.js +313 -0
- package/sdk/vite-plugin.ts +57 -0
- package/sdk/webpack-plugin.js +106 -0
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { postmortemStore } from "./postmortem-store.js";
|
|
3
|
+
import { hybridSearch, tagToQuery } from "./postmortem-retrieval.js";
|
|
4
|
+
import { trackCall, withTierGate } from "./tools-state.js";
|
|
5
|
+
import { getTierForTool } from "./tool-manifest.js";
|
|
6
|
+
import { store } from "../sensor/buffer.js";
|
|
7
|
+
import { incidentStore } from "../sensor/incident-store.js";
|
|
8
|
+
import { detectAiCommit } from "./ai-commit.js";
|
|
9
|
+
import { commitContextStore } from "../sensor/commit-context-store.js";
|
|
10
|
+
import { hypothesisHistory } from "./hypothesis-history.js";
|
|
11
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
function fmtMs(ms) {
|
|
16
|
+
if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
|
|
17
|
+
const min = Math.floor(ms / 6e4);
|
|
18
|
+
const sec = Math.round(ms % 6e4 / 1e3);
|
|
19
|
+
return sec > 0 ? `${min}m ${sec}s` : `${min}m`;
|
|
20
|
+
}
|
|
21
|
+
function parseRunbookYaml(yaml) {
|
|
22
|
+
const steps = [];
|
|
23
|
+
let currentStep = null;
|
|
24
|
+
let inParams = false;
|
|
25
|
+
let name = "";
|
|
26
|
+
let description = "";
|
|
27
|
+
const lines = yaml.split("\n");
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
const trimmed = line.trim();
|
|
30
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
31
|
+
if (currentStep === null) {
|
|
32
|
+
if (line.startsWith("name:")) {
|
|
33
|
+
name = line.replace("name:", "").trim();
|
|
34
|
+
if (name.startsWith('"') && name.endsWith('"') || name.startsWith("'") && name.endsWith("'")) {
|
|
35
|
+
name = name.slice(1, -1);
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (line.startsWith("description:")) {
|
|
40
|
+
description = line.replace("description:", "").trim();
|
|
41
|
+
if (description.startsWith('"') && description.endsWith('"') || description.startsWith("'") && description.endsWith("'")) {
|
|
42
|
+
description = description.slice(1, -1);
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (line.match(/^\s*-\s*name:/)) {
|
|
48
|
+
let stepName = line.replace(/^\s*-\s*name:/, "").trim();
|
|
49
|
+
if (stepName.startsWith('"') && stepName.endsWith('"') || stepName.startsWith("'") && stepName.endsWith("'")) {
|
|
50
|
+
stepName = stepName.slice(1, -1);
|
|
51
|
+
}
|
|
52
|
+
currentStep = {
|
|
53
|
+
step: stepName,
|
|
54
|
+
tool: "",
|
|
55
|
+
params: {}
|
|
56
|
+
};
|
|
57
|
+
steps.push(currentStep);
|
|
58
|
+
inParams = false;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (currentStep) {
|
|
62
|
+
if (line.match(/^\s*tool:/)) {
|
|
63
|
+
let toolName = line.replace(/^\s*tool:/, "").trim();
|
|
64
|
+
if (toolName.startsWith('"') && toolName.endsWith('"') || toolName.startsWith("'") && toolName.endsWith("'")) {
|
|
65
|
+
toolName = toolName.slice(1, -1);
|
|
66
|
+
}
|
|
67
|
+
currentStep.tool = toolName;
|
|
68
|
+
inParams = false;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (line.match(/^\s*params:/)) {
|
|
72
|
+
inParams = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (inParams) {
|
|
76
|
+
const kvMatch = line.match(/^\s*([a-zA-Z0-9_-]+)\s*:\s*(.+)$/);
|
|
77
|
+
if (kvMatch) {
|
|
78
|
+
const [, k, rawVal] = kvMatch;
|
|
79
|
+
let valStr = rawVal.trim();
|
|
80
|
+
if (valStr.startsWith('"') && valStr.endsWith('"') || valStr.startsWith("'") && valStr.endsWith("'")) {
|
|
81
|
+
valStr = valStr.slice(1, -1);
|
|
82
|
+
}
|
|
83
|
+
let val = valStr;
|
|
84
|
+
if (valStr === "true") val = true;
|
|
85
|
+
else if (valStr === "false") val = false;
|
|
86
|
+
else if (valStr === "null") val = null;
|
|
87
|
+
else if (!isNaN(Number(valStr))) val = Number(valStr);
|
|
88
|
+
currentStep.params[k] = val;
|
|
89
|
+
} else {
|
|
90
|
+
const indentMatch = line.match(/^(\s*)/);
|
|
91
|
+
const indent = indentMatch ? indentMatch[0].length : 0;
|
|
92
|
+
if (indent < 6) {
|
|
93
|
+
inParams = false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { name, description, steps };
|
|
100
|
+
}
|
|
101
|
+
function registerRunbookTools(server) {
|
|
102
|
+
server.registerTool(
|
|
103
|
+
"check_fix_history",
|
|
104
|
+
{
|
|
105
|
+
description: "Before running a fix, check whether Mergen has seen it before and whether it worked. Prevents repeated mistakes: if this command previously caused a REGRESSION or has a low resolution rate, the tool surfaces that before execution. Pass the exact command you are about to run (command) or a free-text description (description). Returns: resolution rate, avg MTTR when successful, per-service breakdown, and nearest corpus alternatives if the command is not found. Always call this before execute_fix or before proposing a fix to the user.",
|
|
106
|
+
inputSchema: {
|
|
107
|
+
command: z.string().optional().describe('The shell command you are about to run (e.g. "kubectl rollout restart deployment/api"). Fuzzy-matched against fix history.'),
|
|
108
|
+
description: z.string().optional().describe('Free-text description of what the fix does (e.g. "restart the api pod"). Used for hybrid search when exact command is unknown.'),
|
|
109
|
+
service: z.string().optional().describe("Narrow results to a specific service.")
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
async ({ command, description, service }) => {
|
|
113
|
+
trackCall("check_fix_history");
|
|
114
|
+
if (!command && !description) {
|
|
115
|
+
return {
|
|
116
|
+
content: [{
|
|
117
|
+
type: "text",
|
|
118
|
+
text: "Provide either `command` (the exact fix to check) or `description` (what the fix does)."
|
|
119
|
+
}]
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (command) {
|
|
123
|
+
const rows = postmortemStore.lookupFixHistory(command, service);
|
|
124
|
+
if (rows.length > 0) {
|
|
125
|
+
const totalApplied = rows.reduce((s, r) => s + r.timesApplied, 0);
|
|
126
|
+
const totalResolved = rows.reduce((s, r) => s + r.timesResolved, 0);
|
|
127
|
+
const resolutionRate = totalApplied > 0 ? Math.round(totalResolved / totalApplied * 100) : 0;
|
|
128
|
+
const verdict = resolutionRate >= 80 ? "\u2705 HIGH CONFIDENCE" : resolutionRate >= 50 ? "\u26A0\uFE0F MIXED RESULTS" : "\u274C LOW SUCCESS RATE";
|
|
129
|
+
const tableRows = rows.map((r) => {
|
|
130
|
+
const mttr = r.avgMttrMs != null ? fmtMs(r.avgMttrMs) : "\u2014";
|
|
131
|
+
const rate = r.timesApplied > 0 ? Math.round(r.timesResolved / r.timesApplied * 100) : 0;
|
|
132
|
+
const last = new Date(r.lastUsedAt).toISOString().slice(0, 10);
|
|
133
|
+
return `| ${r.service} | ${r.timesApplied} | ${rate}% | ${mttr} | ${last} |`;
|
|
134
|
+
});
|
|
135
|
+
const lines2 = [
|
|
136
|
+
`## Fix History: \`${command.slice(0, 80)}${command.length > 80 ? "\u2026" : ""}\``,
|
|
137
|
+
"",
|
|
138
|
+
`**Found in corpus:** ${totalApplied} application${totalApplied !== 1 ? "s" : ""} across ${rows.length} service${rows.length !== 1 ? "s" : ""}`,
|
|
139
|
+
"",
|
|
140
|
+
"| Service | Applied | Resolved | Avg MTTR | Last used |",
|
|
141
|
+
"|---------|---------|----------|----------|-----------|",
|
|
142
|
+
...tableRows,
|
|
143
|
+
"",
|
|
144
|
+
`${verdict} \u2014 ${resolutionRate}% overall resolution rate`,
|
|
145
|
+
"",
|
|
146
|
+
resolutionRate >= 80 ? "_This fix has a strong track record. Proceed with `execute_fix`._" : resolutionRate >= 50 ? "_Mixed results. Review the per-service breakdown before proceeding._" : "_Poor track record. Consider `triage_incident` for an alternative diagnosis._"
|
|
147
|
+
];
|
|
148
|
+
return { content: [{ type: "text", text: lines2.join("\n") }] };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const searchQuery = command ?? description ?? "";
|
|
152
|
+
const related = hybridSearch(searchQuery, { service, topK: 5 });
|
|
153
|
+
if (related.length === 0) {
|
|
154
|
+
const corpusSize = postmortemStore.count();
|
|
155
|
+
return {
|
|
156
|
+
content: [{
|
|
157
|
+
type: "text",
|
|
158
|
+
text: [
|
|
159
|
+
`## Fix History: \`${searchQuery.slice(0, 80)}\``,
|
|
160
|
+
"",
|
|
161
|
+
command ? `\u26A0\uFE0F **Not found in corpus.** No previous applications of this command recorded.` : `\u26A0\uFE0F **No matching incidents found** for: "${searchQuery}"`,
|
|
162
|
+
"",
|
|
163
|
+
corpusSize === 0 ? "_Corpus is empty \u2014 no incident history yet. Proceed with caution and validate after applying._" : `_Corpus has ${corpusSize} postmortem${corpusSize !== 1 ? "s" : ""} but none matched. Consider \`triage_incident\` for automated root cause analysis first._`
|
|
164
|
+
].join("\n")
|
|
165
|
+
}]
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const altLines = related.slice(0, 3).map((r, i) => {
|
|
169
|
+
const pm = r.postmortem;
|
|
170
|
+
const fix = pm.fixCommand ? `\`${pm.fixCommand.slice(0, 70)}${pm.fixCommand.length > 70 ? "\u2026" : ""}\`` : "_no fix recorded_";
|
|
171
|
+
const mttr = pm.mttrMs != null ? fmtMs(pm.mttrMs) : "\u2014";
|
|
172
|
+
const date = new Date(pm.generatedAt).toISOString().slice(0, 10);
|
|
173
|
+
return [
|
|
174
|
+
`**${i + 1}. ${pm.tag.replace(/^infra_/, "")}** \u2014 ${pm.service} (${date})`,
|
|
175
|
+
`Fix: ${fix} | MTTR: ${mttr} | ${pm.resolvedAutonomously ? "\u{1F916} autonomous" : "\u{1F464} manual"}`
|
|
176
|
+
].join("\n");
|
|
177
|
+
});
|
|
178
|
+
const lines = [
|
|
179
|
+
`## Fix History: \`${searchQuery.slice(0, 80)}\``,
|
|
180
|
+
"",
|
|
181
|
+
command ? `\u26A0\uFE0F **Not found in corpus.** No exact match \u2014 showing nearest related fixes:` : `**Nearest corpus fixes for:** "${searchQuery}"`,
|
|
182
|
+
"",
|
|
183
|
+
...altLines.flatMap((l) => [l, ""]),
|
|
184
|
+
"---",
|
|
185
|
+
"_If none of these apply, call `triage_incident` for a fresh causal analysis._"
|
|
186
|
+
];
|
|
187
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
server.registerTool(
|
|
191
|
+
"explain_service",
|
|
192
|
+
{
|
|
193
|
+
description: "Returns a deterministic, token-efficient briefing card for a service: top failure modes ranked by frequency, avg MTTR per mode, autonomous resolution rate, verified fix commands ranked by usage, co-occurring services, and current open incident count. Built from the local incident corpus \u2014 no LLM calls, no hallucinated topology. Primary use case: new engineer joins on-call rotation and calls this before their first page. Also used by AI agents to ground themselves before triaging \u2014 prevents fabricated service boundaries.",
|
|
194
|
+
inputSchema: {
|
|
195
|
+
service: z.string().describe('Service or component name (e.g. "api", "checkout-api", "payments-worker").'),
|
|
196
|
+
limit: z.number().int().min(1).max(10).optional().describe("Max failure modes to return (default: 5).")
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
async ({ service, limit = 5 }) => {
|
|
200
|
+
trackCall("explain_service");
|
|
201
|
+
let profile = postmortemStore.serviceProfile(service);
|
|
202
|
+
let resolvedName = service;
|
|
203
|
+
if (profile.totalIncidents === 0) {
|
|
204
|
+
const fuzzy = postmortemStore.fuzzyService(service);
|
|
205
|
+
if (fuzzy) {
|
|
206
|
+
resolvedName = fuzzy;
|
|
207
|
+
profile = postmortemStore.serviceProfile(fuzzy);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (profile.totalIncidents === 0) {
|
|
211
|
+
const allStats = postmortemStore.tagStats();
|
|
212
|
+
const knownServices = postmortemStore.knownServices(10);
|
|
213
|
+
return {
|
|
214
|
+
content: [{
|
|
215
|
+
type: "text",
|
|
216
|
+
text: [
|
|
217
|
+
`## Service Profile: ${service}`,
|
|
218
|
+
"",
|
|
219
|
+
"_No incidents found for this service in the Mergen corpus._",
|
|
220
|
+
"",
|
|
221
|
+
knownServices.length > 0 ? `**Known services in corpus:** ${knownServices.join(", ")}` : `**Corpus is empty.** Incidents are recorded automatically as they are triaged.`,
|
|
222
|
+
"",
|
|
223
|
+
"Once incidents are resolved, this tool returns:",
|
|
224
|
+
"- Top failure modes by frequency",
|
|
225
|
+
"- Average MTTR per failure mode",
|
|
226
|
+
"- Verified fix commands ranked by usage",
|
|
227
|
+
"- Co-occurring services",
|
|
228
|
+
"",
|
|
229
|
+
allStats.length > 0 ? `_Tip: ${allStats.length} failure mode${allStats.length !== 1 ? "s" : ""} exist across all services \u2014 call \`generate_runbook\` with one of: ${allStats.slice(0, 3).map((s) => `\`${s.tag.replace(/^infra_/, "")}\``).join(", ")}_` : `_Run \`mergen-server demo\` to seed 50 real-world incidents and see this tool in action._`
|
|
230
|
+
].join("\n")
|
|
231
|
+
}]
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const coServices = incidentStore.coOccurringServices(resolvedName);
|
|
235
|
+
const openCount = incidentStore.list(void 0, 200).filter(
|
|
236
|
+
(i) => i.service === resolvedName && i.status === "open"
|
|
237
|
+
).length;
|
|
238
|
+
const firstDate = profile.firstSeenAt ? new Date(profile.firstSeenAt).toISOString().slice(0, 10) : "\u2014";
|
|
239
|
+
const lastDate = profile.lastSeenAt ? new Date(profile.lastSeenAt).toISOString().slice(0, 10) : "\u2014";
|
|
240
|
+
const fuzzyNote = resolvedName !== service ? `_No exact match for "${service}" \u2014 showing results for "${resolvedName}"_
|
|
241
|
+
` : "";
|
|
242
|
+
const modes = profile.failureModes.slice(0, limit);
|
|
243
|
+
const modeRows = modes.map((m) => {
|
|
244
|
+
const tag = m.tag.replace(/^infra_/, "");
|
|
245
|
+
const mttr = m.avgMttrMs != null ? fmtMs(m.avgMttrMs) : "\u2014";
|
|
246
|
+
const fix = m.topFixCommand ? `\`${m.topFixCommand.slice(0, 60)}${m.topFixCommand.length > 60 ? "\u2026" : ""}\`` : "\u2014";
|
|
247
|
+
return `| ${tag} | ${m.frequency}\xD7 | ${mttr} | ${m.autonomousRate}% | ${fix} |`;
|
|
248
|
+
});
|
|
249
|
+
const fixLines = profile.topFixCommands.map(
|
|
250
|
+
(f, i) => `${i + 1}. \`${f.command}\` \u2014 applied ${f.timesApplied}\xD7`
|
|
251
|
+
);
|
|
252
|
+
const coLine = coServices.length > 0 ? coServices.map(({ service: svc, count }) => `${svc} (${count})`).join(" \xB7 ") : "_No co-occurring service data yet_";
|
|
253
|
+
const openLine = openCount > 0 ? `\u26A0\uFE0F **${openCount} open incident${openCount !== 1 ? "s" : ""}** currently tracked` : "\u2705 No open incidents";
|
|
254
|
+
const lines = [
|
|
255
|
+
`## Service Profile: ${resolvedName}`,
|
|
256
|
+
`_Mergen corpus \xB7 ${profile.totalIncidents} incident${profile.totalIncidents !== 1 ? "s" : ""} \xB7 First: ${firstDate} \xB7 Last: ${lastDate}_`,
|
|
257
|
+
fuzzyNote,
|
|
258
|
+
openLine,
|
|
259
|
+
"",
|
|
260
|
+
"### Failure Modes",
|
|
261
|
+
"",
|
|
262
|
+
"| Mode | Freq | Avg MTTR | Auto-resolved | Most Recent Verified Fix |",
|
|
263
|
+
"|------|------|----------|---------------|--------------------------|",
|
|
264
|
+
...modeRows,
|
|
265
|
+
""
|
|
266
|
+
];
|
|
267
|
+
if (fixLines.length > 0) {
|
|
268
|
+
lines.push("### Verified Fix Commands (ranked by usage)", "", ...fixLines, "");
|
|
269
|
+
}
|
|
270
|
+
lines.push(
|
|
271
|
+
"### Co-occurring Services",
|
|
272
|
+
"",
|
|
273
|
+
coLine,
|
|
274
|
+
"",
|
|
275
|
+
"---",
|
|
276
|
+
`_Drill down: \`generate_runbook(service: "${resolvedName}")\` \xB7 \`search_postmortems(query: "...", service: "${resolvedName}")\`_`
|
|
277
|
+
);
|
|
278
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
server.registerTool(
|
|
282
|
+
"generate_runbook",
|
|
283
|
+
{
|
|
284
|
+
description: 'Generate a self-updating runbook for a failure mode from your incident corpus. Uses hybrid retrieval (FTS5 BM25 keyword search + TF-IDF embedding, fused via RRF) to find the most relevant past incidents. Synthesizes root cause patterns, verified fix commands, MTTR trends, and step-by-step procedure. Replaces static Confluence/Notion runbooks that decay within weeks. Pass a failure tag (e.g. "db_connection_pool_exhausted") or a free-text description. If no tag is provided, lists all failure modes with corpus coverage.',
|
|
285
|
+
inputSchema: {
|
|
286
|
+
tag: z.string().optional().describe('Failure mode tag (e.g. "infra_db_connection_pool_exhausted"). If omitted, lists all available.'),
|
|
287
|
+
query: z.string().optional().describe('Free-text description to search for (e.g. "database connections timing out"). Used for hybrid retrieval if tag is not given.'),
|
|
288
|
+
service: z.string().optional().describe("Filter to a specific service name."),
|
|
289
|
+
limit: z.number().int().min(1).max(50).optional().describe("Max postmortems to synthesize from (default: 10).")
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
withTierGate(getTierForTool("generate_runbook"), async ({ tag, query, service, limit = 10 }) => {
|
|
293
|
+
trackCall("generate_runbook");
|
|
294
|
+
if (!postmortemStore.isHealthy()) {
|
|
295
|
+
return {
|
|
296
|
+
content: [{
|
|
297
|
+
type: "text",
|
|
298
|
+
text: "\u26A0\uFE0F **Postmortem store is unavailable** (SQLite init failed \u2014 check logs for WASM or file error). Runbooks cannot be generated until the store recovers on next restart."
|
|
299
|
+
}]
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (!tag && !query) {
|
|
303
|
+
const stats = postmortemStore.tagStats();
|
|
304
|
+
if (stats.length === 0) {
|
|
305
|
+
return {
|
|
306
|
+
content: [{
|
|
307
|
+
type: "text",
|
|
308
|
+
text: [
|
|
309
|
+
"## Runbook Corpus \u2014 Empty",
|
|
310
|
+
"",
|
|
311
|
+
"No postmortems have been written yet. The corpus grows automatically as incidents are resolved.",
|
|
312
|
+
"",
|
|
313
|
+
"To seed with 50 real-world incident postmortems run: `mergen-server demo`",
|
|
314
|
+
"Or call `triage_incident` to begin building your own corpus."
|
|
315
|
+
].join("\n")
|
|
316
|
+
}]
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const totalPms = stats.reduce((s, t) => s + t.count, 0);
|
|
320
|
+
const lines2 = [
|
|
321
|
+
"## Runbook Corpus \u2014 Coverage",
|
|
322
|
+
"",
|
|
323
|
+
`**${totalPms} postmortems** across **${stats.length} failure modes**`,
|
|
324
|
+
"",
|
|
325
|
+
"| Failure mode | Incidents | Avg MTTR | Last seen |",
|
|
326
|
+
"|---|---|---|---|"
|
|
327
|
+
];
|
|
328
|
+
for (const s of stats) {
|
|
329
|
+
const displayTag2 = s.tag.replace(/^infra_/, "");
|
|
330
|
+
const mttr = s.avgMttrMs != null ? fmtMs(s.avgMttrMs) : "\u2014";
|
|
331
|
+
const last = new Date(s.lastAt).toISOString().slice(0, 10);
|
|
332
|
+
lines2.push(`| \`${displayTag2}\` | ${s.count} | ${mttr} | ${last} |`);
|
|
333
|
+
}
|
|
334
|
+
lines2.push("", 'Call `generate_runbook(tag: "...")` or `generate_runbook(query: "...")` to synthesize a runbook.');
|
|
335
|
+
return { content: [{ type: "text", text: lines2.join("\n") }] };
|
|
336
|
+
}
|
|
337
|
+
const normalizedTag = tag ? tag.startsWith("infra_") ? tag : `infra_${tag}` : void 0;
|
|
338
|
+
const searchQuery = query ?? (normalizedTag ? tagToQuery(normalizedTag) : "");
|
|
339
|
+
const results = hybridSearch(searchQuery, {
|
|
340
|
+
tag: normalizedTag,
|
|
341
|
+
service,
|
|
342
|
+
topK: limit
|
|
343
|
+
});
|
|
344
|
+
const postmortems = results.length > 0 ? results.map((r) => r.postmortem) : normalizedTag ? postmortemStore.getByTag(normalizedTag, limit) : [];
|
|
345
|
+
if (postmortems.length === 0) {
|
|
346
|
+
return {
|
|
347
|
+
content: [{
|
|
348
|
+
type: "text",
|
|
349
|
+
text: [
|
|
350
|
+
`## Runbook \u2014 ${(normalizedTag ?? query ?? "").replace(/^infra_/, "")}`,
|
|
351
|
+
"",
|
|
352
|
+
"No postmortems found.",
|
|
353
|
+
service ? `No resolved incidents for service \`${service}\` matching this query.` : "",
|
|
354
|
+
"",
|
|
355
|
+
"The runbook auto-generates once the first matching incident is resolved."
|
|
356
|
+
].filter(Boolean).join("\n")
|
|
357
|
+
}]
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
const displayTag = (normalizedTag ?? postmortems[0].tag).replace(/^infra_/, "");
|
|
361
|
+
const totalCount = postmortems.length;
|
|
362
|
+
const autonomousCount = postmortems.filter((pm) => pm.resolvedAutonomously).length;
|
|
363
|
+
const mttrSamples = postmortems.filter((pm) => pm.mttrMs != null).map((pm) => pm.mttrMs);
|
|
364
|
+
const avgMttr = mttrSamples.length > 0 ? mttrSamples.reduce((a, b) => a + b, 0) / mttrSamples.length : null;
|
|
365
|
+
const minMttr = mttrSamples.length > 0 ? Math.min(...mttrSamples) : null;
|
|
366
|
+
const commandCounts = /* @__PURE__ */ new Map();
|
|
367
|
+
for (const pm of postmortems) {
|
|
368
|
+
if (pm.fixCommand) {
|
|
369
|
+
commandCounts.set(pm.fixCommand, (commandCounts.get(pm.fixCommand) ?? 0) + 1);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const topCommands = [...commandCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
373
|
+
const rootCauses = [...new Set(
|
|
374
|
+
postmortems.map((pm) => pm.rootCause.split(".")[0].trim()).filter(Boolean)
|
|
375
|
+
)].slice(0, 3);
|
|
376
|
+
const signalSummary = results.length > 0 ? `_Retrieved via: ${[...new Set(results.flatMap((r) => r.signals))].join(" + ")} \u2192 RRF fusion_` : "";
|
|
377
|
+
const recentRows = postmortems.slice(0, 5).map((pm) => {
|
|
378
|
+
const date = new Date(pm.generatedAt).toISOString().slice(0, 10);
|
|
379
|
+
const mttr = pm.mttrMs != null ? fmtMs(pm.mttrMs) : "\u2014";
|
|
380
|
+
const how = pm.resolvedAutonomously ? "\u{1F916} auto" : "\u{1F464} manual";
|
|
381
|
+
return `| ${date} | ${pm.service} | ${mttr} | ${how} |`;
|
|
382
|
+
});
|
|
383
|
+
const lines = [
|
|
384
|
+
`# Runbook \u2014 ${displayTag}`,
|
|
385
|
+
`_Auto-generated from ${totalCount} incident${totalCount !== 1 ? "s" : ""} \xB7 ${new Date(postmortems[0].generatedAt).toISOString().slice(0, 10)}_`,
|
|
386
|
+
signalSummary,
|
|
387
|
+
"",
|
|
388
|
+
"## Summary",
|
|
389
|
+
"",
|
|
390
|
+
`- **${totalCount}** incidents in corpus`,
|
|
391
|
+
avgMttr != null ? `- **Avg MTTR:** ${fmtMs(avgMttr)}${minMttr != null ? ` (best: ${fmtMs(minMttr)})` : ""}` : "",
|
|
392
|
+
`- **Autonomous resolution rate:** ${Math.round(autonomousCount / totalCount * 100)}%`,
|
|
393
|
+
"",
|
|
394
|
+
"## Root Causes",
|
|
395
|
+
"",
|
|
396
|
+
...rootCauses.map((rc, i) => `${i + 1}. ${rc}`),
|
|
397
|
+
""
|
|
398
|
+
];
|
|
399
|
+
if (topCommands.length > 0) {
|
|
400
|
+
lines.push("## Verified Fix Commands", "");
|
|
401
|
+
lines.push("_Ranked by frequency \u2014 these have resolved this failure mode before:_", "");
|
|
402
|
+
for (const [cmd, cnt] of topCommands) {
|
|
403
|
+
lines.push(`\`\`\`
|
|
404
|
+
${cmd}
|
|
405
|
+
\`\`\``);
|
|
406
|
+
lines.push(`_Applied ${cnt} time${cnt !== 1 ? "s" : ""}_`, "");
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
lines.push(
|
|
410
|
+
"## Procedure",
|
|
411
|
+
"",
|
|
412
|
+
`1. Confirm failure: check logs for \`${displayTag}\` pattern`,
|
|
413
|
+
"2. Run `triage_incident` to get current diagnosis",
|
|
414
|
+
topCommands.length > 0 ? `3. If confidence \u2265 85%: apply \`${topCommands[0][0]}\`` : "3. Apply fix per root cause above",
|
|
415
|
+
"4. Run `validate_fix` to confirm resolution",
|
|
416
|
+
"5. Resolution automatically updates this runbook",
|
|
417
|
+
"",
|
|
418
|
+
"## Recent Incidents",
|
|
419
|
+
"",
|
|
420
|
+
"| Date | Service | MTTR | How |",
|
|
421
|
+
"|---|---|---|---|",
|
|
422
|
+
...recentRows
|
|
423
|
+
);
|
|
424
|
+
return { content: [{ type: "text", text: lines.filter((l) => l !== void 0).join("\n") }] };
|
|
425
|
+
})
|
|
426
|
+
);
|
|
427
|
+
server.registerTool(
|
|
428
|
+
"search_postmortems",
|
|
429
|
+
{
|
|
430
|
+
description: 'Semantic search over the postmortem corpus using hybrid retrieval (FTS5 BM25 + TF-IDF cosine similarity fused via Reciprocal Rank Fusion). Use this to find past incidents similar to the current one before triaging. Returns ranked postmortems with retrieval signal metadata. This is the "token tax" solution: instead of dumping raw logs into context, retrieve the 3-5 most relevant past incidents and include only those.',
|
|
431
|
+
inputSchema: {
|
|
432
|
+
query: z.string().describe(`Describe what you're looking for (e.g. "memory OOM kill in worker service", "rate limit cascade").`),
|
|
433
|
+
service: z.string().optional().describe("Restrict to a specific service."),
|
|
434
|
+
tag: z.string().optional().describe("Restrict to a specific failure mode tag."),
|
|
435
|
+
limit: z.number().int().min(1).max(20).optional().describe("Number of results to return (default: 5).")
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
withTierGate(getTierForTool("search_postmortems"), async ({ query, service, tag, limit = 5 }) => {
|
|
439
|
+
trackCall("search_postmortems");
|
|
440
|
+
if (!postmortemStore.isHealthy()) {
|
|
441
|
+
return {
|
|
442
|
+
content: [{
|
|
443
|
+
type: "text",
|
|
444
|
+
text: "\u26A0\uFE0F **Postmortem store is unavailable** (SQLite init failed). Search cannot run until the store recovers on next restart."
|
|
445
|
+
}]
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
const normalizedTag = tag ? tag.startsWith("infra_") ? tag : `infra_${tag}` : void 0;
|
|
449
|
+
const results = hybridSearch(query, {
|
|
450
|
+
tag: normalizedTag,
|
|
451
|
+
service,
|
|
452
|
+
topK: limit
|
|
453
|
+
});
|
|
454
|
+
if (results.length === 0) {
|
|
455
|
+
const totalInCorpus = postmortemStore.count();
|
|
456
|
+
return {
|
|
457
|
+
content: [{
|
|
458
|
+
type: "text",
|
|
459
|
+
text: totalInCorpus === 0 ? "## No results\n\nCorpus is empty. Resolve an incident with `triage_incident` to start building the corpus." : `## No results for "${query}"
|
|
460
|
+
|
|
461
|
+
Corpus has ${totalInCorpus} postmortem${totalInCorpus !== 1 ? "s" : ""} but none matched. Try a broader query.`
|
|
462
|
+
}]
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
const lines = [
|
|
466
|
+
`## Postmortem Search \u2014 "${query}"`,
|
|
467
|
+
"",
|
|
468
|
+
`_Found ${results.length} result${results.length !== 1 ? "s" : ""} \xB7 hybrid retrieval (FTS5 + TF-IDF \u2192 RRF)_`,
|
|
469
|
+
""
|
|
470
|
+
];
|
|
471
|
+
for (const [i, result] of results.entries()) {
|
|
472
|
+
const { postmortem: pm, score, signals, embeddingSimilarity, keywordRank } = result;
|
|
473
|
+
const date = new Date(pm.generatedAt).toISOString().slice(0, 10);
|
|
474
|
+
const mttr = pm.mttrMs != null ? fmtMs(pm.mttrMs) : "\u2014";
|
|
475
|
+
const signalStr = signals.join("+");
|
|
476
|
+
const simStr = embeddingSimilarity != null ? `sim=${(embeddingSimilarity * 100).toFixed(0)}%` : "";
|
|
477
|
+
const rankStr = keywordRank != null ? `kw-rank=${keywordRank + 1}` : "";
|
|
478
|
+
const meta = [signalStr, simStr, rankStr].filter(Boolean).join(", ");
|
|
479
|
+
lines.push(
|
|
480
|
+
`### ${i + 1}. ${pm.tag.replace(/^infra_/, "")} \u2014 ${pm.service} (${date})`,
|
|
481
|
+
`_RRF score: ${score.toFixed(4)} \xB7 ${meta}_`,
|
|
482
|
+
"",
|
|
483
|
+
`**Root cause:** ${pm.rootCause}`,
|
|
484
|
+
pm.fixCommand ? `**Fix:** \`${pm.fixCommand}\`` : "",
|
|
485
|
+
`**MTTR:** ${mttr} | **Confidence:** ${Math.round(pm.confidence * 100)}% | **Resolution:** ${pm.resolvedAutonomously ? "\u{1F916} autonomous" : "\u{1F464} manual"}`,
|
|
486
|
+
pm.gitBranch ? `**Branch:** ${pm.gitBranch} | **SHA:** ${pm.gitSha ?? "unknown"}` : "",
|
|
487
|
+
""
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
lines.push("---", `_To generate a full runbook: \`generate_runbook(tag: "...")\`_`);
|
|
491
|
+
return { content: [{ type: "text", text: lines.filter((l) => l !== void 0).join("\n") }] };
|
|
492
|
+
})
|
|
493
|
+
);
|
|
494
|
+
server.registerTool(
|
|
495
|
+
"draft_postmortem",
|
|
496
|
+
{
|
|
497
|
+
description: 'Draft a blameless incident postmortem from live telemetry + corpus context. Reconstructs the incident timeline from buffer events, correlates with past similar incidents via hybrid retrieval, and produces a structured Markdown draft ready for team review. Eliminates the post-incident documentation chore \u2014 from "incident closed" to blameless draft in seconds. Outputs: timeline, contributing factors, impact estimate, action items, and related past incidents. Pass a summary of what happened plus the affected service. The draft auto-links to similar corpus incidents.',
|
|
498
|
+
inputSchema: {
|
|
499
|
+
service: z.string().describe('Affected service name (e.g. "api", "payments", "worker").'),
|
|
500
|
+
summary: z.string().optional().describe('One-sentence description of what happened (e.g. "Database connection pool exhausted during peak traffic"). Used to find similar past incidents.'),
|
|
501
|
+
duration_minutes: z.number().int().min(1).max(1440).optional().describe("How long the incident lasted in minutes. Used for impact estimate."),
|
|
502
|
+
severity: z.enum(["sev1", "sev2", "sev3"]).optional().describe("Incident severity (default: sev2)."),
|
|
503
|
+
affected_users: z.string().optional().describe('Who was affected and how (e.g. "all authenticated users, checkout unavailable").'),
|
|
504
|
+
slack_thread: z.string().optional().describe("Slack thread URL or text content to incorporate into the timeline (optional).")
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
withTierGate(getTierForTool("draft_postmortem"), async ({ service, summary, duration_minutes, severity = "sev2", affected_users, slack_thread }) => {
|
|
508
|
+
trackCall("draft_postmortem");
|
|
509
|
+
const markdown = await draftPostmortemDoc({ service, summary, duration_minutes, severity, affected_users, slack_thread });
|
|
510
|
+
return { content: [{ type: "text", text: markdown }] };
|
|
511
|
+
})
|
|
512
|
+
);
|
|
513
|
+
server.registerTool(
|
|
514
|
+
"start_runbook",
|
|
515
|
+
{
|
|
516
|
+
description: "Run a named runbook from ~/.mergen/runbooks/*.yaml \u2014 step-by-step guided incident response. Each step maps to a Mergen tool call. Call with no name to list available runbooks. Built-in runbooks: api-degradation, db-connection-pool, auth-failures. Custom runbooks: add your own YAML files to ~/.mergen/runbooks/. Returns step-by-step results so the AI can walk through the investigation systematically.",
|
|
517
|
+
inputSchema: {
|
|
518
|
+
name: z.string().optional().describe("Runbook name (without .yaml extension). Omit to list all available runbooks."),
|
|
519
|
+
service: z.string().optional().describe('Target service name to substitute into runbook steps (e.g. "api", "payments").')
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
async ({ name, service }) => {
|
|
523
|
+
trackCall("start_runbook");
|
|
524
|
+
const RUNBOOKS_DIR = join(homedir(), ".mergen", "runbooks");
|
|
525
|
+
const BUILTIN_DIR = join(homedir(), ".mergen", "runbooks", "_builtin");
|
|
526
|
+
if (!name) {
|
|
527
|
+
const available = [];
|
|
528
|
+
for (const dir of [BUILTIN_DIR, RUNBOOKS_DIR]) {
|
|
529
|
+
if (!existsSync(dir)) continue;
|
|
530
|
+
const files = await readdir(dir).catch(() => []);
|
|
531
|
+
for (const f of files) {
|
|
532
|
+
if (f.endsWith(".yaml") || f.endsWith(".yml")) {
|
|
533
|
+
available.push(f.replace(/\.ya?ml$/, "") + (dir === BUILTIN_DIR ? " (built-in)" : ""));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (available.length === 0) {
|
|
538
|
+
return { content: [{ type: "text", text: [
|
|
539
|
+
"## Available Runbooks",
|
|
540
|
+
"",
|
|
541
|
+
"No runbooks found. Built-in runbooks are seeded on first `mergen-server setup` run.",
|
|
542
|
+
"",
|
|
543
|
+
"To create one, add a YAML file to ~/.mergen/runbooks/:",
|
|
544
|
+
"",
|
|
545
|
+
"```yaml",
|
|
546
|
+
"name: my-runbook",
|
|
547
|
+
"description: Investigate API latency spikes",
|
|
548
|
+
"steps:",
|
|
549
|
+
" - name: Check recent errors",
|
|
550
|
+
" tool: get_recent_logs",
|
|
551
|
+
" params: { level: error, limit: 50 }",
|
|
552
|
+
" - name: Check network failures",
|
|
553
|
+
" tool: get_network_activity",
|
|
554
|
+
" params: { limit: 30 }",
|
|
555
|
+
" - name: Causal analysis",
|
|
556
|
+
" tool: analyze_runtime",
|
|
557
|
+
"```"
|
|
558
|
+
].join("\n") }] };
|
|
559
|
+
}
|
|
560
|
+
return { content: [{ type: "text", text: `## Available Runbooks
|
|
561
|
+
|
|
562
|
+
${available.map((r) => `- \`${r}\``).join("\n")}
|
|
563
|
+
|
|
564
|
+
Call \`start_runbook(name: "<runbook-name>")\` to execute.` }] };
|
|
565
|
+
}
|
|
566
|
+
let yaml = null;
|
|
567
|
+
for (const dir of [RUNBOOKS_DIR, BUILTIN_DIR]) {
|
|
568
|
+
for (const ext of [".yaml", ".yml"]) {
|
|
569
|
+
const p = join(dir, name + ext);
|
|
570
|
+
if (existsSync(p)) {
|
|
571
|
+
yaml = await readFile(p, "utf-8");
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (yaml) break;
|
|
576
|
+
}
|
|
577
|
+
if (!yaml) {
|
|
578
|
+
return { content: [{ type: "text", text: `Runbook \`${name}\` not found. Call \`start_runbook()\` with no arguments to list available runbooks.` }] };
|
|
579
|
+
}
|
|
580
|
+
const parsed = parseRunbookYaml(yaml);
|
|
581
|
+
const rbName = parsed.name || name;
|
|
582
|
+
const rbDesc = parsed.description;
|
|
583
|
+
const steps = parsed.steps;
|
|
584
|
+
if (service) {
|
|
585
|
+
for (const s of steps) {
|
|
586
|
+
s.params = s.params || {};
|
|
587
|
+
s.params.service = service;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const serviceNote = service ? ` for service **${service}**` : "";
|
|
591
|
+
const output = [
|
|
592
|
+
`## Runbook: ${rbName}${serviceNote}`,
|
|
593
|
+
rbDesc ? `_${rbDesc}_` : "",
|
|
594
|
+
"",
|
|
595
|
+
`**${steps.length} step${steps.length !== 1 ? "s" : ""}:**`,
|
|
596
|
+
"",
|
|
597
|
+
...steps.map((s, i) => {
|
|
598
|
+
const paramStr = Object.keys(s.params).length > 0 ? "(" + Object.entries(s.params).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join(", ") + ")" : "";
|
|
599
|
+
return `${i + 1}. **${s.step}** \u2192 \`${s.tool}${paramStr}\``;
|
|
600
|
+
}),
|
|
601
|
+
"",
|
|
602
|
+
"---",
|
|
603
|
+
"_Execute each step in order. Each tool call returns evidence for the next step._",
|
|
604
|
+
"",
|
|
605
|
+
"**Start with step 1:** call the tool listed above, then proceed to step 2."
|
|
606
|
+
];
|
|
607
|
+
return { content: [{ type: "text", text: output.filter(Boolean).join("\n") }] };
|
|
608
|
+
}
|
|
609
|
+
);
|
|
610
|
+
server.registerTool(
|
|
611
|
+
"suggest_followups",
|
|
612
|
+
{
|
|
613
|
+
description: "After triage or incident resolution: suggest high-value follow-up actions. Reads the latest hypothesis chain and recent incident state to recommend the next steps \u2014 opening a ticket, scheduling a postmortem, updating runbooks, or adding corpus policy. Returns a prioritized list with the exact tool call or command to execute each action. Call this after `triage_incident` or `validate_fix` to close the incident loop.",
|
|
614
|
+
inputSchema: {
|
|
615
|
+
context: z.string().optional().describe("Optional free-text context about what happened (adds to hypothesis history context).")
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
async ({ context }) => {
|
|
619
|
+
trackCall("suggest_followups");
|
|
620
|
+
const latest = hypothesisHistory.latest();
|
|
621
|
+
const openIncidents = incidentStore.list(void 0, 5).filter((i) => i.status === "open" || i.status === "acknowledged");
|
|
622
|
+
const actions = [];
|
|
623
|
+
const tag = latest?.topHypothesis?.tag ?? null;
|
|
624
|
+
const svc = latest?.topHypothesis?.service ?? null;
|
|
625
|
+
const conf = Number(latest?.topHypothesis?.confidence ?? 0);
|
|
626
|
+
const fix = latest?.topHypothesis?.fix ?? null;
|
|
627
|
+
const errors = latest?.chain?.errors ?? [];
|
|
628
|
+
if (fix && conf >= 0.85) {
|
|
629
|
+
actions.push({
|
|
630
|
+
priority: 1,
|
|
631
|
+
emoji: "\u{1F527}",
|
|
632
|
+
action: "Execute the recommended fix",
|
|
633
|
+
tool: `execute_fix(command: "${fix}", confirm: true)`,
|
|
634
|
+
rationale: `Confidence is ${Math.round(conf * 100)}% \u2014 above the 85% autonomous execution threshold.`
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
if (fix && conf >= 0.7) {
|
|
638
|
+
actions.push({
|
|
639
|
+
priority: 2,
|
|
640
|
+
emoji: "\u2705",
|
|
641
|
+
action: "Validate the fix",
|
|
642
|
+
tool: `validate_fix(command: "${fix}")`,
|
|
643
|
+
rationale: "Confirms error rate decreased after fix \u2014 required for corpus update."
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
if (svc || errors.length > 0) {
|
|
647
|
+
const title = tag ? `Investigate ${tag} in ${svc ?? "service"}` : `Post-incident follow-up${svc ? ` \u2014 ${svc}` : ""}`;
|
|
648
|
+
actions.push({
|
|
649
|
+
priority: 3,
|
|
650
|
+
emoji: "\u{1F3AB}",
|
|
651
|
+
action: "Create a tracking ticket",
|
|
652
|
+
tool: `create_ticket(title: "${title}", description: "See Mergen incident context for details.")`,
|
|
653
|
+
rationale: "Ensures incident action items are tracked to completion."
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
if (svc && errors.length >= 5) {
|
|
657
|
+
actions.push({
|
|
658
|
+
priority: 4,
|
|
659
|
+
emoji: "\u{1F4CB}",
|
|
660
|
+
action: "Draft a blameless postmortem",
|
|
661
|
+
tool: `draft_postmortem(service: "${svc}", summary: "${context ?? (tag ? `${tag} failure` : "incident")}")`,
|
|
662
|
+
rationale: `${errors.length} errors observed \u2014 warrants a formal postmortem document.`
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
if (svc) {
|
|
666
|
+
actions.push({
|
|
667
|
+
priority: 5,
|
|
668
|
+
emoji: "\u{1F4D6}",
|
|
669
|
+
action: "Update runbook for this failure mode",
|
|
670
|
+
tool: `generate_runbook(service: "${svc}"${tag ? `, failure_mode: "${tag}"` : ""})`,
|
|
671
|
+
rationale: "Codifies the resolution path so future incidents resolve faster."
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
for (const inc of openIncidents.slice(0, 2)) {
|
|
675
|
+
actions.push({
|
|
676
|
+
priority: 6,
|
|
677
|
+
emoji: "\u{1F6A8}",
|
|
678
|
+
action: `Acknowledge open incident ${inc.pid}`,
|
|
679
|
+
tool: `triage_incident(pid: "${inc.pid}")`,
|
|
680
|
+
rationale: `Incident ${inc.pid} is still ${inc.status} \u2014 may be related.`
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
if (tag) {
|
|
684
|
+
actions.push({
|
|
685
|
+
priority: 7,
|
|
686
|
+
emoji: "\u{1F50D}",
|
|
687
|
+
action: "Search for similar past incidents",
|
|
688
|
+
tool: `find_similar_incidents(query: "${tag}")`,
|
|
689
|
+
rationale: "Recurring patterns need systemic fixes, not just one-off patches."
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
if (actions.length === 0) {
|
|
693
|
+
return { content: [{ type: "text", text: "## Suggested Follow-ups\n\nNo active incident context found. Run `triage_incident` or `analyze_runtime` first to generate context." }] };
|
|
694
|
+
}
|
|
695
|
+
const lines = [
|
|
696
|
+
"## Suggested Follow-ups",
|
|
697
|
+
"",
|
|
698
|
+
"_Prioritized next actions based on current incident state:_",
|
|
699
|
+
"",
|
|
700
|
+
...actions.sort((a, b) => a.priority - b.priority).map((a, i) => [
|
|
701
|
+
`### ${i + 1}. ${a.emoji} ${a.action}`,
|
|
702
|
+
"",
|
|
703
|
+
`**Why:** ${a.rationale}`,
|
|
704
|
+
"",
|
|
705
|
+
a.tool ? `**Call:** \`${a.tool}\`` : a.command ? `**Run:** \`${a.command}\`` : "",
|
|
706
|
+
""
|
|
707
|
+
].filter((l) => l !== void 0).join("\n"))
|
|
708
|
+
];
|
|
709
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
710
|
+
}
|
|
711
|
+
);
|
|
712
|
+
server.registerTool(
|
|
713
|
+
"find_similar_incidents",
|
|
714
|
+
{
|
|
715
|
+
description: "Find past incidents similar to the current failure. Uses hybrid retrieval (BM25 + TF-IDF + RRF fusion) over the postmortem corpus to surface the top matches \u2014 each with its root cause, fix command, and MTTR. Call after `analyze_runtime` to see if this is a recurring pattern and what fixed it before. Returns: similarity score, root cause, fix command, MTTR, and whether it resolved autonomously.",
|
|
716
|
+
inputSchema: {
|
|
717
|
+
query: z.string().optional().describe("Free-text description of the current failure (default: uses latest hypothesis tag)."),
|
|
718
|
+
limit: z.number().int().min(1).max(10).optional().describe("Number of similar incidents to return (default: 3).")
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
withTierGate(getTierForTool("find_similar_incidents"), async ({ query, limit = 3 }) => {
|
|
722
|
+
trackCall("find_similar_incidents");
|
|
723
|
+
const latest = hypothesisHistory.latest();
|
|
724
|
+
const tag = latest?.topHypothesis?.tag ?? null;
|
|
725
|
+
const svc = latest?.topHypothesis?.service ?? null;
|
|
726
|
+
const effectiveQ = query ?? (tag ? tagToQuery(tag) : null);
|
|
727
|
+
if (!effectiveQ) {
|
|
728
|
+
return { content: [{ type: "text", text: "No query provided and no active hypothesis found. Run `analyze_runtime` first or pass a query string." }] };
|
|
729
|
+
}
|
|
730
|
+
const results = await hybridSearch(effectiveQ, { topK: limit });
|
|
731
|
+
if (results.length === 0) {
|
|
732
|
+
return { content: [{ type: "text", text: `## Similar Past Incidents
|
|
733
|
+
|
|
734
|
+
No matches found for query: \`${effectiveQ}\`
|
|
735
|
+
|
|
736
|
+
Corpus size: ${postmortemStore.count()} postmortems.` }] };
|
|
737
|
+
}
|
|
738
|
+
const lines = [
|
|
739
|
+
"## Similar Past Incidents",
|
|
740
|
+
"",
|
|
741
|
+
`_Query: \`${effectiveQ}\`${svc ? ` \xB7 Service: \`${svc}\`` : ""} \xB7 Corpus: ${postmortemStore.count()} postmortems_`,
|
|
742
|
+
""
|
|
743
|
+
];
|
|
744
|
+
for (let i = 0; i < results.length; i++) {
|
|
745
|
+
const r = results[i];
|
|
746
|
+
const pm = r.postmortem;
|
|
747
|
+
const simPct = Math.round((r.score ?? 0) * 100);
|
|
748
|
+
const mttrLabel = pm.mttrMs ? fmtMs(pm.mttrMs) : "unknown";
|
|
749
|
+
const autoLabel = pm.resolvedAutonomously ? "\u{1F916} autonomous" : "\u{1F464} manual";
|
|
750
|
+
lines.push(
|
|
751
|
+
`### ${i + 1}. ${pm.tag ?? pm.service} \u2014 ${simPct}% match`,
|
|
752
|
+
"",
|
|
753
|
+
`| Field | Value |`,
|
|
754
|
+
`|-------|-------|`,
|
|
755
|
+
`| **Service** | ${pm.service ?? "unknown"} |`,
|
|
756
|
+
`| **Root cause** | ${pm.rootCause ?? "_not recorded_"} |`,
|
|
757
|
+
`| **Fix** | ${pm.fixCommand ? `\`${pm.fixCommand}\`` : "_not recorded_"} |`,
|
|
758
|
+
`| **MTTR** | ${mttrLabel} |`,
|
|
759
|
+
`| **Resolution** | ${autoLabel} |`,
|
|
760
|
+
`| **Date** | ${pm.generatedAt ? new Date(pm.generatedAt).toISOString().slice(0, 10) : "unknown"} |`,
|
|
761
|
+
"",
|
|
762
|
+
pm.fixCommand ? `**\u21B3 To apply this fix:** \`execute_fix(command: "${pm.fixCommand}", confirm: true)\`` : "",
|
|
763
|
+
""
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
lines.push("---");
|
|
767
|
+
lines.push('_Call `check_fix_history(command: "<fix>")` before executing any of the above fixes._');
|
|
768
|
+
return { content: [{ type: "text", text: lines.filter((l) => l !== void 0).join("\n") }] };
|
|
769
|
+
})
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
async function draftPostmortemDoc(params) {
|
|
773
|
+
const { service, summary, duration_minutes, severity = "sev2", affected_users, slack_thread } = params;
|
|
774
|
+
const now = Date.now();
|
|
775
|
+
const windowMs = (duration_minutes ?? 60) * 60 * 1e3;
|
|
776
|
+
const windowStart = now - windowMs;
|
|
777
|
+
const errors = store.getLogs(50, "error", windowStart);
|
|
778
|
+
const warns = store.getLogs(20, "warn", windowStart);
|
|
779
|
+
const netFails = store.getNetwork(30, void 0, windowStart).filter((n) => n.status >= 400 || !!n.error);
|
|
780
|
+
const terminal = store.getTerminalOutput(20, void 0, windowStart);
|
|
781
|
+
const searchQuery = summary ?? `${service} incident failure`;
|
|
782
|
+
const relatedPms = hybridSearch(searchQuery, { service, topK: 5 });
|
|
783
|
+
const timelineEvents = [];
|
|
784
|
+
for (const e of errors.slice(0, 10)) {
|
|
785
|
+
const msg = e.args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ").slice(0, 200);
|
|
786
|
+
timelineEvents.push({ ts: e.timestamp, kind: "ERROR", msg });
|
|
787
|
+
}
|
|
788
|
+
for (const n of netFails.slice(0, 10)) {
|
|
789
|
+
timelineEvents.push({
|
|
790
|
+
ts: n.timestamp,
|
|
791
|
+
kind: `HTTP ${n.status || "ERR"}`,
|
|
792
|
+
msg: `${n.method} ${n.url}${n.error ? ` \u2014 ${n.error.slice(0, 100)}` : ""}`
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
for (const t of terminal.slice(0, 5)) {
|
|
796
|
+
if (/error|fatal|panic|oom|killed/i.test(String(t.data ?? ""))) {
|
|
797
|
+
timelineEvents.push({ ts: t.timestamp, kind: "PROCESS", msg: String(t.data ?? "").slice(0, 200) });
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
timelineEvents.sort((a, b) => a.ts - b.ts);
|
|
801
|
+
const errorPatterns = [...new Set(
|
|
802
|
+
errors.map((e) => {
|
|
803
|
+
const msg = String(e.args?.[0] ?? "").slice(0, 120);
|
|
804
|
+
return msg.replace(/\d+/g, "N").replace(/[a-f0-9]{8,}/gi, "<id>");
|
|
805
|
+
})
|
|
806
|
+
)].slice(0, 5);
|
|
807
|
+
const netPatterns = [...new Set(
|
|
808
|
+
netFails.map((n) => {
|
|
809
|
+
let pathname = n.url;
|
|
810
|
+
try {
|
|
811
|
+
pathname = new URL(n.url).pathname;
|
|
812
|
+
} catch {
|
|
813
|
+
}
|
|
814
|
+
return `${n.method} ${pathname} \u2192 ${n.status || "ERR"}`;
|
|
815
|
+
}).slice(0, 5)
|
|
816
|
+
)];
|
|
817
|
+
const userImpact = affected_users ?? "Unknown \u2014 add user impact manually";
|
|
818
|
+
const durationLabel = duration_minutes ? fmtMs(duration_minutes * 60 * 1e3) : "Unknown \u2014 add duration manually";
|
|
819
|
+
const relatedSection = relatedPms.length > 0 ? [
|
|
820
|
+
"## Related Past Incidents",
|
|
821
|
+
"",
|
|
822
|
+
"_Retrieved via hybrid search (FTS5 + TF-IDF \u2192 RRF)_",
|
|
823
|
+
"",
|
|
824
|
+
...relatedPms.slice(0, 3).map((r, i) => {
|
|
825
|
+
const pm = r.postmortem;
|
|
826
|
+
const date = new Date(pm.generatedAt).toISOString().slice(0, 10);
|
|
827
|
+
const mttr = pm.mttrMs != null ? fmtMs(pm.mttrMs) : "\u2014";
|
|
828
|
+
return [
|
|
829
|
+
`### ${i + 1}. ${pm.tag.replace(/^infra_/, "")} \u2014 ${pm.service} (${date})`,
|
|
830
|
+
`**Root cause:** ${pm.rootCause}`,
|
|
831
|
+
pm.fixCommand ? `**Fix that worked:** \`${pm.fixCommand}\`` : "",
|
|
832
|
+
`**MTTR:** ${mttr} | **Resolution:** ${pm.resolvedAutonomously ? "Autonomous" : "Manual"}`,
|
|
833
|
+
""
|
|
834
|
+
].filter(Boolean).join("\n");
|
|
835
|
+
})
|
|
836
|
+
].join("\n") : "";
|
|
837
|
+
const slackSection = slack_thread ? [
|
|
838
|
+
"## Slack Thread Context",
|
|
839
|
+
"",
|
|
840
|
+
"```",
|
|
841
|
+
slack_thread.slice(0, 1e3),
|
|
842
|
+
"```",
|
|
843
|
+
""
|
|
844
|
+
].join("\n") : "";
|
|
845
|
+
const aiCommitsInWindow = [];
|
|
846
|
+
const seenAiShas = /* @__PURE__ */ new Set();
|
|
847
|
+
for (const e of errors) {
|
|
848
|
+
const gs = e.gitSuspect;
|
|
849
|
+
if (!gs || seenAiShas.has(gs.sha)) continue;
|
|
850
|
+
const signal = gs.aiGenerated ? { detected: true, tool: null } : detectAiCommit(gs.summary, gs.author);
|
|
851
|
+
if (signal.detected) {
|
|
852
|
+
seenAiShas.add(gs.sha);
|
|
853
|
+
aiCommitsInWindow.push({ sha: gs.sha, author: gs.author, summary: gs.summary, tool: signal.tool });
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
const deploys = store.getDeployments(20, void 0, windowStart);
|
|
857
|
+
for (const d of deploys) {
|
|
858
|
+
if (!d.actor) continue;
|
|
859
|
+
const signal = detectAiCommit(d.actor, d.actor);
|
|
860
|
+
if (signal.detected && !seenAiShas.has(d.sha)) {
|
|
861
|
+
seenAiShas.add(d.sha);
|
|
862
|
+
aiCommitsInWindow.push({ sha: d.sha.slice(0, 7), author: d.actor, summary: `deployed to ${d.environment}`, tool: signal.tool });
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const prIntentLines = [];
|
|
866
|
+
for (const ac of aiCommitsInWindow) {
|
|
867
|
+
const ctx = commitContextStore.getBySha(ac.sha);
|
|
868
|
+
if (ctx?.prTitle) {
|
|
869
|
+
const issueRefs = ctx.linkedIssues.slice(0, 3).map((i) => i.ref).join(", ");
|
|
870
|
+
const approvers = ctx.approvers.length ? ctx.approvers.join(", ") : "none recorded";
|
|
871
|
+
prIntentLines.push(
|
|
872
|
+
`**PR #${ctx.prNumber ?? "?"}: ${ctx.prTitle}**`,
|
|
873
|
+
`_Author: @${ctx.author ?? ac.author} \xB7 Approved by: ${approvers}${issueRefs ? ` \xB7 Refs: ${issueRefs}` : ""}_`,
|
|
874
|
+
ctx.prBody ? `> ${ctx.prBody.split("\n").filter((l) => l.trim()).slice(0, 2).join(" ").slice(0, 300)}` : "",
|
|
875
|
+
""
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
const aiCommitsSection = aiCommitsInWindow.length > 0 ? [
|
|
880
|
+
"## AI-Generated Commits in Blast Radius",
|
|
881
|
+
"",
|
|
882
|
+
`_${aiCommitsInWindow.length} commit${aiCommitsInWindow.length !== 1 ? "s" : ""} in the incident window were generated by AI coding tools. These have no human author to consult during triage._`,
|
|
883
|
+
"",
|
|
884
|
+
...aiCommitsInWindow.map(
|
|
885
|
+
(c) => `- \`${c.sha.slice(0, 7)}\` **${c.author}**${c.tool ? ` (${c.tool})` : ""} \u2014 ${c.summary.slice(0, 120)}`
|
|
886
|
+
),
|
|
887
|
+
"",
|
|
888
|
+
...prIntentLines.length > 0 ? ["### Captured PR Intent", "", ...prIntentLines] : []
|
|
889
|
+
].join("\n") : "";
|
|
890
|
+
const actionItems = [];
|
|
891
|
+
if (errors.length > 0) actionItems.push(`[ ] Investigate root cause: ${errorPatterns[0] ?? "see errors above"}`);
|
|
892
|
+
if (netFails.length > 0) actionItems.push("[ ] Add retry logic / circuit breaker for failing endpoints");
|
|
893
|
+
if (aiCommitsInWindow.length > 0) actionItems.push(`[ ] Review ${aiCommitsInWindow.length} AI-generated commit${aiCommitsInWindow.length !== 1 ? "s" : ""} in blast radius \u2014 no human author to ask`);
|
|
894
|
+
if (relatedPms.length > 1) actionItems.push(`[ ] Review ${relatedPms.length} similar past incidents \u2014 recurring pattern may need systemic fix`);
|
|
895
|
+
actionItems.push('[ ] Update runbook: `generate_runbook(service: "' + service + '")`');
|
|
896
|
+
actionItems.push("[ ] Schedule blameless retrospective within 5 business days");
|
|
897
|
+
const severityLabel = severity.toUpperCase();
|
|
898
|
+
const dateStr = new Date(windowStart).toISOString().slice(0, 10);
|
|
899
|
+
const lines = [
|
|
900
|
+
`# [${severityLabel}] Incident Postmortem \u2014 ${service}`,
|
|
901
|
+
`_Draft generated by Mergen \xB7 ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", " ")} UTC_`,
|
|
902
|
+
"",
|
|
903
|
+
"## Summary",
|
|
904
|
+
"",
|
|
905
|
+
summary ?? `${service} incident \u2014 add one-sentence summary here`,
|
|
906
|
+
"",
|
|
907
|
+
"## Impact",
|
|
908
|
+
"",
|
|
909
|
+
`- **Service:** ${service}`,
|
|
910
|
+
`- **Severity:** ${severityLabel}`,
|
|
911
|
+
`- **Date:** ${dateStr}`,
|
|
912
|
+
`- **Duration:** ${durationLabel}`,
|
|
913
|
+
`- **Users affected:** ${userImpact}`,
|
|
914
|
+
"",
|
|
915
|
+
"## Timeline",
|
|
916
|
+
"",
|
|
917
|
+
timelineEvents.length > 0 ? timelineEvents.map((e) => {
|
|
918
|
+
const ts = new Date(e.ts).toISOString().slice(11, 19);
|
|
919
|
+
return `- \`${ts}\` **[${e.kind}]** ${e.msg}`;
|
|
920
|
+
}).join("\n") : "_No telemetry events in window \u2014 add timeline manually or extend the window._",
|
|
921
|
+
"",
|
|
922
|
+
slackSection,
|
|
923
|
+
"## Contributing Factors",
|
|
924
|
+
"",
|
|
925
|
+
...errorPatterns.length > 0 ? errorPatterns.map((p, i) => `${i + 1}. ${p}`) : ["_Add contributing factors here_"],
|
|
926
|
+
...netPatterns.length > 0 ? ["", "**Network failures:**", ...netPatterns.map((p) => `- ${p}`)] : [],
|
|
927
|
+
"",
|
|
928
|
+
"## Root Cause",
|
|
929
|
+
"",
|
|
930
|
+
relatedPms.length > 0 ? `_Likely similar to: ${relatedPms[0].postmortem.rootCause} \u2014 verify against current evidence above_` : "_Add root cause analysis here \u2014 call `triage_incident` for automated root cause identification_",
|
|
931
|
+
"",
|
|
932
|
+
"## Resolution",
|
|
933
|
+
"",
|
|
934
|
+
relatedPms.length > 0 && relatedPms[0].postmortem.fixCommand ? `Previous resolution: \`${relatedPms[0].postmortem.fixCommand}\` \u2014 verify if applicable here` : "_Describe the fix that resolved the incident_",
|
|
935
|
+
"",
|
|
936
|
+
aiCommitsSection,
|
|
937
|
+
"## Action Items",
|
|
938
|
+
"",
|
|
939
|
+
...actionItems,
|
|
940
|
+
"",
|
|
941
|
+
relatedSection,
|
|
942
|
+
"---",
|
|
943
|
+
`_Telemetry: ${errors.length} errors, ${warns.length} warnings, ${netFails.length} network failures in window \xB7 Corpus: ${postmortemStore.count()} postmortems_`
|
|
944
|
+
].filter((l) => l !== void 0);
|
|
945
|
+
return lines.join("\n");
|
|
946
|
+
}
|
|
947
|
+
export {
|
|
948
|
+
draftPostmortemDoc,
|
|
949
|
+
parseRunbookYaml,
|
|
950
|
+
registerRunbookTools
|
|
951
|
+
};
|