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,596 @@
|
|
|
1
|
+
import initSqlJs from "sql.js";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { createRequire } from "module";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
import { DATA_DIR, POSTMORTEMS_DB, zeroRetentionMode } from "../sensor/paths.js";
|
|
8
|
+
import logger from "../sensor/logger.js";
|
|
9
|
+
class PostmortemStore {
|
|
10
|
+
db = null;
|
|
11
|
+
/** Whether this sql.js build supports FTS5 — detected at init */
|
|
12
|
+
ftsAvailable = false;
|
|
13
|
+
resolveWasmPath() {
|
|
14
|
+
if (process.env.MERGEN_WASM_PATH) return process.env.MERGEN_WASM_PATH;
|
|
15
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const fromModule = path.resolve(moduleDir, "../../node_modules/sql.js/dist/sql-wasm.wasm");
|
|
17
|
+
if (fs.existsSync(fromModule)) return fromModule;
|
|
18
|
+
try {
|
|
19
|
+
const req = createRequire(import.meta.url);
|
|
20
|
+
const resolved = path.join(path.dirname(req.resolve("sql.js")), "sql-wasm.wasm");
|
|
21
|
+
if (fs.existsSync(resolved)) return resolved;
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
return fromModule;
|
|
25
|
+
}
|
|
26
|
+
async init() {
|
|
27
|
+
if (zeroRetentionMode()) {
|
|
28
|
+
logger.info("postmortem store: zero-retention mode \u2014 no disk writes");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const wasmBinary = fs.readFileSync(this.resolveWasmPath());
|
|
33
|
+
const SQL = await initSqlJs({ wasmBinary });
|
|
34
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
35
|
+
let fileBuffer;
|
|
36
|
+
if (fs.existsSync(POSTMORTEMS_DB)) {
|
|
37
|
+
try {
|
|
38
|
+
fileBuffer = fs.readFileSync(POSTMORTEMS_DB);
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
this.db = fileBuffer ? new SQL.Database(fileBuffer) : new SQL.Database();
|
|
43
|
+
this.db.run(`
|
|
44
|
+
CREATE TABLE IF NOT EXISTS postmortems (
|
|
45
|
+
pid TEXT PRIMARY KEY,
|
|
46
|
+
tag TEXT NOT NULL DEFAULT '',
|
|
47
|
+
service TEXT NOT NULL DEFAULT 'unknown',
|
|
48
|
+
git_sha TEXT,
|
|
49
|
+
git_branch TEXT,
|
|
50
|
+
root_cause TEXT NOT NULL DEFAULT '',
|
|
51
|
+
fix_command TEXT,
|
|
52
|
+
confidence REAL NOT NULL DEFAULT 0,
|
|
53
|
+
mttr_ms INTEGER,
|
|
54
|
+
resolved_autonomously INTEGER NOT NULL DEFAULT 0,
|
|
55
|
+
causally_correct INTEGER NOT NULL DEFAULT 0,
|
|
56
|
+
generated_at INTEGER NOT NULL,
|
|
57
|
+
body TEXT NOT NULL DEFAULT ''
|
|
58
|
+
);
|
|
59
|
+
`);
|
|
60
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS pm_tag_idx ON postmortems (tag);`);
|
|
61
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS pm_gen_idx ON postmortems (generated_at DESC);`);
|
|
62
|
+
try {
|
|
63
|
+
this.db.run(`ALTER TABLE postmortems ADD COLUMN causally_correct INTEGER NOT NULL DEFAULT 0`);
|
|
64
|
+
} catch {
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
this.db.run(`
|
|
68
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS postmortems_fts USING fts5(
|
|
69
|
+
pid UNINDEXED,
|
|
70
|
+
content,
|
|
71
|
+
tokenize = 'porter ascii'
|
|
72
|
+
);
|
|
73
|
+
`);
|
|
74
|
+
this.db.run(`
|
|
75
|
+
INSERT INTO postmortems_fts (pid, content)
|
|
76
|
+
SELECT pm.pid,
|
|
77
|
+
(pm.tag || ' ' || pm.service || ' ' || pm.root_cause ||
|
|
78
|
+
' ' || COALESCE(pm.fix_command, '') || ' ' || pm.body)
|
|
79
|
+
FROM postmortems pm
|
|
80
|
+
WHERE pm.pid NOT IN (SELECT pid FROM postmortems_fts);
|
|
81
|
+
`);
|
|
82
|
+
this.ftsAvailable = true;
|
|
83
|
+
logger.debug("FTS5 index initialised");
|
|
84
|
+
} catch (ftsErr) {
|
|
85
|
+
logger.warn({ ftsErr }, "FTS5 unavailable in this sql.js build \u2014 falling back to LIKE search");
|
|
86
|
+
}
|
|
87
|
+
this._flush();
|
|
88
|
+
logger.info({ path: POSTMORTEMS_DB }, "postmortem store initialised");
|
|
89
|
+
} catch (err) {
|
|
90
|
+
logger.warn({ err }, "postmortem store failed to init \u2014 running without persistence");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
isWriting = false;
|
|
94
|
+
pendingWrite = false;
|
|
95
|
+
nextBuffer = null;
|
|
96
|
+
_flush() {
|
|
97
|
+
if (!this.db || zeroRetentionMode()) return;
|
|
98
|
+
try {
|
|
99
|
+
const data = this.db.export();
|
|
100
|
+
this.writeBufferAsync(Buffer.from(data.buffer, data.byteOffset, data.byteLength));
|
|
101
|
+
} catch (err) {
|
|
102
|
+
logger.warn({ err }, "postmortem store flush failed");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
writeBufferAsync(buf) {
|
|
106
|
+
if (this.isWriting) {
|
|
107
|
+
this.pendingWrite = true;
|
|
108
|
+
this.nextBuffer = buf;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.isWriting = true;
|
|
112
|
+
const tmp = `${POSTMORTEMS_DB}.tmp.${process.pid}`;
|
|
113
|
+
fs.writeFile(tmp, buf, (err) => {
|
|
114
|
+
if (err) {
|
|
115
|
+
logger.warn({ err }, "postmortem store async write failed");
|
|
116
|
+
this.isWriting = false;
|
|
117
|
+
this.processPendingWrite();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
fs.rename(tmp, POSTMORTEMS_DB, (renameErr) => {
|
|
121
|
+
if (renameErr) {
|
|
122
|
+
logger.warn({ err: renameErr }, "postmortem store async rename failed");
|
|
123
|
+
}
|
|
124
|
+
this.isWriting = false;
|
|
125
|
+
this.processPendingWrite();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
processPendingWrite() {
|
|
130
|
+
if (this.pendingWrite && this.nextBuffer) {
|
|
131
|
+
this.pendingWrite = false;
|
|
132
|
+
const buf = this.nextBuffer;
|
|
133
|
+
this.nextBuffer = null;
|
|
134
|
+
this.writeBufferAsync(buf);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
_row(cols, vals) {
|
|
138
|
+
const row = {};
|
|
139
|
+
cols.forEach((c, i) => {
|
|
140
|
+
row[c] = vals[i];
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
pid: String(row.pid ?? ""),
|
|
144
|
+
tag: String(row.tag ?? ""),
|
|
145
|
+
service: String(row.service ?? "unknown"),
|
|
146
|
+
gitSha: row.git_sha ? String(row.git_sha) : null,
|
|
147
|
+
gitBranch: row.git_branch ? String(row.git_branch) : null,
|
|
148
|
+
rootCause: String(row.root_cause ?? ""),
|
|
149
|
+
fixCommand: row.fix_command ? String(row.fix_command) : null,
|
|
150
|
+
confidence: Number(row.confidence ?? 0),
|
|
151
|
+
mttrMs: row.mttr_ms != null ? Number(row.mttr_ms) : null,
|
|
152
|
+
resolvedAutonomously: Boolean(row.resolved_autonomously),
|
|
153
|
+
causallyCorrect: Boolean(row.causally_correct),
|
|
154
|
+
generatedAt: Number(row.generated_at ?? 0),
|
|
155
|
+
body: String(row.body ?? "")
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
write(pm) {
|
|
159
|
+
if (!this.db) return;
|
|
160
|
+
try {
|
|
161
|
+
this.db.run(
|
|
162
|
+
`INSERT OR REPLACE INTO postmortems
|
|
163
|
+
(pid, tag, service, git_sha, git_branch, root_cause, fix_command,
|
|
164
|
+
confidence, mttr_ms, resolved_autonomously, causally_correct, generated_at, body)
|
|
165
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
166
|
+
[
|
|
167
|
+
pm.pid,
|
|
168
|
+
pm.tag,
|
|
169
|
+
pm.service,
|
|
170
|
+
pm.gitSha,
|
|
171
|
+
pm.gitBranch,
|
|
172
|
+
pm.rootCause,
|
|
173
|
+
pm.fixCommand,
|
|
174
|
+
pm.confidence,
|
|
175
|
+
pm.mttrMs ?? null,
|
|
176
|
+
pm.resolvedAutonomously ? 1 : 0,
|
|
177
|
+
pm.causallyCorrect ? 1 : 0,
|
|
178
|
+
pm.generatedAt,
|
|
179
|
+
pm.body
|
|
180
|
+
]
|
|
181
|
+
);
|
|
182
|
+
if (this.ftsAvailable) {
|
|
183
|
+
const content = [pm.tag, pm.service, pm.rootCause, pm.fixCommand ?? "", pm.body].join(" ");
|
|
184
|
+
this.db.run(
|
|
185
|
+
`INSERT OR REPLACE INTO postmortems_fts (pid, content) VALUES (?, ?)`,
|
|
186
|
+
[pm.pid, content]
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
this.invalidateTagStatsCache();
|
|
190
|
+
this._flush();
|
|
191
|
+
logger.debug({ pid: pm.pid, tag: pm.tag }, "postmortem written");
|
|
192
|
+
} catch (err) {
|
|
193
|
+
logger.warn({ err, pid: pm.pid }, "postmortem write failed");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* BM25 keyword search via FTS5. Returns pids ranked by relevance.
|
|
198
|
+
* Falls back to LIKE search when FTS5 is unavailable.
|
|
199
|
+
*/
|
|
200
|
+
keywordSearch(query, limit = 20) {
|
|
201
|
+
if (!this.db) return [];
|
|
202
|
+
const terms = query.replace(/[^\w\s_-]/g, " ").split(/\s+/).filter((t) => t.length >= 2).slice(0, 12);
|
|
203
|
+
if (terms.length === 0) return [];
|
|
204
|
+
if (this.ftsAvailable) {
|
|
205
|
+
try {
|
|
206
|
+
const ftsQuery = terms.join(" OR ");
|
|
207
|
+
const res = this.db.exec(
|
|
208
|
+
`SELECT pid, bm25(postmortems_fts) AS score
|
|
209
|
+
FROM postmortems_fts
|
|
210
|
+
WHERE postmortems_fts MATCH ?
|
|
211
|
+
ORDER BY score
|
|
212
|
+
LIMIT ?`,
|
|
213
|
+
[ftsQuery, limit]
|
|
214
|
+
);
|
|
215
|
+
if (!res[0]?.values) return [];
|
|
216
|
+
return res[0].values.map((v, i) => ({ pid: String(v[0]), rank: i }));
|
|
217
|
+
} catch (e) {
|
|
218
|
+
logger.debug({ e, query }, "FTS5 query failed \u2014 falling through to LIKE");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const like = `%${terms[0]}%`;
|
|
223
|
+
const res = this.db.exec(
|
|
224
|
+
`SELECT pid FROM postmortems
|
|
225
|
+
WHERE root_cause LIKE ? OR body LIKE ? OR tag LIKE ?
|
|
226
|
+
ORDER BY generated_at DESC LIMIT ?`,
|
|
227
|
+
[like, like, like, limit]
|
|
228
|
+
);
|
|
229
|
+
if (!res[0]?.values) return [];
|
|
230
|
+
return res[0].values.map((v, i) => ({ pid: String(v[0]), rank: i }));
|
|
231
|
+
} catch {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
getByTag(tag, limit = 20) {
|
|
236
|
+
if (!this.db) return [];
|
|
237
|
+
try {
|
|
238
|
+
const res = this.db.exec(
|
|
239
|
+
"SELECT * FROM postmortems WHERE tag=? ORDER BY generated_at DESC LIMIT ?",
|
|
240
|
+
[tag, limit]
|
|
241
|
+
);
|
|
242
|
+
if (!res[0]?.values) return [];
|
|
243
|
+
return res[0].values.map((v) => this._row(res[0].columns, v));
|
|
244
|
+
} catch {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
list(limit = 50) {
|
|
249
|
+
if (!this.db) return [];
|
|
250
|
+
try {
|
|
251
|
+
const res = this.db.exec(
|
|
252
|
+
"SELECT * FROM postmortems ORDER BY generated_at DESC LIMIT ?",
|
|
253
|
+
[limit]
|
|
254
|
+
);
|
|
255
|
+
if (!res[0]?.values) return [];
|
|
256
|
+
return res[0].values.map((v) => this._row(res[0].columns, v));
|
|
257
|
+
} catch {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
count() {
|
|
262
|
+
if (!this.db) return 0;
|
|
263
|
+
try {
|
|
264
|
+
const res = this.db.exec("SELECT COUNT(*) FROM postmortems");
|
|
265
|
+
return Number(res[0]?.values?.[0]?.[0] ?? 0);
|
|
266
|
+
} catch {
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
knownServices(limit = 20) {
|
|
271
|
+
if (!this.db) return [];
|
|
272
|
+
try {
|
|
273
|
+
const res = this.db.exec(
|
|
274
|
+
`SELECT DISTINCT service FROM postmortems WHERE service != '' ORDER BY service LIMIT ?`,
|
|
275
|
+
[limit]
|
|
276
|
+
);
|
|
277
|
+
return (res[0]?.values ?? []).map((v) => String(v[0] ?? ""));
|
|
278
|
+
} catch {
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Returns the closest service name in the corpus via LIKE match.
|
|
284
|
+
* Used as a fallback when an exact service name lookup returns nothing —
|
|
285
|
+
* handles "api" vs "api-service" naming drift across teams.
|
|
286
|
+
* Returns null when no match is found.
|
|
287
|
+
*/
|
|
288
|
+
fuzzyService(service) {
|
|
289
|
+
if (!this.db) return null;
|
|
290
|
+
try {
|
|
291
|
+
const res = this.db.exec(
|
|
292
|
+
`SELECT service, COUNT(*) AS cnt
|
|
293
|
+
FROM postmortems
|
|
294
|
+
WHERE service LIKE ?
|
|
295
|
+
GROUP BY service
|
|
296
|
+
ORDER BY cnt DESC
|
|
297
|
+
LIMIT 1`,
|
|
298
|
+
[`%${service}%`]
|
|
299
|
+
);
|
|
300
|
+
const val = res[0]?.values?.[0]?.[0];
|
|
301
|
+
return val ? String(val) : null;
|
|
302
|
+
} catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// 60-second cache for tagStats — called on every validateHypothesis() inside
|
|
307
|
+
// the agent pipeline. Without the cache this is a GROUP BY query per incident.
|
|
308
|
+
_tagStatsCache = null;
|
|
309
|
+
static TAG_STATS_TTL_MS = 6e4;
|
|
310
|
+
/** Tag-level stats for runbook and corpus reporting. Results are cached 60 s. */
|
|
311
|
+
tagStats() {
|
|
312
|
+
const now = Date.now();
|
|
313
|
+
if (this._tagStatsCache && now - this._tagStatsCache.cachedAt < PostmortemStore.TAG_STATS_TTL_MS) {
|
|
314
|
+
return this._tagStatsCache.data;
|
|
315
|
+
}
|
|
316
|
+
const data = this._computeTagStats();
|
|
317
|
+
this._tagStatsCache = { data, cachedAt: now };
|
|
318
|
+
return data;
|
|
319
|
+
}
|
|
320
|
+
invalidateTagStatsCache() {
|
|
321
|
+
this._tagStatsCache = null;
|
|
322
|
+
}
|
|
323
|
+
_computeTagStats() {
|
|
324
|
+
if (!this.db) return [];
|
|
325
|
+
try {
|
|
326
|
+
const res = this.db.exec(`
|
|
327
|
+
SELECT tag, COUNT(*) as cnt, AVG(mttr_ms) as avg_mttr, MAX(generated_at) as last_at
|
|
328
|
+
FROM postmortems
|
|
329
|
+
GROUP BY tag
|
|
330
|
+
ORDER BY cnt DESC
|
|
331
|
+
`);
|
|
332
|
+
if (!res[0]?.values) return [];
|
|
333
|
+
return res[0].values.map((v) => ({
|
|
334
|
+
tag: String(v[0] ?? ""),
|
|
335
|
+
count: Number(v[1] ?? 0),
|
|
336
|
+
avgMttrMs: v[2] != null ? Number(v[2]) : null,
|
|
337
|
+
lastAt: Number(v[3] ?? 0)
|
|
338
|
+
}));
|
|
339
|
+
} catch {
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Look up the resolution history of a specific fix command (or a LIKE pattern).
|
|
345
|
+
* Used by check_fix_history to answer: "has this fix worked before, and how often?"
|
|
346
|
+
*
|
|
347
|
+
* Groups by (fix_command, service) so callers can see per-service success rates
|
|
348
|
+
* for commands that span multiple services (e.g. `kubectl rollout restart`).
|
|
349
|
+
*/
|
|
350
|
+
lookupFixHistory(command, service) {
|
|
351
|
+
if (!this.db) return [];
|
|
352
|
+
try {
|
|
353
|
+
const likePattern = `%${command.replace(/%/g, "\\%").replace(/_/g, "\\_")}%`;
|
|
354
|
+
const sql = service ? `SELECT fix_command, service,
|
|
355
|
+
COUNT(*) AS times_applied,
|
|
356
|
+
SUM(causally_correct) AS times_resolved,
|
|
357
|
+
AVG(mttr_ms) AS avg_mttr,
|
|
358
|
+
MAX(generated_at) AS last_used
|
|
359
|
+
FROM postmortems
|
|
360
|
+
WHERE fix_command LIKE ? AND service = ?
|
|
361
|
+
GROUP BY fix_command, service
|
|
362
|
+
ORDER BY times_applied DESC
|
|
363
|
+
LIMIT 10` : `SELECT fix_command, service,
|
|
364
|
+
COUNT(*) AS times_applied,
|
|
365
|
+
SUM(causally_correct) AS times_resolved,
|
|
366
|
+
AVG(mttr_ms) AS avg_mttr,
|
|
367
|
+
MAX(generated_at) AS last_used
|
|
368
|
+
FROM postmortems
|
|
369
|
+
WHERE fix_command LIKE ?
|
|
370
|
+
GROUP BY fix_command, service
|
|
371
|
+
ORDER BY times_applied DESC
|
|
372
|
+
LIMIT 10`;
|
|
373
|
+
const params = service ? [likePattern, service] : [likePattern];
|
|
374
|
+
const res = this.db.exec(sql, params);
|
|
375
|
+
if (!res[0]?.values) return [];
|
|
376
|
+
return res[0].values.map((v) => ({
|
|
377
|
+
fixCommand: String(v[0] ?? ""),
|
|
378
|
+
service: String(v[1] ?? "unknown"),
|
|
379
|
+
timesApplied: Number(v[2] ?? 0),
|
|
380
|
+
timesResolved: Number(v[3] ?? 0),
|
|
381
|
+
avgMttrMs: v[4] != null ? Number(v[4]) : null,
|
|
382
|
+
lastUsedAt: Number(v[5] ?? 0)
|
|
383
|
+
}));
|
|
384
|
+
} catch (err) {
|
|
385
|
+
logger.warn({ err, command }, "postmortem store lookupFixHistory failed");
|
|
386
|
+
return [];
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Per-service aggregated profile for the explain_service tool.
|
|
391
|
+
* All three queries run directly against SQLite — zero LLM latency.
|
|
392
|
+
*/
|
|
393
|
+
serviceProfile(service) {
|
|
394
|
+
const empty = { totalIncidents: 0, firstSeenAt: null, lastSeenAt: null, failureModes: [], topFixCommands: [] };
|
|
395
|
+
if (!this.db) return empty;
|
|
396
|
+
try {
|
|
397
|
+
const summaryRes = this.db.exec(
|
|
398
|
+
`SELECT COUNT(*), MIN(generated_at), MAX(generated_at) FROM postmortems WHERE service=?`,
|
|
399
|
+
[service]
|
|
400
|
+
);
|
|
401
|
+
const sumRow = summaryRes[0]?.values?.[0];
|
|
402
|
+
const totalIncidents = sumRow ? Number(sumRow[0] ?? 0) : 0;
|
|
403
|
+
if (totalIncidents === 0) return empty;
|
|
404
|
+
const modesRes = this.db.exec(
|
|
405
|
+
`SELECT tag,
|
|
406
|
+
COUNT(*) AS freq,
|
|
407
|
+
AVG(mttr_ms) AS avg_mttr,
|
|
408
|
+
MAX(generated_at) AS last_seen,
|
|
409
|
+
SUM(resolved_autonomously) AS auto_count,
|
|
410
|
+
(SELECT fix_command FROM postmortems p2
|
|
411
|
+
WHERE p2.service=p1.service AND p2.tag=p1.tag
|
|
412
|
+
AND p2.fix_command IS NOT NULL
|
|
413
|
+
ORDER BY p2.generated_at DESC LIMIT 1) AS top_fix
|
|
414
|
+
FROM postmortems p1
|
|
415
|
+
WHERE service=?
|
|
416
|
+
GROUP BY tag
|
|
417
|
+
ORDER BY freq DESC
|
|
418
|
+
LIMIT 5`,
|
|
419
|
+
[service]
|
|
420
|
+
);
|
|
421
|
+
const failureModes = (modesRes[0]?.values ?? []).map((v) => ({
|
|
422
|
+
tag: String(v[0] ?? ""),
|
|
423
|
+
frequency: Number(v[1] ?? 0),
|
|
424
|
+
avgMttrMs: v[2] != null ? Number(v[2]) : null,
|
|
425
|
+
lastSeenAt: Number(v[3] ?? 0),
|
|
426
|
+
autonomousRate: Number(v[1] ?? 1) > 0 ? Math.round(Number(v[4] ?? 0) / Number(v[1] ?? 1) * 100) : 0,
|
|
427
|
+
topFixCommand: v[5] ? String(v[5]) : null
|
|
428
|
+
}));
|
|
429
|
+
const fixRes = this.db.exec(
|
|
430
|
+
`SELECT fix_command, COUNT(*) AS times_applied
|
|
431
|
+
FROM postmortems
|
|
432
|
+
WHERE service=? AND fix_command IS NOT NULL
|
|
433
|
+
GROUP BY fix_command
|
|
434
|
+
ORDER BY times_applied DESC
|
|
435
|
+
LIMIT 3`,
|
|
436
|
+
[service]
|
|
437
|
+
);
|
|
438
|
+
const topFixCommands = (fixRes[0]?.values ?? []).map((v) => ({
|
|
439
|
+
command: String(v[0] ?? ""),
|
|
440
|
+
timesApplied: Number(v[1] ?? 0)
|
|
441
|
+
}));
|
|
442
|
+
return {
|
|
443
|
+
totalIncidents,
|
|
444
|
+
firstSeenAt: sumRow && sumRow[1] != null ? Number(sumRow[1]) : null,
|
|
445
|
+
lastSeenAt: sumRow && sumRow[2] != null ? Number(sumRow[2]) : null,
|
|
446
|
+
failureModes,
|
|
447
|
+
topFixCommands
|
|
448
|
+
};
|
|
449
|
+
} catch (err) {
|
|
450
|
+
logger.warn({ err, service }, "postmortem store serviceProfile failed");
|
|
451
|
+
return empty;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Count of postmortems where autopilot ran (resolved_autonomously=1) but
|
|
456
|
+
* causal verification is missing (causally_correct=0). This includes:
|
|
457
|
+
* - Rows written before the causallyCorrect fix was deployed (DEFAULT 0)
|
|
458
|
+
* - Post-fix rows where the error rate did not verifiably drop
|
|
459
|
+
*
|
|
460
|
+
* Used only by the startup diagnostic — not surfaced as a metric.
|
|
461
|
+
* Returns 0 when the DB is not initialized.
|
|
462
|
+
*/
|
|
463
|
+
countUnverifiedAutonomous() {
|
|
464
|
+
if (!this.db) return 0;
|
|
465
|
+
try {
|
|
466
|
+
const res = this.db.exec(
|
|
467
|
+
`SELECT COUNT(*) FROM postmortems WHERE resolved_autonomously=1 AND causally_correct=0`
|
|
468
|
+
);
|
|
469
|
+
return Number(res[0]?.values?.[0]?.[0] ?? 0);
|
|
470
|
+
} catch {
|
|
471
|
+
return 0;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Total MTTR saved vs. the baseline average manual MTTR (for outcome billing).
|
|
476
|
+
* Returns null if there are fewer than 3 samples.
|
|
477
|
+
*/
|
|
478
|
+
totalMttrSavedMs(autonomousMttrMs = 12e4) {
|
|
479
|
+
if (!this.db) return null;
|
|
480
|
+
try {
|
|
481
|
+
const res = this.db.exec(
|
|
482
|
+
`SELECT SUM(mttr_ms - ?) FROM postmortems WHERE resolved_autonomously=1 AND mttr_ms > ?`,
|
|
483
|
+
[autonomousMttrMs, autonomousMttrMs]
|
|
484
|
+
);
|
|
485
|
+
const val = res[0]?.values?.[0]?.[0];
|
|
486
|
+
return val != null ? Number(val) : null;
|
|
487
|
+
} catch {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
/** Returns true when the SQLite database initialised successfully. */
|
|
492
|
+
isHealthy() {
|
|
493
|
+
return this.db !== null;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const postmortemStore = new PostmortemStore();
|
|
497
|
+
function detectGitSha() {
|
|
498
|
+
try {
|
|
499
|
+
return execSync("git rev-parse HEAD", { stdio: "pipe", timeout: 2e3 }).toString().trim().slice(0, 12);
|
|
500
|
+
} catch {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function detectGitBranch() {
|
|
505
|
+
try {
|
|
506
|
+
return execSync("git rev-parse --abbrev-ref HEAD", { stdio: "pipe", timeout: 2e3 }).toString().trim();
|
|
507
|
+
} catch {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function fmtMs(ms) {
|
|
512
|
+
if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
|
|
513
|
+
const min = Math.floor(ms / 6e4);
|
|
514
|
+
const sec = Math.round(ms % 6e4 / 1e3);
|
|
515
|
+
return sec > 0 ? `${min}m ${sec}s` : `${min}m`;
|
|
516
|
+
}
|
|
517
|
+
function generatePostmortem(input) {
|
|
518
|
+
const now = Date.now();
|
|
519
|
+
const gitSha = detectGitSha();
|
|
520
|
+
const gitBranch = detectGitBranch();
|
|
521
|
+
const mttrLabel = input.mttrMs != null ? fmtMs(input.mttrMs) : "unknown";
|
|
522
|
+
const evidenceSection = input.evidence?.length ? `## Evidence
|
|
523
|
+
|
|
524
|
+
${input.evidence.map((e) => `- ${e}`).join("\n")}
|
|
525
|
+
|
|
526
|
+
` : "";
|
|
527
|
+
const fixSection = input.fixCommand ? `## Fix Applied
|
|
528
|
+
|
|
529
|
+
\`\`\`
|
|
530
|
+
${input.fixCommand}
|
|
531
|
+
\`\`\`
|
|
532
|
+
|
|
533
|
+
` : input.fixHint ? `## Fix Applied
|
|
534
|
+
|
|
535
|
+
${input.fixHint}
|
|
536
|
+
|
|
537
|
+
` : "";
|
|
538
|
+
const headerLine = gitBranch ? `**Branch:** ${gitBranch} | **SHA:** ${gitSha ?? "unknown"}` : "";
|
|
539
|
+
const relatedPrior = postmortemStore.getByTag(input.tag, 10).filter((pm2) => pm2.pid !== input.pid && pm2.service === input.service).slice(0, 3);
|
|
540
|
+
const relatedSection = relatedPrior.length > 0 ? [
|
|
541
|
+
"## Related Past Incidents (same failure mode, same service)",
|
|
542
|
+
"",
|
|
543
|
+
...relatedPrior.map((pm2) => {
|
|
544
|
+
const date = new Date(pm2.generatedAt).toISOString().slice(0, 10);
|
|
545
|
+
const mttr = pm2.mttrMs != null ? fmtMs(pm2.mttrMs) : "unknown";
|
|
546
|
+
const how = pm2.resolvedAutonomously ? "\u{1F916} autonomous" : "\u{1F464} manual";
|
|
547
|
+
return `- [${date}] MTTR: ${mttr} \xB7 ${how} \xB7 ${pm2.rootCause.slice(0, 100)}`;
|
|
548
|
+
}),
|
|
549
|
+
"",
|
|
550
|
+
relatedPrior.length > 1 ? `_Recurring pattern detected (${relatedPrior.length + 1} incidents). Consider a systemic fix rather than a one-off patch._` : "",
|
|
551
|
+
""
|
|
552
|
+
].filter(Boolean).join("\n") : "";
|
|
553
|
+
const body = [
|
|
554
|
+
`# Postmortem \u2014 ${input.tag.replace(/^infra_/, "")}`,
|
|
555
|
+
"",
|
|
556
|
+
`**Service:** ${input.service} | **Date:** ${new Date(now).toISOString().slice(0, 10)}`,
|
|
557
|
+
`**Confidence:** ${Math.round(input.confidence * 100)}% | **MTTR:** ${mttrLabel}`,
|
|
558
|
+
`**Resolution:** ${input.resolvedAutonomously ? "Autonomous (Mergen)" : "Manual"}`,
|
|
559
|
+
headerLine,
|
|
560
|
+
"",
|
|
561
|
+
"## Root Cause",
|
|
562
|
+
"",
|
|
563
|
+
input.rootCause,
|
|
564
|
+
"",
|
|
565
|
+
evidenceSection,
|
|
566
|
+
fixSection,
|
|
567
|
+
"## Timeline",
|
|
568
|
+
"",
|
|
569
|
+
"- Incident detected and triaged by Mergen",
|
|
570
|
+
input.resolvedAutonomously ? `- Fix executed autonomously \u2014 RESOLVED in ${mttrLabel}` : `- Fix applied manually \u2014 RESOLVED in ${mttrLabel}`,
|
|
571
|
+
"",
|
|
572
|
+
relatedSection
|
|
573
|
+
].filter((l) => l !== void 0).join("\n");
|
|
574
|
+
const pm = {
|
|
575
|
+
pid: input.pid,
|
|
576
|
+
tag: input.tag,
|
|
577
|
+
service: input.service,
|
|
578
|
+
gitSha,
|
|
579
|
+
gitBranch,
|
|
580
|
+
rootCause: input.rootCause,
|
|
581
|
+
fixCommand: input.fixCommand,
|
|
582
|
+
confidence: input.confidence,
|
|
583
|
+
mttrMs: input.mttrMs,
|
|
584
|
+
resolvedAutonomously: input.resolvedAutonomously,
|
|
585
|
+
causallyCorrect: input.causallyCorrect ?? false,
|
|
586
|
+
generatedAt: now,
|
|
587
|
+
body
|
|
588
|
+
};
|
|
589
|
+
postmortemStore.write(pm);
|
|
590
|
+
logger.info({ pid: pm.pid, tag: pm.tag, mttrMs: pm.mttrMs }, "postmortem generated");
|
|
591
|
+
return pm;
|
|
592
|
+
}
|
|
593
|
+
export {
|
|
594
|
+
generatePostmortem,
|
|
595
|
+
postmortemStore
|
|
596
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { recordHabituationEvent } from "../sensor/habituation-store.js";
|
|
5
|
+
import logger from "../sensor/logger.js";
|
|
6
|
+
const COMMENT_THRESHOLD = 0.85;
|
|
7
|
+
const _commented = /* @__PURE__ */ new Set();
|
|
8
|
+
function hasMergenComment(repoAndPr) {
|
|
9
|
+
return _commented.has(repoAndPr);
|
|
10
|
+
}
|
|
11
|
+
function loadGitHubToken() {
|
|
12
|
+
if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
|
|
13
|
+
try {
|
|
14
|
+
const cfgPath = path.join(os.homedir(), ".mergen", "config.json");
|
|
15
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
|
16
|
+
const gh = cfg.github ?? {};
|
|
17
|
+
if (typeof gh.token === "string" && gh.token) return gh.token;
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
async function postGitHubComment(repo, prNumber, body, token) {
|
|
23
|
+
const res = await fetch(`https://api.github.com/repos/${repo}/issues/${prNumber}/comments`, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: {
|
|
26
|
+
Authorization: `Bearer ${token}`,
|
|
27
|
+
Accept: "application/vnd.github+json",
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({ body }),
|
|
32
|
+
signal: AbortSignal.timeout(1e4)
|
|
33
|
+
});
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
const text = await res.text().catch(() => "");
|
|
36
|
+
throw new Error(`GitHub API ${res.status}: ${text.slice(0, 120)}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function maybePostPRComment(result) {
|
|
40
|
+
if (process.env.MERGEN_PR_COMMENTS !== "true") return;
|
|
41
|
+
if (!result.wouldHaveShown) return;
|
|
42
|
+
if (result.relevanceScore < COMMENT_THRESHOLD) return;
|
|
43
|
+
if (!result.wouldHaveComment) return;
|
|
44
|
+
const key = `${result.repo}#${result.prNumber}`;
|
|
45
|
+
if (_commented.has(key)) return;
|
|
46
|
+
const token = loadGitHubToken();
|
|
47
|
+
if (!token) {
|
|
48
|
+
logger.warn(
|
|
49
|
+
{ prId: result.prId },
|
|
50
|
+
"pr-commenter: MERGEN_PR_COMMENTS=true but no GitHub token \u2014 set GITHUB_TOKEN or run: mergen-server connect github"
|
|
51
|
+
);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
_commented.add(key);
|
|
55
|
+
const liveComment = result.wouldHaveComment.replace(
|
|
56
|
+
/\n_\[Mergen shadow mode — not posted yet\]_\n?/,
|
|
57
|
+
"\n"
|
|
58
|
+
);
|
|
59
|
+
try {
|
|
60
|
+
await postGitHubComment(result.repo, result.prNumber, liveComment, token);
|
|
61
|
+
logger.info(
|
|
62
|
+
{ prId: result.prId, score: result.relevanceScore },
|
|
63
|
+
"pr-commenter: comment posted"
|
|
64
|
+
);
|
|
65
|
+
recordHabituationEvent({
|
|
66
|
+
recordedAt: Date.now(),
|
|
67
|
+
eventType: "comment_posted",
|
|
68
|
+
actor: result.author,
|
|
69
|
+
repo: result.repo,
|
|
70
|
+
prNumber: result.prNumber,
|
|
71
|
+
relevanceScore: result.relevanceScore
|
|
72
|
+
});
|
|
73
|
+
} catch (err) {
|
|
74
|
+
_commented.delete(key);
|
|
75
|
+
logger.warn({ err, prId: result.prId }, "pr-commenter: post failed");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
hasMergenComment,
|
|
80
|
+
maybePostPRComment
|
|
81
|
+
};
|