thumbgate 0.9.10

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 (364) hide show
  1. package/.claude-plugin/README.md +134 -0
  2. package/.claude-plugin/bundle/icon.png +0 -0
  3. package/.claude-plugin/bundle/icon.svg +18 -0
  4. package/.claude-plugin/bundle/server/index.js +24 -0
  5. package/.claude-plugin/marketplace.json +36 -0
  6. package/.claude-plugin/plugin.json +21 -0
  7. package/.well-known/mcp/server-card.json +231 -0
  8. package/LICENSE +21 -0
  9. package/README.md +375 -0
  10. package/adapters/README.md +9 -0
  11. package/adapters/amp/skills/thumbgate-feedback/SKILL.md +22 -0
  12. package/adapters/chatgpt/INSTALL.md +83 -0
  13. package/adapters/chatgpt/openapi.yaml +1281 -0
  14. package/adapters/claude/.mcp.json +14 -0
  15. package/adapters/codex/config.toml +9 -0
  16. package/adapters/gemini/function-declarations.json +224 -0
  17. package/adapters/mcp/server-stdio.js +788 -0
  18. package/adapters/opencode/opencode.json +15 -0
  19. package/bin/cli.js +1484 -0
  20. package/bin/memory.sh +64 -0
  21. package/bin/obsidian-sync.sh +20 -0
  22. package/bin/postinstall.js +37 -0
  23. package/config/build-metadata.json +4 -0
  24. package/config/e2e-critical-flows.json +45 -0
  25. package/config/gate-templates.json +77 -0
  26. package/config/gates/claim-verification.json +29 -0
  27. package/config/gates/computer-use.json +39 -0
  28. package/config/gates/default.json +117 -0
  29. package/config/github-about.json +25 -0
  30. package/config/mcp-allowlists.json +135 -0
  31. package/config/model-tiers.json +33 -0
  32. package/config/partner-routing.json +132 -0
  33. package/config/policy-bundles/constrained-v1.json +64 -0
  34. package/config/policy-bundles/default-v1.json +91 -0
  35. package/config/rubrics/default-v1.json +52 -0
  36. package/config/skill-packs/react-testing.json +23 -0
  37. package/config/skill-packs/stripe-integration/references/api-spec.json +1 -0
  38. package/config/skill-packs/stripe-integration/references/webhook-guide.md +3 -0
  39. package/config/skill-specs/pr-reviewer.json +9 -0
  40. package/config/skill-specs/release-status.json +9 -0
  41. package/config/skill-specs/ticket-triage.json +9 -0
  42. package/config/subagent-profiles.json +32 -0
  43. package/config/tessl-tiles.json +29 -0
  44. package/config/thumbgate-settings.managed.json +12 -0
  45. package/openapi/openapi.yaml +1281 -0
  46. package/package.json +283 -0
  47. package/plugins/amp-skill/INSTALL.md +52 -0
  48. package/plugins/amp-skill/SKILL.md +64 -0
  49. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +22 -0
  50. package/plugins/claude-codex-bridge/.mcp.json +12 -0
  51. package/plugins/claude-codex-bridge/INSTALL.md +43 -0
  52. package/plugins/claude-codex-bridge/README.md +46 -0
  53. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +288 -0
  54. package/plugins/claude-codex-bridge/skills/adversarial-review/SKILL.md +24 -0
  55. package/plugins/claude-codex-bridge/skills/result/SKILL.md +22 -0
  56. package/plugins/claude-codex-bridge/skills/review/SKILL.md +28 -0
  57. package/plugins/claude-codex-bridge/skills/second-pass/SKILL.md +27 -0
  58. package/plugins/claude-codex-bridge/skills/setup/SKILL.md +21 -0
  59. package/plugins/claude-codex-bridge/skills/status/SKILL.md +19 -0
  60. package/plugins/claude-skill/INSTALL.md +55 -0
  61. package/plugins/claude-skill/SKILL.md +46 -0
  62. package/plugins/codex-profile/.codex-plugin/plugin.json +43 -0
  63. package/plugins/codex-profile/.mcp.json +12 -0
  64. package/plugins/codex-profile/AGENTS.md +20 -0
  65. package/plugins/codex-profile/INSTALL.md +66 -0
  66. package/plugins/codex-profile/README.md +37 -0
  67. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +23 -0
  68. package/plugins/cursor-marketplace/CHANGELOG.md +30 -0
  69. package/plugins/cursor-marketplace/LICENSE +21 -0
  70. package/plugins/cursor-marketplace/README.md +124 -0
  71. package/plugins/cursor-marketplace/agents/reliability-reviewer.md +31 -0
  72. package/plugins/cursor-marketplace/assets/logo-400x400.png +0 -0
  73. package/plugins/cursor-marketplace/commands/capture-feedback.md +33 -0
  74. package/plugins/cursor-marketplace/commands/check-gates.md +25 -0
  75. package/plugins/cursor-marketplace/commands/show-lessons.md +27 -0
  76. package/plugins/cursor-marketplace/hooks/hooks.json +10 -0
  77. package/plugins/cursor-marketplace/mcp.json +12 -0
  78. package/plugins/cursor-marketplace/rules/feedback-capture.mdc +34 -0
  79. package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +30 -0
  80. package/plugins/cursor-marketplace/rules/session-continuity.mdc +28 -0
  81. package/plugins/cursor-marketplace/scripts/gate-check.sh +11 -0
  82. package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +47 -0
  83. package/plugins/cursor-marketplace/skills/prevention-rules/SKILL.md +31 -0
  84. package/plugins/cursor-marketplace/skills/recall-context/SKILL.md +30 -0
  85. package/plugins/cursor-marketplace/skills/search-lessons/SKILL.md +33 -0
  86. package/plugins/gemini-extension/INSTALL.md +92 -0
  87. package/plugins/gemini-extension/gemini_prompt.txt +14 -0
  88. package/plugins/gemini-extension/tool_contract.json +45 -0
  89. package/plugins/opencode-profile/INSTALL.md +57 -0
  90. package/public/assets/instagram-card.png +0 -0
  91. package/public/assets/tiktok-agent-memory.mp4 +0 -0
  92. package/public/blog.html +400 -0
  93. package/public/dashboard.html +1093 -0
  94. package/public/guide.html +317 -0
  95. package/public/index.html +1014 -0
  96. package/public/learn/agent-harness-pattern.html +180 -0
  97. package/public/learn/ai-agent-persistent-memory.html +202 -0
  98. package/public/learn/learn.css +45 -0
  99. package/public/learn/mcp-pre-action-gates-explained.html +172 -0
  100. package/public/learn/stop-ai-agent-force-push.html +134 -0
  101. package/public/learn/vibe-coding-safety-net.html +142 -0
  102. package/public/learn.html +213 -0
  103. package/public/lessons.html +650 -0
  104. package/public/vercel.json +8 -0
  105. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  106. package/scripts/a2ui-engine.js +73 -0
  107. package/scripts/access-anomaly-detector.js +12 -0
  108. package/scripts/adk-consolidator.js +266 -0
  109. package/scripts/agent-readiness.js +220 -0
  110. package/scripts/agent-security-hardening.js +227 -0
  111. package/scripts/agentic-data-pipeline.js +847 -0
  112. package/scripts/analytics-report.js +328 -0
  113. package/scripts/analytics-window.js +158 -0
  114. package/scripts/async-job-runner.js +1001 -0
  115. package/scripts/audit-trail.js +398 -0
  116. package/scripts/auto-promote-gates.js +299 -0
  117. package/scripts/auto-wire-hooks.js +312 -0
  118. package/scripts/autonomous-sales-agent.js +39 -0
  119. package/scripts/autoresearch-runner.js +216 -0
  120. package/scripts/background-agent-governance.js +237 -0
  121. package/scripts/behavioral-extraction.js +97 -0
  122. package/scripts/belief-update.js +84 -0
  123. package/scripts/billing.js +2438 -0
  124. package/scripts/bot-detector.js +50 -0
  125. package/scripts/budget-guard.js +173 -0
  126. package/scripts/build-claude-mcpb.js +189 -0
  127. package/scripts/build-metadata.js +97 -0
  128. package/scripts/check-congruence.js +322 -0
  129. package/scripts/cli-feedback.js +135 -0
  130. package/scripts/cli-telemetry.js +87 -0
  131. package/scripts/cloudflare-dynamic-sandbox.js +315 -0
  132. package/scripts/code-reasoning.js +350 -0
  133. package/scripts/codegraph-context.js +466 -0
  134. package/scripts/commercial-offer.js +56 -0
  135. package/scripts/computer-use-firewall.js +250 -0
  136. package/scripts/context-engine.js +694 -0
  137. package/scripts/contextfs.js +1287 -0
  138. package/scripts/conversation-context.js +119 -0
  139. package/scripts/creator-campaigns.js +239 -0
  140. package/scripts/daemon-manager.js +108 -0
  141. package/scripts/daily-digest.js +11 -0
  142. package/scripts/dashboard-render-spec.js +395 -0
  143. package/scripts/dashboard.js +1058 -0
  144. package/scripts/data-governance.js +173 -0
  145. package/scripts/delegation-runtime.js +900 -0
  146. package/scripts/deploy-gcp.sh +44 -0
  147. package/scripts/deploy-policy.js +263 -0
  148. package/scripts/disagreement-mining.js +315 -0
  149. package/scripts/dispatch-brief.js +159 -0
  150. package/scripts/distribution-surfaces.js +44 -0
  151. package/scripts/dpo-optimizer.js +209 -0
  152. package/scripts/ephemeral-agent-store.js +219 -0
  153. package/scripts/eval-harness.js +56 -0
  154. package/scripts/evolution-state.js +241 -0
  155. package/scripts/experiment-tracker.js +267 -0
  156. package/scripts/export-databricks-bundle.js +242 -0
  157. package/scripts/export-dpo-pairs.js +345 -0
  158. package/scripts/export-kto-pairs.js +310 -0
  159. package/scripts/export-training.js +448 -0
  160. package/scripts/failure-diagnostics.js +558 -0
  161. package/scripts/feedback-attribution.js +313 -0
  162. package/scripts/feedback-fallback.js +111 -0
  163. package/scripts/feedback-history-distiller.js +391 -0
  164. package/scripts/feedback-inbox-read.js +162 -0
  165. package/scripts/feedback-loop.js +1887 -0
  166. package/scripts/feedback-paths.js +145 -0
  167. package/scripts/feedback-quality.js +139 -0
  168. package/scripts/feedback-root-consolidator.js +238 -0
  169. package/scripts/feedback-schema.js +426 -0
  170. package/scripts/feedback-session.js +286 -0
  171. package/scripts/feedback-to-memory.js +185 -0
  172. package/scripts/feedback-to-rules.js +163 -0
  173. package/scripts/filesystem-search.js +404 -0
  174. package/scripts/funnel-analytics.js +35 -0
  175. package/scripts/gate-satisfy.js +42 -0
  176. package/scripts/gate-stats.js +116 -0
  177. package/scripts/gate-templates.js +70 -0
  178. package/scripts/gates-engine.js +816 -0
  179. package/scripts/generate-paperbanana-diagrams.sh +99 -0
  180. package/scripts/generate-pretool-hook.sh +40 -0
  181. package/scripts/github-about.js +350 -0
  182. package/scripts/github-outreach.js +65 -0
  183. package/scripts/gtm-revenue-loop.js +520 -0
  184. package/scripts/hallucination-detector.js +226 -0
  185. package/scripts/hf-papers.js +317 -0
  186. package/scripts/history-distiller.js +200 -0
  187. package/scripts/hook-auto-capture.sh +95 -0
  188. package/scripts/hook-stop-pr-thread-check.sh +68 -0
  189. package/scripts/hook-stop-self-score.sh +51 -0
  190. package/scripts/hook-stop-verify-deploy.sh +31 -0
  191. package/scripts/hook-thumbgate-cache-updater.js +48 -0
  192. package/scripts/hook-verify-before-done.sh +20 -0
  193. package/scripts/hosted-config.js +170 -0
  194. package/scripts/hybrid-feedback-context.js +676 -0
  195. package/scripts/install-mcp.js +159 -0
  196. package/scripts/intent-router.js +392 -0
  197. package/scripts/internal-agent-bootstrap.js +490 -0
  198. package/scripts/jsonl-watcher.js +155 -0
  199. package/scripts/lesson-db.js +613 -0
  200. package/scripts/lesson-inference.js +315 -0
  201. package/scripts/lesson-retrieval.js +95 -0
  202. package/scripts/lesson-rotation.js +137 -0
  203. package/scripts/lesson-search.js +644 -0
  204. package/scripts/lesson-synthesis.js +196 -0
  205. package/scripts/license.js +50 -0
  206. package/scripts/local-model-profile.js +383 -0
  207. package/scripts/markdown-escape.js +12 -0
  208. package/scripts/marketing-experiment.js +671 -0
  209. package/scripts/mcp-config.js +149 -0
  210. package/scripts/mcp-policy.js +99 -0
  211. package/scripts/memalign-recall.js +111 -0
  212. package/scripts/memory-firewall.js +222 -0
  213. package/scripts/memory-migration.js +296 -0
  214. package/scripts/meta-policy.js +194 -0
  215. package/scripts/metered-billing.js +16 -0
  216. package/scripts/model-tier-router.js +301 -0
  217. package/scripts/money-watcher.js +71 -0
  218. package/scripts/multi-hop-recall.js +240 -0
  219. package/scripts/natural-language-harness.js +330 -0
  220. package/scripts/obsidian-export.js +712 -0
  221. package/scripts/operational-dashboard.js +103 -0
  222. package/scripts/operational-summary.js +93 -0
  223. package/scripts/optimize-context.js +17 -0
  224. package/scripts/org-dashboard.js +201 -0
  225. package/scripts/partner-orchestration.js +146 -0
  226. package/scripts/per-step-scoring.js +165 -0
  227. package/scripts/perplexity-marketing.js +466 -0
  228. package/scripts/pii-scanner.js +153 -0
  229. package/scripts/plan-gate.js +154 -0
  230. package/scripts/post-everywhere.js +308 -0
  231. package/scripts/post-to-x-retry.sh +22 -0
  232. package/scripts/post-to-x.js +369 -0
  233. package/scripts/pr-manager.js +236 -0
  234. package/scripts/predictive-insights.js +356 -0
  235. package/scripts/principle-extractor.js +162 -0
  236. package/scripts/pro-features.js +40 -0
  237. package/scripts/pro-local-dashboard.js +174 -0
  238. package/scripts/problem-detail.js +53 -0
  239. package/scripts/product-feedback.js +134 -0
  240. package/scripts/profile-router.js +245 -0
  241. package/scripts/prompt-dlp.js +221 -0
  242. package/scripts/prompt-guard.js +83 -0
  243. package/scripts/prove-adapters.js +863 -0
  244. package/scripts/prove-attribution.js +365 -0
  245. package/scripts/prove-automation.js +653 -0
  246. package/scripts/prove-autoresearch.js +304 -0
  247. package/scripts/prove-claim-verification.js +277 -0
  248. package/scripts/prove-cloudflare-sandbox.js +163 -0
  249. package/scripts/prove-data-pipeline.js +410 -0
  250. package/scripts/prove-data-quality.js +227 -0
  251. package/scripts/prove-evolution.js +352 -0
  252. package/scripts/prove-harnesses.js +287 -0
  253. package/scripts/prove-intelligence.js +259 -0
  254. package/scripts/prove-lancedb.js +371 -0
  255. package/scripts/prove-local-intelligence.js +342 -0
  256. package/scripts/prove-loop-closure.js +263 -0
  257. package/scripts/prove-predictive-insights.js +357 -0
  258. package/scripts/prove-runtime.js +350 -0
  259. package/scripts/prove-seo-gsd.js +234 -0
  260. package/scripts/prove-settings.js +279 -0
  261. package/scripts/prove-subway-upgrades.js +277 -0
  262. package/scripts/prove-tessl.js +229 -0
  263. package/scripts/prove-training-export.js +327 -0
  264. package/scripts/prove-workflow-contract.js +116 -0
  265. package/scripts/prove-xmemory.js +332 -0
  266. package/scripts/publish-decision.js +133 -0
  267. package/scripts/pulse.js +80 -0
  268. package/scripts/rate-limiter.js +125 -0
  269. package/scripts/reddit-dm-outreach.js +182 -0
  270. package/scripts/reddit-monitor-cron.sh +26 -0
  271. package/scripts/reflector-agent.js +221 -0
  272. package/scripts/reminder-engine.js +132 -0
  273. package/scripts/revenue-status.js +472 -0
  274. package/scripts/risk-scorer.js +458 -0
  275. package/scripts/rlaif-self-audit.js +129 -0
  276. package/scripts/rubric-engine.js +230 -0
  277. package/scripts/schedule-manager.js +251 -0
  278. package/scripts/secret-scanner.js +414 -0
  279. package/scripts/self-heal.js +147 -0
  280. package/scripts/self-healing-check.js +188 -0
  281. package/scripts/semantic-layer.js +98 -0
  282. package/scripts/seo-gsd.js +1153 -0
  283. package/scripts/settings-hierarchy.js +214 -0
  284. package/scripts/shieldcortex-memory-firewall-runner.mjs +53 -0
  285. package/scripts/skill-exporter.js +262 -0
  286. package/scripts/skill-generator.js +446 -0
  287. package/scripts/skill-materializer.js +134 -0
  288. package/scripts/skill-packs.js +136 -0
  289. package/scripts/skill-proposer.js +99 -0
  290. package/scripts/skill-quality-tracker.js +284 -0
  291. package/scripts/slo-alert-engine.js +14 -0
  292. package/scripts/slow-loop.js +72 -0
  293. package/scripts/social-analytics/db/schema.sql +32 -0
  294. package/scripts/social-analytics/digest.js +256 -0
  295. package/scripts/social-analytics/generate-instagram-card.js +97 -0
  296. package/scripts/social-analytics/instagram-thumbgate-post.js +73 -0
  297. package/scripts/social-analytics/mcp-server.js +289 -0
  298. package/scripts/social-analytics/normalizer.js +580 -0
  299. package/scripts/social-analytics/notify.js +162 -0
  300. package/scripts/social-analytics/poll-all.js +107 -0
  301. package/scripts/social-analytics/pollers/github.js +195 -0
  302. package/scripts/social-analytics/pollers/instagram.js +253 -0
  303. package/scripts/social-analytics/pollers/linkedin.js +330 -0
  304. package/scripts/social-analytics/pollers/plausible.js +247 -0
  305. package/scripts/social-analytics/pollers/reddit.js +306 -0
  306. package/scripts/social-analytics/pollers/threads.js +233 -0
  307. package/scripts/social-analytics/pollers/tiktok.js +203 -0
  308. package/scripts/social-analytics/pollers/x.js +227 -0
  309. package/scripts/social-analytics/pollers/youtube.js +304 -0
  310. package/scripts/social-analytics/pollers/zernio.js +180 -0
  311. package/scripts/social-analytics/publish-instagram-thumbgate.js +85 -0
  312. package/scripts/social-analytics/publishers/devto.js +122 -0
  313. package/scripts/social-analytics/publishers/instagram.js +317 -0
  314. package/scripts/social-analytics/publishers/linkedin.js +294 -0
  315. package/scripts/social-analytics/publishers/reddit.js +390 -0
  316. package/scripts/social-analytics/publishers/threads.js +275 -0
  317. package/scripts/social-analytics/publishers/tiktok.js +217 -0
  318. package/scripts/social-analytics/publishers/x.js +259 -0
  319. package/scripts/social-analytics/publishers/youtube.js +223 -0
  320. package/scripts/social-analytics/publishers/zernio.js +209 -0
  321. package/scripts/social-analytics/run-digest.js +34 -0
  322. package/scripts/social-analytics/store.js +257 -0
  323. package/scripts/social-analytics/utm.js +143 -0
  324. package/scripts/social-pipeline.js +2628 -0
  325. package/scripts/social-quality-gate.js +18 -0
  326. package/scripts/social-reply-monitor.js +445 -0
  327. package/scripts/status-dashboard.js +155 -0
  328. package/scripts/statusline-lesson.js +16 -0
  329. package/scripts/statusline-tower.js +8 -0
  330. package/scripts/statusline.sh +116 -0
  331. package/scripts/stripe-live-status.js +115 -0
  332. package/scripts/subagent-profiles.js +79 -0
  333. package/scripts/sync-gh-secrets-from-env.sh +70 -0
  334. package/scripts/sync-github-about.js +52 -0
  335. package/scripts/sync-version.js +451 -0
  336. package/scripts/synthetic-dpo.js +234 -0
  337. package/scripts/telemetry-analytics.js +821 -0
  338. package/scripts/tessl-export.js +371 -0
  339. package/scripts/test-coverage.js +120 -0
  340. package/scripts/thompson-sampling.js +417 -0
  341. package/scripts/thumbgate-search.js +189 -0
  342. package/scripts/tool-kpi-tracker.js +12 -0
  343. package/scripts/tool-registry.js +811 -0
  344. package/scripts/train_from_feedback.py +910 -0
  345. package/scripts/user-profile.js +78 -0
  346. package/scripts/validate-feedback.js +580 -0
  347. package/scripts/validate-workflow-contract.js +287 -0
  348. package/scripts/vector-store.js +198 -0
  349. package/scripts/verification-loop.js +291 -0
  350. package/scripts/verify-obsidian-setup.sh +269 -0
  351. package/scripts/verify-run.js +269 -0
  352. package/scripts/webhook-delivery.js +62 -0
  353. package/scripts/weekly-auto-post.js +124 -0
  354. package/scripts/workflow-runs.js +154 -0
  355. package/scripts/workflow-sprint-intake.js +475 -0
  356. package/scripts/workspace-evolver.js +374 -0
  357. package/scripts/x-autonomous-marketing.js +139 -0
  358. package/scripts/xmemory-lite.js +405 -0
  359. package/skills/agent-memory/SKILL.md +97 -0
  360. package/skills/solve-architecture-autonomy/SKILL.md +17 -0
  361. package/skills/solve-architecture-autonomy/tool.js +33 -0
  362. package/skills/thumbgate/SKILL.md +114 -0
  363. package/skills/thumbgate-feedback/SKILL.md +49 -0
  364. package/src/api/server.js +4208 -0
