mergen-server 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (308) hide show
  1. package/LICENSE +57 -0
  2. package/README.md +96 -0
  3. package/dist/__stubs__/calibration.js +471 -0
  4. package/dist/__stubs__/causal.js +601 -0
  5. package/dist/__stubs__/closed-source.js +150 -0
  6. package/dist/__stubs__/license.js +35 -0
  7. package/dist/__stubs__/plans.js +197 -0
  8. package/dist/app.js +467 -0
  9. package/dist/cli.js +250 -0
  10. package/dist/commands/agent-identity.js +79 -0
  11. package/dist/commands/gate.js +107 -0
  12. package/dist/commands/github.js +632 -0
  13. package/dist/commands/incident.js +644 -0
  14. package/dist/commands/policy.js +710 -0
  15. package/dist/commands/scan.js +243 -0
  16. package/dist/commands/setup.js +1218 -0
  17. package/dist/commands/shared.js +109 -0
  18. package/dist/commands/team.js +809 -0
  19. package/dist/datadog/blame-attribution.js +228 -0
  20. package/dist/datadog/client.js +159 -0
  21. package/dist/datadog/compactor.js +169 -0
  22. package/dist/datadog/fingerprinter.js +34 -0
  23. package/dist/datadog/incident-state.js +21 -0
  24. package/dist/datadog/line-matcher.js +56 -0
  25. package/dist/datadog/memory-store.js +399 -0
  26. package/dist/datadog/otel-trace.js +67 -0
  27. package/dist/index.js +505 -0
  28. package/dist/intelligence/action-risk.js +132 -0
  29. package/dist/intelligence/activity-feed.js +40 -0
  30. package/dist/intelligence/agent-identity.js +115 -0
  31. package/dist/intelligence/agent-pipeline.js +329 -0
  32. package/dist/intelligence/agent-profiles.js +81 -0
  33. package/dist/intelligence/ai-commit.js +16 -0
  34. package/dist/intelligence/approval-analytics.js +42 -0
  35. package/dist/intelligence/approval-events.js +5 -0
  36. package/dist/intelligence/arch-boundaries.js +94 -0
  37. package/dist/intelligence/arch-graph.js +117 -0
  38. package/dist/intelligence/autonomy.js +397 -0
  39. package/dist/intelligence/azure-broker.js +57 -0
  40. package/dist/intelligence/azure-proxy-sessions.js +49 -0
  41. package/dist/intelligence/baseline.js +1 -0
  42. package/dist/intelligence/behavior-baseline.js +134 -0
  43. package/dist/intelligence/billing.js +1 -0
  44. package/dist/intelligence/blast-radius.js +224 -0
  45. package/dist/intelligence/bypass.js +157 -0
  46. package/dist/intelligence/calibration-classifier.js +77 -0
  47. package/dist/intelligence/calibration-git-sync.js +74 -0
  48. package/dist/intelligence/calibration.js +1 -0
  49. package/dist/intelligence/cascade-detector.js +58 -0
  50. package/dist/intelligence/case-study-generator.js +126 -0
  51. package/dist/intelligence/causal-graph.js +126 -0
  52. package/dist/intelligence/causal.js +1 -0
  53. package/dist/intelligence/change-risk.js +102 -0
  54. package/dist/intelligence/compliance-report.js +134 -0
  55. package/dist/intelligence/confidence-report.js +85 -0
  56. package/dist/intelligence/corpus-to-policy.js +140 -0
  57. package/dist/intelligence/credential-broker.js +112 -0
  58. package/dist/intelligence/credential-misuse.js +39 -0
  59. package/dist/intelligence/debug-sessions.js +108 -0
  60. package/dist/intelligence/decision-escalation.js +36 -0
  61. package/dist/intelligence/decision-memory.js +47 -0
  62. package/dist/intelligence/decision-similarity.js +85 -0
  63. package/dist/intelligence/default-safety-tests.js +53 -0
  64. package/dist/intelligence/degradation-watcher.js +108 -0
  65. package/dist/intelligence/detector-plugins.js +70 -0
  66. package/dist/intelligence/detectors.js +1 -0
  67. package/dist/intelligence/device-auth.js +138 -0
  68. package/dist/intelligence/diff-size.js +42 -0
  69. package/dist/intelligence/ebpf-verify.js +45 -0
  70. package/dist/intelligence/enterprise-policy-engine.js +1122 -0
  71. package/dist/intelligence/error-fingerprint.js +1 -0
  72. package/dist/intelligence/execution-gate.js +100 -0
  73. package/dist/intelligence/execution-mode.js +22 -0
  74. package/dist/intelligence/gate-analytics.js +256 -0
  75. package/dist/intelligence/gate-decision.js +64 -0
  76. package/dist/intelligence/gcp-broker.js +50 -0
  77. package/dist/intelligence/git-adr-sync.js +207 -0
  78. package/dist/intelligence/hitl-hold.js +460 -0
  79. package/dist/intelligence/hypothesis-history.js +93 -0
  80. package/dist/intelligence/idp-identity.js +83 -0
  81. package/dist/intelligence/impl-critic.js +216 -0
  82. package/dist/intelligence/incident-autopilot.js +622 -0
  83. package/dist/intelligence/incident-replay.js +162 -0
  84. package/dist/intelligence/incident-result-cache.js +33 -0
  85. package/dist/intelligence/incident-similarity.js +58 -0
  86. package/dist/intelligence/infra-detectors.js +312 -0
  87. package/dist/intelligence/license.js +185 -0
  88. package/dist/intelligence/llm-spokesperson.js +62 -0
  89. package/dist/intelligence/mcp-prompts.js +320 -0
  90. package/dist/intelligence/mcp-resources.js +171 -0
  91. package/dist/intelligence/normalize.js +108 -0
  92. package/dist/intelligence/notifications.js +83 -0
  93. package/dist/intelligence/oidc-issuer.js +87 -0
  94. package/dist/intelligence/override-corpus.js +597 -0
  95. package/dist/intelligence/planning-gate.js +84 -0
  96. package/dist/intelligence/plans.js +223 -0
  97. package/dist/intelligence/platt-scaling.js +164 -0
  98. package/dist/intelligence/policy-proposals.js +84 -0
  99. package/dist/intelligence/policy-suggester.js +161 -0
  100. package/dist/intelligence/policy-sync.js +103 -0
  101. package/dist/intelligence/policy-test-runner.js +35 -0
  102. package/dist/intelligence/postmortem-parser.js +181 -0
  103. package/dist/intelligence/postmortem-retrieval.js +224 -0
  104. package/dist/intelligence/postmortem-store.js +596 -0
  105. package/dist/intelligence/pr-commenter.js +81 -0
  106. package/dist/intelligence/pr-policy-gate.js +38 -0
  107. package/dist/intelligence/pr-shadow-analyzer.js +229 -0
  108. package/dist/intelligence/process-tree.js +108 -0
  109. package/dist/intelligence/prompts.js +1 -0
  110. package/dist/intelligence/replay.js +38 -0
  111. package/dist/intelligence/repro-steps.js +1 -0
  112. package/dist/intelligence/resource-extractors.js +0 -0
  113. package/dist/intelligence/resource-file-context.js +138 -0
  114. package/dist/intelligence/risk-score.js +22 -0
  115. package/dist/intelligence/rollback.js +137 -0
  116. package/dist/intelligence/runbook-updater.js +143 -0
  117. package/dist/intelligence/sandbox.js +194 -0
  118. package/dist/intelligence/script-trust.js +104 -0
  119. package/dist/intelligence/session-metrics.js +67 -0
  120. package/dist/intelligence/session-threat-tracker.js +231 -0
  121. package/dist/intelligence/shadow-digest-cron.js +81 -0
  122. package/dist/intelligence/shadow-log.js +284 -0
  123. package/dist/intelligence/shell-ast.js +145 -0
  124. package/dist/intelligence/siem-forward.js +104 -0
  125. package/dist/intelligence/slack-digest.js +151 -0
  126. package/dist/intelligence/slack-override-loop.js +217 -0
  127. package/dist/intelligence/slack-routing.js +67 -0
  128. package/dist/intelligence/slack.js +900 -0
  129. package/dist/intelligence/sql-ast.js +31 -0
  130. package/dist/intelligence/team.js +1 -0
  131. package/dist/intelligence/telemetry.js +76 -0
  132. package/dist/intelligence/threshold-optimizer.js +75 -0
  133. package/dist/intelligence/token-budget.js +66 -0
  134. package/dist/intelligence/tool-guard.js +708 -0
  135. package/dist/intelligence/tool-manifest 2.js +74 -0
  136. package/dist/intelligence/tool-manifest.js +125 -0
  137. package/dist/intelligence/tools-analysis.js +857 -0
  138. package/dist/intelligence/tools-arch.js +152 -0
  139. package/dist/intelligence/tools-autonomy.js +495 -0
  140. package/dist/intelligence/tools-blast-radius.js +140 -0
  141. package/dist/intelligence/tools-browser.js +431 -0
  142. package/dist/intelligence/tools-change-timeline.js +154 -0
  143. package/dist/intelligence/tools-credentials.js +117 -0
  144. package/dist/intelligence/tools-datadog.js +233 -0
  145. package/dist/intelligence/tools-debug-sessions.js +209 -0
  146. package/dist/intelligence/tools-discovery.js +284 -0
  147. package/dist/intelligence/tools-infra.js +612 -0
  148. package/dist/intelligence/tools-intent.js +128 -0
  149. package/dist/intelligence/tools-memory.js +514 -0
  150. package/dist/intelligence/tools-runbook.js +951 -0
  151. package/dist/intelligence/tools-sessions.js +107 -0
  152. package/dist/intelligence/tools-state.js +232 -0
  153. package/dist/intelligence/tools-utility.js +430 -0
  154. package/dist/intelligence/tools-validate.js +215 -0
  155. package/dist/intelligence/tools.js +1 -0
  156. package/dist/intelligence/trace-context.js +11 -0
  157. package/dist/intelligence/unclassified-clusters.js +87 -0
  158. package/dist/intelligence/usage.js +195 -0
  159. package/dist/routes/active-authors.js +63 -0
  160. package/dist/routes/activity-feed.js +21 -0
  161. package/dist/routes/adr.js +117 -0
  162. package/dist/routes/agent-activity.js +184 -0
  163. package/dist/routes/agent-blunders.js +163 -0
  164. package/dist/routes/agents.js +175 -0
  165. package/dist/routes/api-keys.js +65 -0
  166. package/dist/routes/arch.js +75 -0
  167. package/dist/routes/audit-export.js +93 -0
  168. package/dist/routes/billing-dashboard.js +158 -0
  169. package/dist/routes/billing-outcome.js +77 -0
  170. package/dist/routes/calibration.js +305 -0
  171. package/dist/routes/ci-gate-history.js +20 -0
  172. package/dist/routes/ci-gate.js +232 -0
  173. package/dist/routes/ci.js +293 -0
  174. package/dist/routes/compliance-report.js +24 -0
  175. package/dist/routes/confidence.js +30 -0
  176. package/dist/routes/credentials.js +119 -0
  177. package/dist/routes/dashboard.js +1407 -0
  178. package/dist/routes/decision-memory.js +42 -0
  179. package/dist/routes/demo.js +844 -0
  180. package/dist/routes/device-auth-stub.js +173 -0
  181. package/dist/routes/explain-why.js +39 -0
  182. package/dist/routes/gate-analytics.js +213 -0
  183. package/dist/routes/gate.js +28 -0
  184. package/dist/routes/github-webhook.js +364 -0
  185. package/dist/routes/habituation.js +30 -0
  186. package/dist/routes/health-integrations.js +243 -0
  187. package/dist/routes/heartbeats.js +45 -0
  188. package/dist/routes/hitl.js +364 -0
  189. package/dist/routes/impact-report.js +645 -0
  190. package/dist/routes/incident-webhook.js +63 -0
  191. package/dist/routes/incidents.js +366 -0
  192. package/dist/routes/layers.js +62 -0
  193. package/dist/routes/license.js +187 -0
  194. package/dist/routes/oidc-issuer.js +30 -0
  195. package/dist/routes/onboarding.js +170 -0
  196. package/dist/routes/otel.js +67 -0
  197. package/dist/routes/otlp-receiver.js +179 -0
  198. package/dist/routes/overrides.js +241 -0
  199. package/dist/routes/pagerduty.js +258 -0
  200. package/dist/routes/policies.js +809 -0
  201. package/dist/routes/policy-nl.js +169 -0
  202. package/dist/routes/postmortem.js +102 -0
  203. package/dist/routes/pr-shadow.js +47 -0
  204. package/dist/routes/rbac.js +61 -0
  205. package/dist/routes/replay.js +17 -0
  206. package/dist/routes/risk-report.js +164 -0
  207. package/dist/routes/runbooks.js +125 -0
  208. package/dist/routes/safety-test.js +174 -0
  209. package/dist/routes/sdk.js +222 -0
  210. package/dist/routes/sensor.js +819 -0
  211. package/dist/routes/sentry.js +140 -0
  212. package/dist/routes/sessions.js +43 -0
  213. package/dist/routes/setup-ui.js +594 -0
  214. package/dist/routes/shadow-report.js +107 -0
  215. package/dist/routes/slack-routing.js +171 -0
  216. package/dist/routes/team-usage.js +71 -0
  217. package/dist/routes/telemetry.js +28 -0
  218. package/dist/routes/tenants.js +199 -0
  219. package/dist/routes/tickets.js +167 -0
  220. package/dist/routes/validate.js +13 -0
  221. package/dist/routes/war-room.js +113 -0
  222. package/dist/scripts/check-arch.js +22 -0
  223. package/dist/seeds/community-corpus.js +176 -0
  224. package/dist/seeds/corpus.js +1224 -0
  225. package/dist/sensor/action-ledger.js +327 -0
  226. package/dist/sensor/adr-store.js +140 -0
  227. package/dist/sensor/agent-blunder-store.js +370 -0
  228. package/dist/sensor/agent-context-store.js +225 -0
  229. package/dist/sensor/agent-memory-store.js +231 -0
  230. package/dist/sensor/audit-fetch.js +23 -0
  231. package/dist/sensor/audit-log.js +273 -0
  232. package/dist/sensor/buffer-schemas.js +225 -0
  233. package/dist/sensor/buffer.js +599 -0
  234. package/dist/sensor/bypass-tracker.js +93 -0
  235. package/dist/sensor/ci-gate-history.js +70 -0
  236. package/dist/sensor/cloud-auth.js +183 -0
  237. package/dist/sensor/commit-context-store.js +235 -0
  238. package/dist/sensor/docker-log-stream.js +164 -0
  239. package/dist/sensor/docker-monitor.js +122 -0
  240. package/dist/sensor/extended-buffer.js +46 -0
  241. package/dist/sensor/feedback-token.js +41 -0
  242. package/dist/sensor/file-lock.js +6 -0
  243. package/dist/sensor/fs-watcher.js +106 -0
  244. package/dist/sensor/gate-heartbeat.js +122 -0
  245. package/dist/sensor/git-suspect.js +136 -0
  246. package/dist/sensor/habituation-store.js +77 -0
  247. package/dist/sensor/heartbeat-monitor.js +133 -0
  248. package/dist/sensor/incident-store.js +340 -0
  249. package/dist/sensor/infra-normalizer.js +513 -0
  250. package/dist/sensor/ingest.js +339 -0
  251. package/dist/sensor/jest-reporter.js +52 -0
  252. package/dist/sensor/k8s-events.js +132 -0
  253. package/dist/sensor/layer2-store.js +111 -0
  254. package/dist/sensor/layer3-store.js +230 -0
  255. package/dist/sensor/layer4-store.js +147 -0
  256. package/dist/sensor/logger.js +13 -0
  257. package/dist/sensor/otel-exporter.js +161 -0
  258. package/dist/sensor/paths.js +71 -0
  259. package/dist/sensor/policy-history.js +147 -0
  260. package/dist/sensor/pr-shadow-store.js +83 -0
  261. package/dist/sensor/process-watcher.js +162 -0
  262. package/dist/sensor/rbac.js +92 -0
  263. package/dist/sensor/redact.js +114 -0
  264. package/dist/sensor/redis-store.js +191 -0
  265. package/dist/sensor/route-reachability.js +64 -0
  266. package/dist/sensor/security-utils.js +18 -0
  267. package/dist/sensor/service-graph.js +170 -0
  268. package/dist/sensor/service-topology.js +227 -0
  269. package/dist/sensor/session-history.js +74 -0
  270. package/dist/sensor/session-persist.js +38 -0
  271. package/dist/sensor/shadow-promote.js +122 -0
  272. package/dist/sensor/sourcemap.js +281 -0
  273. package/dist/sensor/sqlite-store.js +205 -0
  274. package/dist/sensor/sso.js +164 -0
  275. package/dist/sensor/vitest-reporter.js +65 -0
  276. package/dist/sensor/watcher.js +48 -0
  277. package/dist/storage/interfaces.js +0 -0
  278. package/dist/storage/pg/pg-action-ledger.js +138 -0
  279. package/dist/storage/pg/pg-approval-store.js +107 -0
  280. package/dist/storage/pg/pg-blunder-store.js +193 -0
  281. package/dist/storage/pg/pg-ci-gate-history.js +83 -0
  282. package/dist/storage/pg/pg-client.js +24 -0
  283. package/dist/storage/pg/pg-event-store.js +63 -0
  284. package/dist/storage/pg/pg-incident-store.js +190 -0
  285. package/dist/storage/pg/pg-migrations.js +26 -0
  286. package/dist/storage/pg/pg-override-corpus.js +447 -0
  287. package/dist/storage/pg/pg-shadow-log.js +129 -0
  288. package/dist/storage/sqlite/sqlite-action-ledger.js +18 -0
  289. package/dist/storage/sqlite/sqlite-approval-store.js +37 -0
  290. package/dist/storage/sqlite/sqlite-blunder-store.js +27 -0
  291. package/dist/storage/sqlite/sqlite-ci-gate-history.js +19 -0
  292. package/dist/storage/sqlite/sqlite-event-store.js +29 -0
  293. package/dist/storage/sqlite/sqlite-incident-store.js +34 -0
  294. package/dist/storage/sqlite/sqlite-override-corpus.js +75 -0
  295. package/dist/storage/sqlite/sqlite-shadow-log.js +36 -0
  296. package/dist/storage/store-factory.js +55 -0
  297. package/dist/storage/store-registry.js +31 -0
  298. package/dist/update-checker.js +107 -0
  299. package/dist/workers/autopilot-worker.js +28 -0
  300. package/dist/workers/notification-worker.js +28 -0
  301. package/dist/workers/queues.js +58 -0
  302. package/dist/workers/validation-worker.js +24 -0
  303. package/dist/workers/worker-registry.js +18 -0
  304. package/package.json +123 -0
  305. package/sdk/mergen-inject.js +260 -0
  306. package/sdk/node.js +313 -0
  307. package/sdk/vite-plugin.ts +57 -0
  308. package/sdk/webpack-plugin.js +106 -0
@@ -0,0 +1,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
+ };