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,137 @@
1
+ import { executeRemediation } from "./autonomy.js";
2
+ import logger from "../sensor/logger.js";
3
+ function deriveRollback(command, stdout) {
4
+ const cmd = command.trim();
5
+ const kubectlRestart = cmd.match(/^kubectl\s+rollout\s+restart\s+(deploy(?:ment)?\/\S+)/i);
6
+ if (kubectlRestart) {
7
+ return { type: "command", command: `kubectl rollout undo ${kubectlRestart[1]}` };
8
+ }
9
+ const kubectlRestartNs = cmd.match(/^kubectl\s+rollout\s+restart\s+(?:-n\s+\S+\s+)(deploy(?:ment)?\/\S+)/i);
10
+ if (kubectlRestartNs) {
11
+ const nsMatch = cmd.match(/-n\s+(\S+)/);
12
+ const ns = nsMatch ? ` -n ${nsMatch[1]}` : "";
13
+ return { type: "command", command: `kubectl rollout undo${ns} ${kubectlRestartNs[1]}` };
14
+ }
15
+ const kubectlSetImage = cmd.match(/^kubectl\s+set\s+image\s+(?:deploy(?:ment)?\/)?(\S+)/i);
16
+ if (kubectlSetImage) {
17
+ return { type: "command", command: `kubectl rollout undo deploy/${kubectlSetImage[1]}` };
18
+ }
19
+ const helmUpgrade = cmd.match(/^helm\s+upgrade\s+(\S+)/i);
20
+ if (helmUpgrade) {
21
+ return { type: "command", command: `helm rollback ${helmUpgrade[1]}` };
22
+ }
23
+ const npmInstall = cmd.match(/^npm\s+install\s+(\S+)@(\S+)/i);
24
+ if (npmInstall) {
25
+ const [, pkg, ver] = npmInstall;
26
+ const prevMatch = stdout.match(new RegExp(`removed.*${escapeRegex(pkg)}@(\\S+)`, "i"));
27
+ if (prevMatch) {
28
+ return { type: "command", command: `npm install ${pkg}@${prevMatch[1]}` };
29
+ }
30
+ return { type: "none", reason: `can't determine prior ${pkg} version \u2014 check package-lock.json and revert manually` };
31
+ }
32
+ const pipInstall = cmd.match(/^pip\s+install\s+(\S+)==(\S+)/i);
33
+ if (pipInstall) {
34
+ const [, pkg] = pipInstall;
35
+ return { type: "none", reason: `can't determine prior ${pkg} version \u2014 revert requirements.txt and run pip install manually` };
36
+ }
37
+ const yarnAdd = cmd.match(/^yarn\s+add\s+(\S+)@(\S+)/i);
38
+ if (yarnAdd) {
39
+ const [, pkg] = yarnAdd;
40
+ return { type: "none", reason: `can't determine prior ${pkg} version \u2014 check yarn.lock and revert manually` };
41
+ }
42
+ if (cmd.startsWith("git revert")) {
43
+ return { type: "command", command: "git revert HEAD --no-edit" };
44
+ }
45
+ const dockerComposeUp = cmd.match(/^docker-compose\s+up\b(.*)$/i);
46
+ if (dockerComposeUp) {
47
+ const serviceArg = dockerComposeUp[1].trim().split(/\s+/).filter((a) => a && !a.startsWith("-")).pop();
48
+ return { type: "command", command: serviceArg ? `docker-compose stop ${serviceArg}` : "docker-compose down" };
49
+ }
50
+ if (/^docker-compose\s+down\b/i.test(cmd)) {
51
+ return { type: "none", reason: "docker-compose down already tore down state \u2014 nothing to roll back to; redeploy manually if this was unintended" };
52
+ }
53
+ const systemctlEnable = cmd.match(/^systemctl\s+enable\s+(\S+)/i);
54
+ if (systemctlEnable) {
55
+ return { type: "command", command: `systemctl disable ${systemctlEnable[1]}` };
56
+ }
57
+ const systemctlDisable = cmd.match(/^systemctl\s+disable\s+(\S+)/i);
58
+ if (systemctlDisable) {
59
+ return { type: "command", command: `systemctl enable ${systemctlDisable[1]}` };
60
+ }
61
+ if (/^(pm2|systemctl|service|docker|supervisorctl)\s/.test(cmd) || /^kill\s+-[1H]/.test(cmd) || /^pkill\s+-[1H]/.test(cmd)) {
62
+ return { type: "none", reason: "restart commands are stateless \u2014 re-run the fix or restart manually" };
63
+ }
64
+ const backupMatch = cmd.match(/\b(?:cp|mv)\s+(\S+)\s+(\1\.bak\S*)/i);
65
+ if (backupMatch) {
66
+ const [, original, backupPath] = backupMatch;
67
+ return { type: "command", command: `mv ${backupPath} ${original}` };
68
+ }
69
+ return { type: "none", reason: "no rollback strategy known for this command type \u2014 revert manually" };
70
+ }
71
+ async function executeRollback(strategy, opts) {
72
+ if (strategy.type === "none") {
73
+ return { ok: false, message: strategy.reason };
74
+ }
75
+ logger.info({ command: strategy.command }, "rollback: executing");
76
+ const result = await executeRemediation(strategy.command, {
77
+ cwd: opts?.cwd,
78
+ actor: opts?.actor ?? "autopilot-rollback"
79
+ });
80
+ if (result.blocked) {
81
+ return { ok: false, message: `rollback blocked: ${result.blockReason}` };
82
+ }
83
+ if (!result.ok) {
84
+ return { ok: false, message: `rollback exited ${result.exitCode}: ${result.stderr.slice(0, 200)}` };
85
+ }
86
+ return { ok: true, message: strategy.command };
87
+ }
88
+ function escapeRegex(s) {
89
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
90
+ }
91
+ function generateRollbackPlan(intent) {
92
+ const { files, commands = [], featureFlag = null } = intent;
93
+ const anticipatedRollbackCommands = [];
94
+ for (const cmd of commands) {
95
+ const strategy = deriveRollback(cmd, "");
96
+ if (strategy.type === "command") {
97
+ anticipatedRollbackCommands.push(strategy.command);
98
+ }
99
+ }
100
+ const canAutoRollback = featureFlag !== null || anticipatedRollbackCommands.length > 0 && anticipatedRollbackCommands.length === commands.length;
101
+ const rollbackProcedure = [];
102
+ if (featureFlag) {
103
+ rollbackProcedure.push(`Disable feature flag: ${featureFlag}`);
104
+ rollbackProcedure.push("Verify error rates return to baseline via validate_fix or GET /validate");
105
+ }
106
+ if (anticipatedRollbackCommands.length > 0) {
107
+ rollbackProcedure.push("Execute rollback commands:");
108
+ for (const cmd of anticipatedRollbackCommands) {
109
+ rollbackProcedure.push(` ${cmd}`);
110
+ }
111
+ rollbackProcedure.push("Validate recovery: call validate_fix or check GET /validate after 60 s");
112
+ }
113
+ if (files.length > 0 && anticipatedRollbackCommands.length === 0 && !featureFlag) {
114
+ rollbackProcedure.push("Revert the following files using git:");
115
+ for (const f of files) rollbackProcedure.push(` git checkout HEAD -- ${f}`);
116
+ rollbackProcedure.push("Or use: git revert HEAD --no-edit");
117
+ rollbackProcedure.push("Redeploy / restart the service after revert");
118
+ }
119
+ if (rollbackProcedure.length === 0) {
120
+ rollbackProcedure.push("No automated rollback available \u2014 manual intervention required");
121
+ rollbackProcedure.push("Document the issue in the override corpus before proceeding");
122
+ }
123
+ const estimatedRollbackMs = featureFlag ? 1e4 : anticipatedRollbackCommands.length > 0 ? 6e4 : 12e4;
124
+ return {
125
+ filesModified: files,
126
+ featureFlag,
127
+ rollbackProcedure,
128
+ canAutoRollback,
129
+ estimatedRollbackMs,
130
+ anticipatedRollbackCommands
131
+ };
132
+ }
133
+ export {
134
+ deriveRollback,
135
+ executeRollback,
136
+ generateRollbackPlan
137
+ };
@@ -0,0 +1,143 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { USER_RUNBOOKS_DIR, zeroRetentionMode } from "../sensor/paths.js";
4
+ import { postmortemStore } from "./postmortem-store.js";
5
+ import logger from "../sensor/logger.js";
6
+ function fmtMs(ms) {
7
+ if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
8
+ const min = Math.floor(ms / 6e4);
9
+ const sec = Math.round(ms % 6e4 / 1e3);
10
+ return sec > 0 ? `${min}m ${sec}s` : `${min}m`;
11
+ }
12
+ function tagToSlug(tag) {
13
+ return tag.replace(/^infra_/, "").replace(/_/g, "-");
14
+ }
15
+ function tagToTitle(tag) {
16
+ return tag.replace(/^infra_/, "").replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
17
+ }
18
+ function aggregateFixStats(postmortems) {
19
+ const map = /* @__PURE__ */ new Map();
20
+ for (const pm of postmortems) {
21
+ if (!pm.fixCommand) continue;
22
+ const entry = map.get(pm.fixCommand) ?? { applied: 0, resolved: 0, mttrSamples: [] };
23
+ entry.applied++;
24
+ if (pm.causallyCorrect) entry.resolved++;
25
+ if (pm.mttrMs != null) entry.mttrSamples.push(pm.mttrMs);
26
+ map.set(pm.fixCommand, entry);
27
+ }
28
+ return [...map.entries()].map(([cmd, s]) => ({
29
+ command: cmd,
30
+ timesApplied: s.applied,
31
+ timesResolved: s.resolved,
32
+ successRate: s.applied > 0 ? Math.round(s.resolved / s.applied * 100) : 0,
33
+ avgMttrMs: s.mttrSamples.length > 0 ? s.mttrSamples.reduce((a, b) => a + b, 0) / s.mttrSamples.length : null
34
+ })).sort((a, b) => b.timesApplied - a.timesApplied || b.successRate - a.successRate).slice(0, 5);
35
+ }
36
+ function yamlStr(value) {
37
+ if (/[:#,\[\]{}&*!|>'"%@`]/.test(value) || value.includes("\n")) {
38
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
39
+ }
40
+ return value;
41
+ }
42
+ function buildRunbookYaml(tag, postmortems, fixes) {
43
+ const total = postmortems.length;
44
+ const autoCount = postmortems.filter((pm) => pm.resolvedAutonomously).length;
45
+ const autoRate = total > 0 ? Math.round(autoCount / total * 100) : 0;
46
+ const mttrSamples = postmortems.filter((pm) => pm.mttrMs != null).map((pm) => pm.mttrMs);
47
+ const avgMttr = mttrSamples.length > 0 ? fmtMs(mttrSamples.reduce((a, b) => a + b, 0) / mttrSamples.length) : "unknown";
48
+ const topFix = fixes[0]?.command ?? null;
49
+ const slug = tagToSlug(tag);
50
+ const title = tagToTitle(tag);
51
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
52
+ const services = [...new Set(postmortems.map((pm) => pm.service))].slice(0, 5);
53
+ const lines = [
54
+ `# Auto-generated by Mergen \u2014 do not edit manually.`,
55
+ `# Source: ${total} incident postmortems. Re-generated on every resolution.`,
56
+ `name: ${yamlStr(slug)}`,
57
+ `title: ${yamlStr(title)}`,
58
+ `description: ${yamlStr(`Auto-generated runbook for ${title} \u2014 based on ${total} real incidents.`)}`,
59
+ ``,
60
+ `corpus_stats:`,
61
+ ` last_updated: ${yamlStr(date)}`,
62
+ ` total_incidents: ${total}`,
63
+ ` avg_mttr: ${yamlStr(avgMttr)}`,
64
+ ` autonomous_rate_pct: ${autoRate}`,
65
+ ` services: [${services.map(yamlStr).join(", ")}]`,
66
+ ` top_fix: ${topFix ? yamlStr(topFix) : "null"}`,
67
+ ``
68
+ ];
69
+ if (fixes.length > 0) {
70
+ lines.push(`verified_fixes:`);
71
+ for (const f of fixes) {
72
+ const mttr = f.avgMttrMs != null ? fmtMs(f.avgMttrMs) : "unknown";
73
+ lines.push(` - command: ${yamlStr(f.command)}`);
74
+ lines.push(` times_applied: ${f.timesApplied}`);
75
+ lines.push(` success_rate_pct: ${f.successRate}`);
76
+ lines.push(` avg_mttr: ${yamlStr(mttr)}`);
77
+ }
78
+ lines.push(``);
79
+ }
80
+ const rootCauses = [...new Set(
81
+ postmortems.map((pm) => pm.rootCause.split(".")[0].trim()).filter(Boolean)
82
+ )].slice(0, 3);
83
+ if (rootCauses.length > 0) {
84
+ lines.push(`common_root_causes:`);
85
+ for (const rc of rootCauses) {
86
+ lines.push(` - ${yamlStr(rc)}`);
87
+ }
88
+ lines.push(``);
89
+ }
90
+ lines.push(`steps:`);
91
+ lines.push(` - name: Confirm current error state`);
92
+ lines.push(` tool: analyze_runtime`);
93
+ lines.push(``);
94
+ lines.push(` - name: Search for similar past incidents`);
95
+ lines.push(` tool: search_postmortems`);
96
+ lines.push(` params:`);
97
+ lines.push(` query: ${yamlStr(slug.replace(/-/g, " "))}`);
98
+ lines.push(``);
99
+ if (topFix) {
100
+ lines.push(` - name: Check fix history before applying`);
101
+ lines.push(` tool: check_fix_history`);
102
+ lines.push(` params:`);
103
+ lines.push(` command: ${yamlStr(topFix)}`);
104
+ lines.push(``);
105
+ lines.push(` - name: Apply top verified fix (${fixes[0].successRate}% success rate)`);
106
+ lines.push(` tool: execute_fix`);
107
+ lines.push(` params:`);
108
+ lines.push(` command: ${yamlStr(topFix)}`);
109
+ lines.push(` confirm: true`);
110
+ lines.push(``);
111
+ }
112
+ lines.push(` - name: Validate resolution`);
113
+ lines.push(` tool: validate_fix`);
114
+ lines.push(``);
115
+ lines.push(` - name: Suggest follow-up actions`);
116
+ lines.push(` tool: suggest_followups`);
117
+ lines.push(``);
118
+ return lines.join("\n");
119
+ }
120
+ function updateRunbookFromPostmortem(pm) {
121
+ if (zeroRetentionMode()) return;
122
+ try {
123
+ const related = postmortemStore.getByTag(pm.tag, 100);
124
+ if (related.length < 2) return;
125
+ const fixes = aggregateFixStats(related);
126
+ const yaml = buildRunbookYaml(pm.tag, related, fixes);
127
+ const slug = tagToSlug(pm.tag);
128
+ const outPath = path.join(USER_RUNBOOKS_DIR, `${slug}.yaml`);
129
+ fs.mkdirSync(USER_RUNBOOKS_DIR, { recursive: true });
130
+ const tmp = `${outPath}.tmp.${process.pid}`;
131
+ fs.writeFileSync(tmp, yaml, "utf8");
132
+ fs.renameSync(tmp, outPath);
133
+ logger.info(
134
+ { tag: pm.tag, incidents: related.length, fixes: fixes.length, path: outPath },
135
+ "runbook-updater: runbook updated"
136
+ );
137
+ } catch (err) {
138
+ logger.warn({ err, tag: pm.tag }, "runbook-updater: failed to update runbook");
139
+ }
140
+ }
141
+ export {
142
+ updateRunbookFromPostmortem
143
+ };
@@ -0,0 +1,194 @@
1
+ import { spawn, execFile } from "child_process";
2
+ import { promisify } from "util";
3
+ import { realpathSync } from "fs";
4
+ import { tmpdir } from "os";
5
+ import logger from "../sensor/logger.js";
6
+ const execFileAsync = promisify(execFile);
7
+ function getRequestedSandboxBackend() {
8
+ const v = process.env.MERGEN_SANDBOX;
9
+ return v === "docker" || v === "seatbelt" ? v : null;
10
+ }
11
+ function isSandboxRequested() {
12
+ return getRequestedSandboxBackend() !== null;
13
+ }
14
+ function resolveAllowNetwork(requested) {
15
+ const raw = requested ?? process.env.MERGEN_SANDBOX_NETWORK ?? "none";
16
+ if (raw === "bridge") return true;
17
+ if (raw !== "none") {
18
+ logger.warn(
19
+ { value: raw },
20
+ `sandbox: MERGEN_SANDBOX_NETWORK="${raw}" is not a recognized value ('none' or 'bridge') \u2014 failing closed to no network access.`
21
+ );
22
+ }
23
+ return false;
24
+ }
25
+ async function isDockerAvailable() {
26
+ try {
27
+ await execFileAsync("docker", ["info", "--format", "{{.ServerVersion}}"], { timeout: 5e3 });
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+ async function isDockerRuntimeAvailable(runtime) {
34
+ try {
35
+ const { stdout } = await execFileAsync("docker", ["info", "--format", "{{json .Runtimes}}"], { timeout: 5e3 });
36
+ const runtimes = JSON.parse(stdout || "{}");
37
+ return runtime in runtimes;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+ async function isMacSandboxAvailable() {
43
+ if (process.platform !== "darwin") return false;
44
+ try {
45
+ await execFileAsync("sandbox-exec", ["-p", "(version 1)(allow default)", "/usr/bin/true"], { timeout: 5e3 });
46
+ return true;
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+ function getMacSandboxCapability() {
52
+ const requested = getRequestedSandboxBackend() === "seatbelt";
53
+ return { requested, platform: process.platform, supported: requested && process.platform === "darwin" };
54
+ }
55
+ function sbplStringLiteral(s) {
56
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
57
+ }
58
+ function resolvePathForProfile(p) {
59
+ try {
60
+ return realpathSync(p);
61
+ } catch {
62
+ return p;
63
+ }
64
+ }
65
+ function buildSeatbeltProfile(opts) {
66
+ const writeAllows = opts.writablePaths.map(resolvePathForProfile).map((p) => ` (subpath "${sbplStringLiteral(p)}")`).join("\n");
67
+ return [
68
+ "(version 1)",
69
+ "(deny default)",
70
+ "(allow file-read*)",
71
+ "(allow process-fork)",
72
+ "(allow process-exec)",
73
+ "(allow signal (target self))",
74
+ "(allow sysctl-read)",
75
+ "(allow mach-lookup)",
76
+ "(allow iokit-open)",
77
+ // Almost every real invocation redirects stdout/stderr or a sink
78
+ // argument through /dev/null at some point (`cmd > /dev/null 2>&1`,
79
+ // `curl -o /dev/null`, ...) — without this, file-write* alone leaves
80
+ // /dev/null unwritable and such commands fail with a confusing
81
+ // "Operation not permitted" that has nothing to do with the actual
82
+ // workspace boundary being enforced. Not a meaningful security
83
+ // boundary either way, so it's unconditional rather than tied to
84
+ // writablePaths.
85
+ '(allow file-write-data (literal "/dev/null"))',
86
+ "(allow file-write*",
87
+ writeAllows,
88
+ ")",
89
+ opts.allowNetwork ? "(allow network*)" : ";; network denied by default (MERGEN_SANDBOX_NETWORK=bridge to allow)"
90
+ ].filter(Boolean).join("\n");
91
+ }
92
+ function extraWritablePathsFromEnv() {
93
+ return (process.env.MERGEN_SANDBOX_EXTRA_WRITE_PATHS ?? "").split(",").map((p) => p.trim()).filter(Boolean);
94
+ }
95
+ function directSpawn(cmd, args, opts) {
96
+ return {
97
+ child: spawn(cmd, args, { stdio: opts.stdio ?? "inherit", shell: false, cwd: opts.cwd, env: opts.env }),
98
+ sandboxed: false,
99
+ backend: null
100
+ };
101
+ }
102
+ async function spawnSandboxed(cmd, args, opts) {
103
+ const backend = getRequestedSandboxBackend();
104
+ if (!backend) return directSpawn(cmd, args, opts);
105
+ if (backend === "docker") {
106
+ const available2 = await isDockerAvailable();
107
+ if (!available2) {
108
+ logger.warn(
109
+ "sandbox: MERGEN_SANDBOX=docker is set but Docker is not available (not installed or daemon not running) \u2014 falling back to direct host execution, UNSANDBOXED. Start Docker to get the isolation you opted into."
110
+ );
111
+ return directSpawn(cmd, args, opts);
112
+ }
113
+ const image = opts.image ?? process.env.MERGEN_SANDBOX_IMAGE ?? "alpine:3.20";
114
+ const network = resolveAllowNetwork(opts.network) ? "bridge" : "none";
115
+ const capArgs = ["--cap-drop=ALL"];
116
+ const requestedRuntime = opts.dockerRuntime ?? process.env.MERGEN_SANDBOX_DOCKER_RUNTIME;
117
+ const runtimeArgs = [];
118
+ if (requestedRuntime) {
119
+ if (await isDockerRuntimeAvailable(requestedRuntime)) {
120
+ runtimeArgs.push("--runtime", requestedRuntime);
121
+ } else {
122
+ logger.warn(
123
+ { requestedRuntime },
124
+ `sandbox: MERGEN_SANDBOX_DOCKER_RUNTIME=${requestedRuntime} is not registered with this Docker daemon \u2014 running under the default runtime (runc) instead. Install gVisor and register it as a Docker runtime to use it.`
125
+ );
126
+ }
127
+ }
128
+ const envArgs = [];
129
+ for (const [key, value] of Object.entries(opts.env ?? {})) {
130
+ if (value === void 0) continue;
131
+ envArgs.push("-e", `${key}=${value}`);
132
+ }
133
+ const dockerArgs = [
134
+ "run",
135
+ "--rm",
136
+ "--read-only",
137
+ "--network",
138
+ network,
139
+ ...capArgs,
140
+ ...runtimeArgs,
141
+ ...envArgs,
142
+ "-v",
143
+ `${opts.cwd}:/workspace`,
144
+ "-w",
145
+ "/workspace",
146
+ image,
147
+ cmd,
148
+ ...args
149
+ ];
150
+ logger.info({ image, network, runtime: requestedRuntime && runtimeArgs.length > 0 ? requestedRuntime : "runc", cmd }, "sandbox: running command inside Docker container");
151
+ return {
152
+ child: spawn("docker", dockerArgs, { stdio: opts.stdio ?? "inherit", shell: false, env: opts.env }),
153
+ sandboxed: true,
154
+ backend: "docker"
155
+ };
156
+ }
157
+ if (process.platform !== "darwin") {
158
+ logger.warn(
159
+ "sandbox: MERGEN_SANDBOX=seatbelt is set but this is not macOS \u2014 sandbox-exec is a macOS-only tool. Falling back to direct host execution, UNSANDBOXED."
160
+ );
161
+ return directSpawn(cmd, args, opts);
162
+ }
163
+ const available = await isMacSandboxAvailable();
164
+ if (!available) {
165
+ logger.warn(
166
+ "sandbox: MERGEN_SANDBOX=seatbelt is set but sandbox-exec is not usable on this machine (missing, or blocked by an MDM/system policy). Falling back to direct host execution, UNSANDBOXED."
167
+ );
168
+ return directSpawn(cmd, args, opts);
169
+ }
170
+ const allowNetwork = resolveAllowNetwork(opts.network);
171
+ const writablePaths = [opts.cwd, tmpdir(), ...opts.extraWritablePaths ?? extraWritablePathsFromEnv()];
172
+ const profile = buildSeatbeltProfile({ writablePaths, allowNetwork });
173
+ logger.info({ network: allowNetwork ? "bridge" : "none", cmd, writablePaths }, "sandbox: running command inside macOS Seatbelt (sandbox-exec)");
174
+ return {
175
+ child: spawn("sandbox-exec", ["-p", profile, cmd, ...args], {
176
+ stdio: opts.stdio ?? "inherit",
177
+ shell: false,
178
+ cwd: opts.cwd,
179
+ env: opts.env
180
+ }),
181
+ sandboxed: true,
182
+ backend: "seatbelt"
183
+ };
184
+ }
185
+ export {
186
+ buildSeatbeltProfile,
187
+ getMacSandboxCapability,
188
+ getRequestedSandboxBackend,
189
+ isDockerAvailable,
190
+ isDockerRuntimeAvailable,
191
+ isMacSandboxAvailable,
192
+ isSandboxRequested,
193
+ spawnSandboxed
194
+ };
@@ -0,0 +1,104 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { execFile } from "child_process";
4
+ import { promisify } from "util";
5
+ import { createHash } from "crypto";
6
+ const execFileAsync = promisify(execFile);
7
+ const _gitTrackedCache = /* @__PURE__ */ new Map();
8
+ const GIT_TRACKED_CACHE_TTL_MS = 3e4;
9
+ const INLINE_CODE_FLAGS = /* @__PURE__ */ new Set(["-c", "-e", "-r", "-m"]);
10
+ const SCRIPT_INTERPRETERS = /* @__PURE__ */ new Set([
11
+ "bash",
12
+ "sh",
13
+ "zsh",
14
+ "ksh",
15
+ "dash",
16
+ "python",
17
+ "python3",
18
+ "python2",
19
+ "node",
20
+ "ruby",
21
+ "perl",
22
+ "php",
23
+ "pwsh",
24
+ "powershell"
25
+ ]);
26
+ function recentModificationWindowMs() {
27
+ const envValue = process.env.MERGEN_SCRIPT_TRUST_WINDOW_SECONDS;
28
+ if (envValue === void 0) return 3e4;
29
+ const raw = Number(envValue);
30
+ return (Number.isFinite(raw) && raw >= 0 ? raw : 30) * 1e3;
31
+ }
32
+ function detectScriptInvocation(cmd) {
33
+ const interpreter = path.basename(cmd.name).toLowerCase();
34
+ if (!SCRIPT_INTERPRETERS.has(interpreter)) return null;
35
+ if (cmd.args.some((a) => INLINE_CODE_FLAGS.has(a))) return null;
36
+ const firstNonFlag = cmd.args.find((a) => !a.startsWith("-"));
37
+ if (!firstNonFlag) return null;
38
+ return { interpreter, scriptPath: firstNonFlag };
39
+ }
40
+ function detectScriptInvocationFromRawText(raw) {
41
+ const segments = raw.split(/[;&|()\n]+/);
42
+ for (const segment of segments) {
43
+ const tokens = segment.trim().split(/\s+/).filter(Boolean);
44
+ for (let i = 0; i < tokens.length; i++) {
45
+ const interpreter = path.basename(tokens[i]).toLowerCase();
46
+ if (!SCRIPT_INTERPRETERS.has(interpreter)) continue;
47
+ const rest = tokens.slice(i + 1);
48
+ if (rest.some((t) => INLINE_CODE_FLAGS.has(t))) continue;
49
+ const firstNonFlag = rest.find((t) => !t.startsWith("-"));
50
+ if (firstNonFlag) return { interpreter, scriptPath: firstNonFlag };
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+ async function evaluateScriptTrust(scriptPath, cwd) {
56
+ const reasons = [];
57
+ const resolved = path.resolve(cwd, scriptPath);
58
+ const relativeToRoot = path.relative(cwd, resolved);
59
+ if (relativeToRoot.startsWith("..") || path.isAbsolute(relativeToRoot)) {
60
+ reasons.push(`script path resolves outside the working directory: ${resolved}`);
61
+ }
62
+ let contentHash = null;
63
+ try {
64
+ const stat = await fs.promises.stat(resolved);
65
+ const ageMs = Date.now() - stat.mtimeMs;
66
+ const windowMs = recentModificationWindowMs();
67
+ if (ageMs < windowMs) {
68
+ reasons.push(`script was modified ${Math.max(0, Math.round(ageMs / 1e3))}s ago \u2014 inside the ${Math.round(windowMs / 1e3)}s "just generated" window`);
69
+ }
70
+ const buf = await fs.promises.readFile(resolved);
71
+ contentHash = createHash("sha256").update(buf).digest("hex");
72
+ } catch {
73
+ reasons.push(`script file could not be found or read: ${resolved}`);
74
+ }
75
+ const cacheKey = contentHash !== null ? `${resolved}:${contentHash}` : null;
76
+ const cached = cacheKey !== null ? _gitTrackedCache.get(cacheKey) : void 0;
77
+ if (cached && cached.expiresAt > Date.now()) {
78
+ if (cached.untrackedReason) reasons.push(cached.untrackedReason);
79
+ } else {
80
+ let untrackedReason = null;
81
+ try {
82
+ await execFileAsync("git", ["ls-files", "--error-unmatch", resolved], { cwd, timeout: 5e3 });
83
+ } catch {
84
+ untrackedReason = "script is not tracked by git (untracked file, or not inside a git repository)";
85
+ }
86
+ if (!untrackedReason) {
87
+ try {
88
+ await execFileAsync("git", ["diff", "--quiet", "HEAD", "--", resolved], { cwd, timeout: 5e3 });
89
+ } catch {
90
+ untrackedReason = "script content differs from the git HEAD version (uncommitted or unreviewed edit)";
91
+ }
92
+ }
93
+ if (untrackedReason) reasons.push(untrackedReason);
94
+ if (cacheKey !== null) {
95
+ _gitTrackedCache.set(cacheKey, { untrackedReason, expiresAt: Date.now() + GIT_TRACKED_CACHE_TTL_MS });
96
+ }
97
+ }
98
+ return { trusted: reasons.length === 0, reasons };
99
+ }
100
+ export {
101
+ detectScriptInvocation,
102
+ detectScriptInvocationFromRawText,
103
+ evaluateScriptTrust
104
+ };
@@ -0,0 +1,67 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { DATA_DIR } from "../sensor/paths.js";
4
+ const METRICS_FILE = path.join(DATA_DIR, "session-metrics.json");
5
+ const MAX_SESSIONS = 500;
6
+ let _sessions = [];
7
+ let _loaded = false;
8
+ function load() {
9
+ if (_loaded) return;
10
+ _loaded = true;
11
+ try {
12
+ if (!fs.existsSync(METRICS_FILE)) return;
13
+ const parsed = JSON.parse(fs.readFileSync(METRICS_FILE, "utf8"));
14
+ if (parsed?.version === 1 && Array.isArray(parsed.sessions)) {
15
+ _sessions = parsed.sessions.slice(-MAX_SESSIONS);
16
+ }
17
+ } catch {
18
+ }
19
+ }
20
+ function persist() {
21
+ try {
22
+ fs.mkdirSync(DATA_DIR, { recursive: true });
23
+ fs.writeFileSync(METRICS_FILE, JSON.stringify({ version: 1, sessions: _sessions }), "utf8");
24
+ } catch {
25
+ }
26
+ }
27
+ function startSession(pid, tag) {
28
+ load();
29
+ if (_sessions.some((s) => s.pid === pid)) return;
30
+ _sessions.push({ pid, tag, startedAt: Date.now() });
31
+ if (_sessions.length > MAX_SESSIONS) _sessions = _sessions.slice(-MAX_SESSIONS);
32
+ persist();
33
+ }
34
+ function closeSession(pid, outcome) {
35
+ load();
36
+ const session = _sessions.find((s) => s.pid === pid && !s.firstOutcome);
37
+ if (!session) return;
38
+ session.firstValidateAt = Date.now();
39
+ session.firstOutcome = outcome;
40
+ persist();
41
+ }
42
+ function getSessionMetrics() {
43
+ load();
44
+ const withOutcome = _sessions.filter((s) => s.firstOutcome);
45
+ const resolved = withOutcome.filter((s) => s.firstOutcome === "resolved").length;
46
+ const partial = withOutcome.filter((s) => s.firstOutcome === "partial").length;
47
+ const unresolved = withOutcome.filter((s) => s.firstOutcome === "unresolved").length;
48
+ return {
49
+ total: _sessions.length,
50
+ withOutcome: withOutcome.length,
51
+ firstAttemptResolved: resolved,
52
+ firstAttemptPartial: partial,
53
+ firstAttemptUnresolved: unresolved,
54
+ firstAttemptSuccessRate: withOutcome.length >= 3 ? (resolved + partial * 0.5) / withOutcome.length : null,
55
+ recentSessions: _sessions.filter((s) => s.firstOutcome).slice(-10).reverse().map((s) => ({ tag: s.tag, outcome: s.firstOutcome, startedAt: s.startedAt }))
56
+ };
57
+ }
58
+ function _resetSessionMetricsForTesting() {
59
+ _sessions = [];
60
+ _loaded = true;
61
+ }
62
+ export {
63
+ _resetSessionMetricsForTesting,
64
+ closeSession,
65
+ getSessionMetrics,
66
+ startSession
67
+ };