@@ -0,0 +1,398 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Audit Trail — OpenShell-inspired governance layer
6
+ *
7
+ * Records every gate decision (allow/deny/warn) into a structured audit log,
8
+ * then auto-feeds deny/warn decisions into the ThumbGate feedback pipeline as
9
+ * negative signal. This closes the loop: gate blocks → feedback capture →
10
+ * prevention rule generation → stronger gates.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const { resolveFeedbackDir } = require('./feedback-paths');
16
+
17
+ const AUDIT_LOG_FILENAME = 'audit-trail.jsonl';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Paths
21
+ // ---------------------------------------------------------------------------
22
+
23
+ function getAuditLogPath() {
24
+ return path.join(resolveFeedbackDir(), AUDIT_LOG_FILENAME);
25
+ }
26
+
27
+ function ensureDir(dirPath) {
28
+ if (!fs.existsSync(dirPath)) {
29
+ fs.mkdirSync(dirPath, { recursive: true });
30
+ }
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Core audit record
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /**
38
+ * @param {object} params
39
+ * @param {string} params.toolName — tool that was evaluated
40
+ * @param {object} params.toolInput — the tool input payload
41
+ * @param {string} params.decision — 'allow' | 'deny' | 'warn'
42
+ * @param {string} [params.gateId] — which gate matched (null for allow)
43
+ * @param {string} [params.message] — gate message
44
+ * @param {string} [params.severity] — gate severity
45
+ * @param {number} [params.latencyMs] — tool execution time in milliseconds
46
+ * @param {string} [params.source] — 'gates-engine' | 'secret-guard' | 'mcp-policy' | 'profile-router' | 'tool-latency'
47
+ * @returns {object} the stored audit record
48
+ */
49
+ function recordAuditEvent(params = {}) {
50
+ const logPath = getAuditLogPath();
51
+ ensureDir(path.dirname(logPath));
52
+
53
+ const record = {
54
+ id: `audit_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
55
+ timestamp: new Date().toISOString(),
56
+ toolName: params.toolName || 'unknown',
57
+ toolInput: sanitizeToolInput(params.toolInput || {}),
58
+ decision: params.decision || 'allow',
59
+ gateId: params.gateId || null,
60
+ message: params.message || null,
61
+ severity: params.severity || null,
62
+ latencyMs: typeof params.latencyMs === 'number' ? params.latencyMs : null,
63
+ source: params.source || 'gates-engine',
64
+ };
65
+
66
+ fs.appendFileSync(logPath, JSON.stringify(record) + '\n');
67
+ return record;
68
+ }
69
+
70
+ /**
71
+ * Strip secrets and large payloads from tool input before audit storage.
72
+ */
73
+ function sanitizeToolInput(toolInput) {
74
+ const safe = {};
75
+ const MAX_VALUE_LEN = 200;
76
+
77
+ for (const [key, value] of Object.entries(toolInput)) {
78
+ if (typeof value === 'string') {
79
+ // Never log content/new_string/old_string verbatim — could contain secrets
80
+ if (['content', 'new_string', 'old_string'].includes(key)) {
81
+ safe[key] = `[redacted:${value.length} chars]`;
82
+ } else {
83
+ safe[key] = value.length > MAX_VALUE_LEN
84
+ ? value.slice(0, MAX_VALUE_LEN) + '...'
85
+ : value;
86
+ }
87
+ } else {
88
+ safe[key] = value;
89
+ }
90
+ }
91
+ return safe;
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Auto-feedback from audit events
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /**
99
+ * Converts deny/warn audit events into ThumbGate feedback signal.
100
+ * This is the core OpenShell insight: policy decisions ARE training signal.
101
+ */
102
+ function auditToFeedback(auditRecord) {
103
+ if (auditRecord.decision === 'allow') return null;
104
+
105
+ try {
106
+ const feedbackLoop = require('./feedback-loop');
107
+ const signal = auditRecord.decision === 'deny' ? 'down' : 'down';
108
+ const context = `Gate "${auditRecord.gateId}" ${auditRecord.decision === 'deny' ? 'blocked' : 'warned'} tool "${auditRecord.toolName}": ${auditRecord.message || 'no message'}`;
109
+
110
+ return feedbackLoop.captureFeedback({
111
+ signal,
112
+ context,
113
+ what_went_wrong: `Agent attempted action blocked by policy gate: ${auditRecord.gateId}`,
114
+ what_to_change: auditRecord.message || 'Follow safety policy before attempting this action',
115
+ tags: ['audit-trail', 'auto-capture', `gate:${auditRecord.gateId}`, auditRecord.source].filter(Boolean),
116
+ title: `MISTAKE: Policy violation — ${auditRecord.gateId}`,
117
+ });
118
+ } catch {
119
+ // Feedback capture failure should never break the audit trail
120
+ return null;
121
+ }
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Read / query audit log
126
+ // ---------------------------------------------------------------------------
127
+
128
+ function readAuditLog(logPath) {
129
+ const p = logPath || getAuditLogPath();
130
+ if (!fs.existsSync(p)) return [];
131
+ const raw = fs.readFileSync(p, 'utf-8').trim();
132
+ if (!raw) return [];
133
+ return raw.split('\n').map(line => {
134
+ try { return JSON.parse(line); }
135
+ catch { return null; }
136
+ }).filter(Boolean);
137
+ }
138
+
139
+ function auditStats(logPath) {
140
+ const entries = readAuditLog(logPath);
141
+ const stats = { total: entries.length, allow: 0, deny: 0, warn: 0, byGate: {}, bySource: {} };
142
+
143
+ for (const entry of entries) {
144
+ stats[entry.decision] = (stats[entry.decision] || 0) + 1;
145
+ if (entry.gateId) {
146
+ if (!stats.byGate[entry.gateId]) stats.byGate[entry.gateId] = { deny: 0, warn: 0, allow: 0 };
147
+ stats.byGate[entry.gateId][entry.decision] = (stats.byGate[entry.gateId][entry.decision] || 0) + 1;
148
+ }
149
+ if (entry.source) {
150
+ stats.bySource[entry.source] = (stats.bySource[entry.source] || 0) + 1;
151
+ }
152
+ }
153
+
154
+ return stats;
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Skill Adherence Measurement (M2.7-inspired)
159
+ // ---------------------------------------------------------------------------
160
+
161
+ /**
162
+ * Computes skill adherence rate per tool from audit trail data.
163
+ * Adherence = allow / (allow + deny + warn) per tool.
164
+ * M2.7 tracks "97% skill adherence across 40+ skills" — this gives us the same metric.
165
+ *
166
+ * @param {string} [logPath]
167
+ * @returns {{ overall: number, byTool: Object<string, { allow: number, deny: number, warn: number, adherence: number }>, totalTools: number }}
168
+ */
169
+ function skillAdherence(logPath) {
170
+ const entries = readAuditLog(logPath);
171
+ const byTool = {};
172
+
173
+ for (const entry of entries) {
174
+ const tool = entry.toolName || 'unknown';
175
+ if (!byTool[tool]) byTool[tool] = { allow: 0, deny: 0, warn: 0 };
176
+ byTool[tool][entry.decision] = (byTool[tool][entry.decision] || 0) + 1;
177
+ }
178
+
179
+ let totalAllow = 0;
180
+ let totalAll = 0;
181
+ for (const [, counts] of Object.entries(byTool)) {
182
+ const all = counts.allow + counts.deny + counts.warn;
183
+ counts.adherence = all > 0 ? Math.round((counts.allow / all) * 10000) / 100 : 100;
184
+ totalAllow += counts.allow;
185
+ totalAll += all;
186
+ }
187
+
188
+ return {
189
+ overall: totalAll > 0 ? Math.round((totalAllow / totalAll) * 10000) / 100 : 100,
190
+ byTool,
191
+ totalTools: Object.keys(byTool).length,
192
+ };
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // Deny-triggered self-heal (M2.7 self-evolution loop)
197
+ // ---------------------------------------------------------------------------
198
+
199
+ /**
200
+ * Checks if recent audit denials exceed a threshold, triggering autonomous self-heal.
201
+ * This closes the M2.7-inspired loop: audit deny → self-heal → eval → keep/revert.
202
+ *
203
+ * @param {object} [opts]
204
+ * @param {number} [opts.windowMs=300000] — lookback window (default 5 min)
205
+ * @param {number} [opts.denyThreshold=3] — denials in window to trigger heal
206
+ * @param {string} [opts.logPath]
207
+ * @returns {{ triggered: boolean, recentDenials: number, threshold: number, healResult?: object }}
208
+ */
209
+ function evaluateSelfHealTrigger(opts = {}) {
210
+ const windowMs = opts.windowMs || 5 * 60 * 1000;
211
+ const denyThreshold = opts.denyThreshold || 3;
212
+ const entries = readAuditLog(opts.logPath);
213
+ const cutoff = Date.now() - windowMs;
214
+
215
+ const recentDenials = entries.filter(e =>
216
+ e.decision === 'deny' && new Date(e.timestamp).getTime() > cutoff
217
+ );
218
+
219
+ if (recentDenials.length < denyThreshold) {
220
+ return { triggered: false, recentDenials: recentDenials.length, threshold: denyThreshold };
221
+ }
222
+
223
+ // Threshold exceeded — trigger self-heal
224
+ let healResult = null;
225
+ try {
226
+ const { runSelfHeal } = require('./self-heal');
227
+ const uniqueGates = [...new Set(recentDenials.map(d => d.gateId).filter(Boolean))];
228
+ healResult = runSelfHeal({
229
+ reason: `audit-trail: ${recentDenials.length} denials in ${windowMs / 1000}s (gates: ${uniqueGates.join(', ')})`,
230
+ });
231
+ } catch {
232
+ healResult = { error: 'self-heal module unavailable' };
233
+ }
234
+
235
+ return {
236
+ triggered: true,
237
+ recentDenials: recentDenials.length,
238
+ threshold: denyThreshold,
239
+ gates: [...new Set(recentDenials.map(d => d.gateId).filter(Boolean))],
240
+ healResult,
241
+ };
242
+ }
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // Semantic cache threshold auto-tuning
246
+ // ---------------------------------------------------------------------------
247
+
248
+ const CACHE_TUNE_STATE_FILENAME = 'cache-tune-state.json';
249
+
250
+ /**
251
+ * Auto-tunes THUMBGATE_SEMANTIC_CACHE_THRESHOLD based on audit trail feedback.
252
+ * If deny rate is high → tighten cache (raise threshold, fewer false hits).
253
+ * If deny rate is low → loosen cache (lower threshold, more cache hits).
254
+ *
255
+ * @param {string} [logPath]
256
+ * @returns {{ currentThreshold: number, recommendedThreshold: number, denyRate: number, applied: boolean }}
257
+ */
258
+ function tuneCacheThreshold(logPath) {
259
+ const stats = auditStats(logPath);
260
+ const total = stats.total || 1;
261
+ const denyRate = stats.deny / total;
262
+
263
+ const currentThreshold = parseFloat(process.env.THUMBGATE_SEMANTIC_CACHE_THRESHOLD || '0.7');
264
+ const MIN_THRESHOLD = 0.5;
265
+ const MAX_THRESHOLD = 0.95;
266
+ const STEP = 0.02;
267
+
268
+ // High deny rate (>20%) → agent is hitting gates often → tighten cache to reduce hallucinated recalls
269
+ // Low deny rate (<5%) → agent is compliant → loosen cache for more hits and cost savings
270
+ let recommended = currentThreshold;
271
+ if (denyRate > 0.20) {
272
+ recommended = Math.min(currentThreshold + STEP, MAX_THRESHOLD);
273
+ } else if (denyRate < 0.05 && total > 10) {
274
+ recommended = Math.max(currentThreshold - STEP, MIN_THRESHOLD);
275
+ }
276
+ recommended = Math.round(recommended * 100) / 100;
277
+
278
+ // Persist tuning state
279
+ const statePath = path.join(path.dirname(getAuditLogPath()), CACHE_TUNE_STATE_FILENAME);
280
+ const tuneRecord = {
281
+ timestamp: new Date().toISOString(),
282
+ currentThreshold,
283
+ recommendedThreshold: recommended,
284
+ denyRate: Math.round(denyRate * 10000) / 100,
285
+ totalEvents: stats.total,
286
+ };
287
+
288
+ try {
289
+ ensureDir(path.dirname(statePath));
290
+ fs.writeFileSync(statePath, JSON.stringify(tuneRecord, null, 2) + '\n');
291
+ } catch { /* non-critical */ }
292
+
293
+ return {
294
+ currentThreshold,
295
+ recommendedThreshold: recommended,
296
+ denyRate: Math.round(denyRate * 10000) / 100,
297
+ applied: recommended !== currentThreshold,
298
+ };
299
+ }
300
+
301
+ // ---------------------------------------------------------------------------
302
+ // Exports
303
+ // ---------------------------------------------------------------------------
304
+
305
+ module.exports = {
306
+ recordAuditEvent,
307
+ auditToFeedback,
308
+ readAuditLog,
309
+ auditStats,
310
+ latencyStats,
311
+ skillAdherence,
312
+ evaluateSelfHealTrigger,
313
+ tuneCacheThreshold,
314
+ getAuditLogPath,
315
+ sanitizeToolInput,
316
+ AUDIT_LOG_FILENAME,
317
+ CACHE_TUNE_STATE_FILENAME,
318
+ };
319
+
320
+ // ---------------------------------------------------------------------------
321
+ // CLI
322
+ // ---------------------------------------------------------------------------
323
+
324
+ /**
325
+ * Compute latency statistics from audit trail entries that have latencyMs.
326
+ * @param {string} [logPath]
327
+ * @returns {{ count: number, avgMs: number, p50Ms: number, p95Ms: number, p99Ms: number, maxMs: number, slowest: Array }}
328
+ */
329
+ function latencyStats(logPath) {
330
+ const entries = readAuditLog(logPath);
331
+ const withLatency = entries.filter(e => typeof e.latencyMs === 'number');
332
+ if (withLatency.length === 0) return { count: 0, avgMs: 0, p50Ms: 0, p95Ms: 0, p99Ms: 0, maxMs: 0, slowest: [] };
333
+
334
+ const sorted = withLatency.map(e => e.latencyMs).sort((a, b) => a - b);
335
+ const sum = sorted.reduce((a, b) => a + b, 0);
336
+ const percentile = (arr, p) => arr[Math.min(Math.ceil(arr.length * p) - 1, arr.length - 1)];
337
+
338
+ // Per-tool breakdown
339
+ const byTool = {};
340
+ for (const e of withLatency) {
341
+ const tool = e.toolName || 'unknown';
342
+ if (!byTool[tool]) byTool[tool] = [];
343
+ byTool[tool].push(e.latencyMs);
344
+ }
345
+ const toolStats = {};
346
+ for (const [tool, latencies] of Object.entries(byTool)) {
347
+ const s = latencies.sort((a, b) => a - b);
348
+ toolStats[tool] = {
349
+ count: s.length,
350
+ avgMs: Math.round(s.reduce((a, b) => a + b, 0) / s.length),
351
+ p95Ms: percentile(s, 0.95),
352
+ maxMs: s[s.length - 1],
353
+ };
354
+ }
355
+
356
+ // Top 5 slowest calls
357
+ const slowest = withLatency
358
+ .sort((a, b) => b.latencyMs - a.latencyMs)
359
+ .slice(0, 5)
360
+ .map(e => ({ tool: e.toolName, latencyMs: e.latencyMs, timestamp: e.timestamp }));
361
+
362
+ return {
363
+ count: sorted.length,
364
+ avgMs: Math.round(sum / sorted.length),
365
+ p50Ms: percentile(sorted, 0.50),
366
+ p95Ms: percentile(sorted, 0.95),
367
+ p99Ms: percentile(sorted, 0.99),
368
+ maxMs: sorted[sorted.length - 1],
369
+ byTool: toolStats,
370
+ slowest,
371
+ };
372
+ }
373
+
374
+ if (require.main === module) {
375
+ const args = process.argv.slice(2);
376
+ if (args.includes('--stats')) {
377
+ console.log(JSON.stringify(auditStats(), null, 2));
378
+ } else if (args.includes('--adherence')) {
379
+ console.log(JSON.stringify(skillAdherence(), null, 2));
380
+ } else if (args.includes('--self-heal')) {
381
+ console.log(JSON.stringify(evaluateSelfHealTrigger(), null, 2));
382
+ } else if (args.includes('--tune-cache')) {
383
+ console.log(JSON.stringify(tuneCacheThreshold(), null, 2));
384
+ } else if (args.includes('--latency')) {
385
+ console.log(JSON.stringify(latencyStats(), null, 2));
386
+ } else {
387
+ const entries = readAuditLog();
388
+ const adherence = skillAdherence();
389
+ console.log(`Audit trail: ${entries.length} entries`);
390
+ const stats = auditStats();
391
+ console.log(` allow: ${stats.allow} warn: ${stats.warn} deny: ${stats.deny}`);
392
+ console.log(` skill adherence: ${adherence.overall}% across ${adherence.totalTools} tools`);
393
+ const lat = latencyStats();
394
+ if (lat.count > 0) {
395
+ console.log(` latency: avg=${lat.avgMs}ms p95=${lat.p95Ms}ms max=${lat.maxMs}ms (${lat.count} samples)`);
396
+ }
397
+ }
398
+ }
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const MAX_AUTO_GATES = 10;
8
+ const WARN_THRESHOLD = 3; // 3+ repeated failures surface a warning gate
9
+ const BLOCK_THRESHOLD = 5; // 5+ repeated failures hard-block the action
10
+ const WINDOW_DAYS = 30;
11
+
12
+ const NEG_SIGNALS = new Set(['negative', 'negative_strong', 'down', 'thumbs_down']);
13
+
14
+ function getFeedbackLogPath() {
15
+ if (process.env.THUMBGATE_FEEDBACK_DIR) {
16
+ return path.join(process.env.THUMBGATE_FEEDBACK_DIR, 'feedback-log.jsonl');
17
+ }
18
+ const localFallback = path.join(process.cwd(), '.thumbgate', 'feedback-log.jsonl');
19
+ const localClaude = path.join(process.cwd(), '.claude', 'memory', 'feedback', 'feedback-log.jsonl');
20
+ if (fs.existsSync(localFallback)) return localFallback;
21
+ if (fs.existsSync(localClaude)) return localClaude;
22
+ return localFallback; // default even if doesn't exist
23
+ }
24
+
25
+ function getAutoGatesPath() {
26
+ return path.join(path.dirname(getFeedbackLogPath()), 'auto-promoted-gates.json');
27
+ }
28
+
29
+ function readJSONL(filePath) {
30
+ if (!fs.existsSync(filePath)) return [];
31
+ const raw = fs.readFileSync(filePath, 'utf-8').trim();
32
+ if (!raw) return [];
33
+ return raw.split('\n').map((line) => {
34
+ try { return JSON.parse(line); } catch { return null; }
35
+ }).filter(Boolean);
36
+ }
37
+
38
+ function loadAutoGates() {
39
+ const autoGatesPath = getAutoGatesPath();
40
+ if (!fs.existsSync(autoGatesPath)) {
41
+ return { version: 1, gates: [], promotionLog: [] };
42
+ }
43
+ try {
44
+ return JSON.parse(fs.readFileSync(autoGatesPath, 'utf-8'));
45
+ } catch {
46
+ return { version: 1, gates: [], promotionLog: [] };
47
+ }
48
+ }
49
+
50
+ function saveAutoGates(data) {
51
+ const autoGatesPath = getAutoGatesPath();
52
+ const dir = path.dirname(autoGatesPath);
53
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
54
+ fs.writeFileSync(autoGatesPath, JSON.stringify(data, null, 2) + '\n');
55
+ }
56
+
57
+ function isNegative(entry) {
58
+ const sig = (entry.signal || entry.feedback || '').toLowerCase();
59
+ return NEG_SIGNALS.has(sig);
60
+ }
61
+
62
+ function extractPatternKey(entry) {
63
+ // Use tags as primary grouping key; fall back to context normalization
64
+ const tags = (entry.tags || []).filter((t) => !['feedback', 'negative', 'positive'].includes(t));
65
+ if (tags.length > 0) return tags.sort().join('+');
66
+
67
+ const ctx = (entry.context || entry.whatWentWrong || '').toLowerCase().trim();
68
+ if (ctx.length < 10) return null;
69
+ // Normalize paths and numbers for grouping
70
+ return ctx.replace(/\/Users\/[^\s/]+/g, '~').replace(/:[0-9]+/g, '').replace(/\s+/g, ' ').slice(0, 100);
71
+ }
72
+
73
+ function extractDiagnosticKeys(entry) {
74
+ const keys = [];
75
+ const diagnosis = entry && entry.diagnosis ? entry.diagnosis : null;
76
+ if (!diagnosis) return keys;
77
+
78
+ if (diagnosis.rootCauseCategory) {
79
+ keys.push(`diagnosis:${diagnosis.rootCauseCategory}`);
80
+ }
81
+
82
+ const violations = Array.isArray(diagnosis.violations) ? diagnosis.violations : [];
83
+ violations.slice(0, 3).forEach((violation) => {
84
+ const key = violation.constraintId || violation.message;
85
+ if (key) {
86
+ keys.push(`constraint:${key}`);
87
+ }
88
+ });
89
+
90
+ return keys;
91
+ }
92
+
93
+ function groupNegativeFeedback(entries, windowDays) {
94
+ const cutoff = Date.now() - windowDays * 24 * 60 * 60 * 1000;
95
+ const groups = {};
96
+
97
+ for (const entry of entries) {
98
+ if (!isNegative(entry)) continue;
99
+
100
+ const ts = entry.timestamp ? new Date(entry.timestamp).getTime() : 0;
101
+ if (ts < cutoff) continue;
102
+
103
+ const keys = [extractPatternKey(entry), ...extractDiagnosticKeys(entry)]
104
+ .filter(Boolean)
105
+ .filter((value, index, values) => values.indexOf(value) === index);
106
+ if (keys.length === 0) continue;
107
+
108
+ for (const key of keys) {
109
+ if (!groups[key]) {
110
+ groups[key] = {
111
+ key,
112
+ count: 0,
113
+ entries: [],
114
+ latestContext: '',
115
+ latestTimestamp: '',
116
+ };
117
+ }
118
+ groups[key].count++;
119
+ groups[key].entries.push(entry);
120
+ if (!groups[key].latestTimestamp || (entry.timestamp && entry.timestamp > groups[key].latestTimestamp)) {
121
+ groups[key].latestTimestamp = entry.timestamp || '';
122
+ groups[key].latestContext = entry.context || entry.whatWentWrong || '';
123
+ }
124
+ }
125
+ }
126
+
127
+ return groups;
128
+ }
129
+
130
+ function patternToGateId(key) {
131
+ return 'auto-' + key.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').slice(0, 50).toLowerCase();
132
+ }
133
+
134
+ function buildGateRule(group) {
135
+ const action = group.count === 'MANUAL' ? group.manualAction || 'block' : (group.count >= BLOCK_THRESHOLD ? 'block' : 'warn');
136
+ const severity = action === 'block' ? 'critical' : 'medium';
137
+ const context = group.latestContext.slice(0, 120);
138
+ const kind = group.key.startsWith('diagnosis:')
139
+ ? 'repeated diagnosis'
140
+ : group.key.startsWith('constraint:')
141
+ ? 'repeated constraint violation'
142
+ : 'repeated pattern';
143
+
144
+ const occurrencesText = group.count === 'MANUAL' ? 'manual' : `${group.count} occurrences`;
145
+ const suggestedMessage = `Auto-promoted ${kind}: "${context}" (${occurrencesText} in ${WINDOW_DAYS} days)`;
146
+
147
+ return {
148
+ id: patternToGateId(group.key),
149
+ trigger: `auto:${group.key}`,
150
+ pattern: group.key.replace(/^diagnosis:|constraint:/, ''),
151
+ action,
152
+ message: suggestedMessage,
153
+ severity,
154
+ occurrences: group.count,
155
+ promotedAt: new Date().toISOString(),
156
+ source: group.source || 'auto-promote',
157
+ };
158
+ }
159
+
160
+ function forcePromote(context, action = 'block') {
161
+ if (!context) throw new Error('context is required for force-promote');
162
+ const data = loadAutoGates();
163
+ const gateId = patternToGateId(context);
164
+
165
+ // Remove existing if any
166
+ data.gates = data.gates.filter(g => g.id !== gateId);
167
+
168
+ const gate = buildGateRule({
169
+ key: context,
170
+ latestContext: context,
171
+ count: 'MANUAL',
172
+ manualAction: action,
173
+ source: 'force-promote'
174
+ });
175
+ data.gates.unshift(gate);
176
+
177
+ if (data.gates.length > MAX_AUTO_GATES) {
178
+ data.gates = data.gates.slice(0, MAX_AUTO_GATES);
179
+ }
180
+
181
+ data.promotionLog = data.promotionLog || [];
182
+ data.promotionLog.push({
183
+ gateId,
184
+ context,
185
+ action,
186
+ promotedAt: new Date().toISOString(),
187
+ source: 'force-promote'
188
+ });
189
+
190
+ saveAutoGates(data);
191
+ return { gateId, action, totalGates: data.gates.length };
192
+ }
193
+
194
+ function promote(feedbackLogPath) {
195
+ const logPath = feedbackLogPath || getFeedbackLogPath();
196
+ const entries = readJSONL(logPath);
197
+ const groups = groupNegativeFeedback(entries, WINDOW_DAYS);
198
+ const data = loadAutoGates();
199
+ const existingIds = new Set(data.gates.map((g) => g.id));
200
+ const promotions = [];
201
+
202
+ for (const group of Object.values(groups)) {
203
+ if (group.count < WARN_THRESHOLD) continue;
204
+
205
+ const gateId = patternToGateId(group.key);
206
+
207
+ // Check for existing gate — possibly upgrade
208
+ const existingIdx = data.gates.findIndex((g) => g.id === gateId);
209
+ if (existingIdx !== -1) {
210
+ const existing = data.gates[existingIdx];
211
+ const newAction = group.count >= BLOCK_THRESHOLD ? 'block' : 'warn';
212
+ if (existing.action !== newAction && newAction === 'block') {
213
+ // Upgrade from warn to block
214
+ data.gates[existingIdx] = { ...existing, action: 'block', severity: 'critical', occurrences: group.count, upgradedAt: new Date().toISOString() };
215
+ promotions.push({ type: 'upgrade', gateId, from: existing.action, to: 'block', occurrences: group.count });
216
+ }
217
+ // Update occurrence count even if no action change
218
+ data.gates[existingIdx].occurrences = group.count;
219
+ continue;
220
+ }
221
+
222
+ // New gate
223
+ const gate = buildGateRule(group);
224
+
225
+ // Enforce max limit — rotate oldest
226
+ if (data.gates.length >= MAX_AUTO_GATES) {
227
+ const removed = data.gates.shift();
228
+ promotions.push({ type: 'rotated', removedGateId: removed.id });
229
+ }
230
+
231
+ data.gates.push(gate);
232
+ promotions.push({ type: 'new', gateId: gate.id, action: gate.action, occurrences: group.count });
233
+ }
234
+
235
+ // Log promotions
236
+ for (const p of promotions) {
237
+ data.promotionLog = data.promotionLog || [];
238
+ data.promotionLog.push({ ...p, timestamp: new Date().toISOString() });
239
+ }
240
+
241
+ saveAutoGates(data);
242
+
243
+ return { promotions, totalGates: data.gates.length, data };
244
+ }
245
+
246
+ function runCli(argv = process.argv.slice(2)) {
247
+ const forceContext = argv.find((arg) => arg.startsWith('--force-block='))?.split('=')[1];
248
+ if (forceContext) {
249
+ const result = forcePromote(forceContext, 'block');
250
+ console.log(`Forced block gate created: ${result.gateId}`);
251
+ console.log(`Total auto-promoted gates: ${result.totalGates}`);
252
+ return 0;
253
+ }
254
+
255
+ const logPath = argv[0] && !argv[0].startsWith('--') ? argv[0] : undefined;
256
+ const result = promote(logPath);
257
+ if (result.promotions.length === 0) {
258
+ console.log('No new promotions.');
259
+ } else {
260
+ for (const promotion of result.promotions) {
261
+ if (promotion.type === 'new') {
262
+ console.log(`NEW gate: ${promotion.gateId} (${promotion.action}, ${promotion.occurrences} occurrences)`);
263
+ } else if (promotion.type === 'upgrade') {
264
+ console.log(`UPGRADE: ${promotion.gateId} ${promotion.from} -> ${promotion.to} (${promotion.occurrences} occurrences)`);
265
+ } else if (promotion.type === 'rotated') {
266
+ console.log(`ROTATED out: ${promotion.removedGateId}`);
267
+ }
268
+ }
269
+ }
270
+ console.log(`Total auto-promoted gates: ${result.totalGates}`);
271
+ return 0;
272
+ }
273
+
274
+ if (require.main === module) {
275
+ try {
276
+ process.exitCode = runCli();
277
+ } catch (err) {
278
+ console.error('auto-promote-gates error:', err.message);
279
+ process.exit(1);
280
+ }
281
+ }
282
+
283
+ module.exports = {
284
+ promote,
285
+ forcePromote,
286
+ runCli,
287
+ loadAutoGates,
288
+ saveAutoGates,
289
+ getAutoGatesPath,
290
+ groupNegativeFeedback,
291
+ patternToGateId,
292
+ buildGateRule,
293
+ extractPatternKey,
294
+ isNegative,
295
+ MAX_AUTO_GATES,
296
+ WARN_THRESHOLD,
297
+ BLOCK_THRESHOLD,
298
+ WINDOW_DAYS,
299
+ };