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,613 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Lesson DB — SQLite + FTS5 backing store for feedback & memories.
6
+ *
7
+ * Dual-written alongside JSONL (source of truth). Provides:
8
+ * - Full-text search via FTS5 (replaces Jaccard token-overlap)
9
+ * - Indexed tag/domain/signal queries
10
+ * - Corrective-action inference from past similar failures
11
+ *
12
+ * @module lesson-db
13
+ */
14
+
15
+ const path = require('node:path');
16
+ const fs = require('node:fs');
17
+
18
+ const PROJECT_ROOT = path.join(__dirname, '..');
19
+ const DEFAULT_DB_PATH = path.join(PROJECT_ROOT, '.claude', 'memory', 'lessons.sqlite');
20
+
21
+ /** @returns {import('better-sqlite3').Database} */
22
+ function initDB(dbPath) {
23
+ const Database = require('better-sqlite3');
24
+ const resolvedPath = dbPath || process.env.LESSON_DB_PATH || DEFAULT_DB_PATH;
25
+
26
+ // Ensure parent directory exists
27
+ const dir = path.dirname(resolvedPath);
28
+ if (!fs.existsSync(dir)) {
29
+ fs.mkdirSync(dir, { recursive: true });
30
+ }
31
+
32
+ const db = new Database(resolvedPath);
33
+ db.pragma('busy_timeout = 3000');
34
+ db.pragma('journal_mode = WAL');
35
+ db.pragma('foreign_keys = ON');
36
+
37
+ db.exec(`
38
+ CREATE TABLE IF NOT EXISTS lessons (
39
+ id TEXT PRIMARY KEY,
40
+ signal TEXT NOT NULL CHECK(signal IN ('positive','negative')),
41
+ context TEXT,
42
+ whatWentWrong TEXT,
43
+ whatToChange TEXT,
44
+ whatWorked TEXT,
45
+ domain TEXT,
46
+ tags TEXT,
47
+ rootCause TEXT,
48
+ importance TEXT DEFAULT 'medium',
49
+ skill TEXT,
50
+ timestamp TEXT NOT NULL,
51
+ sourceFeedbackId TEXT,
52
+ pruned INTEGER DEFAULT 0
53
+ );
54
+
55
+ CREATE INDEX IF NOT EXISTS idx_lessons_signal ON lessons(signal);
56
+ CREATE INDEX IF NOT EXISTS idx_lessons_domain ON lessons(domain);
57
+ CREATE INDEX IF NOT EXISTS idx_lessons_importance ON lessons(importance);
58
+ CREATE INDEX IF NOT EXISTS idx_lessons_timestamp ON lessons(timestamp);
59
+ CREATE INDEX IF NOT EXISTS idx_lessons_skill ON lessons(skill);
60
+ `);
61
+
62
+ // FTS5 virtual table for full-text search
63
+ db.exec(`
64
+ CREATE VIRTUAL TABLE IF NOT EXISTS lessons_fts USING fts5(
65
+ context, whatWentWrong, whatToChange, whatWorked, rootCause,
66
+ content=lessons,
67
+ content_rowid=rowid
68
+ );
69
+ `);
70
+
71
+ // Triggers to keep FTS in sync with lessons table
72
+ db.exec(`
73
+ CREATE TRIGGER IF NOT EXISTS lessons_ai AFTER INSERT ON lessons BEGIN
74
+ INSERT INTO lessons_fts(rowid, context, whatWentWrong, whatToChange, whatWorked, rootCause)
75
+ VALUES (new.rowid, new.context, new.whatWentWrong, new.whatToChange, new.whatWorked, new.rootCause);
76
+ END;
77
+
78
+ CREATE TRIGGER IF NOT EXISTS lessons_ad AFTER DELETE ON lessons BEGIN
79
+ INSERT INTO lessons_fts(lessons_fts, rowid, context, whatWentWrong, whatToChange, whatWorked, rootCause)
80
+ VALUES ('delete', old.rowid, old.context, old.whatWentWrong, old.whatToChange, old.whatWorked, old.rootCause);
81
+ END;
82
+
83
+ CREATE TRIGGER IF NOT EXISTS lessons_au AFTER UPDATE ON lessons BEGIN
84
+ INSERT INTO lessons_fts(lessons_fts, rowid, context, whatWentWrong, whatToChange, whatWorked, rootCause)
85
+ VALUES ('delete', old.rowid, old.context, old.whatWentWrong, old.whatToChange, old.whatWorked, old.rootCause);
86
+ INSERT INTO lessons_fts(rowid, context, whatWentWrong, whatToChange, whatWorked, rootCause)
87
+ VALUES (new.rowid, new.context, new.whatWentWrong, new.whatToChange, new.whatWorked, new.rootCause);
88
+ END;
89
+ `);
90
+
91
+ // Session search table — stores session notes for cross-session recall
92
+ db.exec(`
93
+ CREATE TABLE IF NOT EXISTS sessions (
94
+ id TEXT PRIMARY KEY,
95
+ project TEXT,
96
+ branch TEXT,
97
+ summary TEXT,
98
+ content TEXT,
99
+ created_at TEXT NOT NULL
100
+ );
101
+
102
+ CREATE VIRTUAL TABLE IF NOT EXISTS sessions_fts USING fts5(
103
+ summary, content,
104
+ content=sessions,
105
+ content_rowid=rowid
106
+ );
107
+
108
+ CREATE TRIGGER IF NOT EXISTS sessions_ai AFTER INSERT ON sessions BEGIN
109
+ INSERT INTO sessions_fts(rowid, summary, content)
110
+ VALUES (new.rowid, new.summary, new.content);
111
+ END;
112
+
113
+ CREATE TRIGGER IF NOT EXISTS sessions_ad AFTER DELETE ON sessions BEGIN
114
+ INSERT INTO sessions_fts(sessions_fts, rowid, summary, content)
115
+ VALUES ('delete', old.rowid, old.summary, old.content);
116
+ END;
117
+ `);
118
+
119
+ return db;
120
+ }
121
+
122
+ /**
123
+ * Upsert a session note into SQLite for cross-session search.
124
+ */
125
+ function upsertSession(db, session) {
126
+ db.prepare(`
127
+ INSERT OR REPLACE INTO sessions (id, project, branch, summary, content, created_at)
128
+ VALUES (?, ?, ?, ?, ?, ?)
129
+ `).run(
130
+ session.id,
131
+ session.project || null,
132
+ session.branch || null,
133
+ session.summary || null,
134
+ session.content || '',
135
+ session.created_at || new Date().toISOString(),
136
+ );
137
+ return session.id;
138
+ }
139
+
140
+ /**
141
+ * Full-text search across past sessions using FTS5.
142
+ */
143
+ function searchSessions(db, query, limit = 10) {
144
+ if (!query || !query.trim()) {
145
+ return db.prepare('SELECT * FROM sessions ORDER BY created_at DESC LIMIT ?').all(limit);
146
+ }
147
+ const ftsQuery = sanitizeFtsQuery(query);
148
+ try {
149
+ return db.prepare(`
150
+ SELECT s.*, rank
151
+ FROM sessions_fts fts
152
+ JOIN sessions s ON s.rowid = fts.rowid
153
+ WHERE sessions_fts MATCH ?
154
+ ORDER BY rank
155
+ LIMIT ?
156
+ `).all(ftsQuery, limit);
157
+ } catch {
158
+ return db.prepare(
159
+ 'SELECT * FROM sessions WHERE content LIKE ? OR summary LIKE ? ORDER BY created_at DESC LIMIT ?',
160
+ ).all(`%${query}%`, `%${query}%`, limit);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Upsert a lesson (feedback event + optional memory record) into SQLite.
166
+ * Idempotent — uses INSERT OR REPLACE on the primary key.
167
+ *
168
+ * Dedup rules:
169
+ * 1. Skip pruned records (Bayesian entropy indicates contradictory signal)
170
+ * 2. Skip if an existing lesson has identical whatToChange + overlapping tags
171
+ * (bumps the existing record's importance instead)
172
+ * 3. Always store if whatToChange is empty (raw feedback, not actionable yet)
173
+ */
174
+ function upsertLesson(db, feedbackEvent, memoryRecord) {
175
+ // Rule 1: skip pruned records — they add noise, not signal
176
+ if (memoryRecord?.pruned) {
177
+ return null;
178
+ }
179
+
180
+ const id = memoryRecord?.id || feedbackEvent.id;
181
+ const signal = feedbackEvent.signal === 'positive' || feedbackEvent.signal === 'up' ? 'positive' : 'negative';
182
+ const tags = Array.isArray(feedbackEvent.tags) ? feedbackEvent.tags : [];
183
+ const tagsJson = JSON.stringify(tags);
184
+ const domain = feedbackEvent.richContext?.domain || 'general';
185
+ const rootCause = feedbackEvent.diagnosis?.rootCauseCategory || null;
186
+ const importance = memoryRecord?.importance || (signal === 'negative' ? 'high' : 'medium');
187
+ const skill = feedbackEvent.skill || null;
188
+ const whatToChange = feedbackEvent.whatToChange || null;
189
+
190
+ // Rule 2: dedup — if an existing lesson has the same whatToChange and shares tags, skip
191
+ if (whatToChange && whatToChange.trim()) {
192
+ const duplicate = findDuplicate(db, whatToChange, tags);
193
+ if (duplicate) {
194
+ // Bump importance if the new one is higher priority
195
+ const PRIORITY = { critical: 4, high: 3, medium: 2, low: 1 };
196
+ if ((PRIORITY[importance] || 0) > (PRIORITY[duplicate.importance] || 0)) {
197
+ db.prepare('UPDATE lessons SET importance = ? WHERE id = ?').run(importance, duplicate.id);
198
+ }
199
+ return null; // deduplicated
200
+ }
201
+ }
202
+
203
+ const stmt = db.prepare(`
204
+ INSERT OR REPLACE INTO lessons
205
+ (id, signal, context, whatWentWrong, whatToChange, whatWorked, domain, tags, rootCause, importance, skill, timestamp, sourceFeedbackId, pruned)
206
+ VALUES
207
+ (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
208
+ `);
209
+
210
+ stmt.run(
211
+ id,
212
+ signal,
213
+ feedbackEvent.context || null,
214
+ feedbackEvent.whatWentWrong || memoryRecord?.content || null,
215
+ whatToChange,
216
+ feedbackEvent.whatWorked || null,
217
+ domain,
218
+ tagsJson,
219
+ rootCause,
220
+ importance,
221
+ skill,
222
+ feedbackEvent.timestamp || new Date().toISOString(),
223
+ feedbackEvent.id,
224
+ 0, // not pruned (we skip pruned above)
225
+ );
226
+
227
+ return id;
228
+ }
229
+
230
+ /**
231
+ * Find an existing lesson with identical whatToChange and overlapping tags.
232
+ * Returns the existing row or null.
233
+ */
234
+ function findDuplicate(db, whatToChange, tags) {
235
+ if (!whatToChange || !whatToChange.trim()) return null;
236
+
237
+ // Exact match on whatToChange text (normalized)
238
+ const normalized = whatToChange.trim().toLowerCase();
239
+ const candidates = db.prepare(
240
+ `SELECT id, importance, tags FROM lessons WHERE LOWER(TRIM(whatToChange)) = ?`,
241
+ ).all(normalized);
242
+
243
+ if (candidates.length === 0) return null;
244
+
245
+ // If any candidate shares at least one tag, it's a duplicate
246
+ for (const c of candidates) {
247
+ if (tags.length === 0) return c; // no tags to compare = text match is enough
248
+ const cTags = safeParseTags(c.tags);
249
+ if (tags.some((t) => cTags.includes(t))) return c;
250
+ }
251
+
252
+ return null;
253
+ }
254
+
255
+ /**
256
+ * Compact the lesson DB — merge near-duplicate lessons and remove stale entries.
257
+ *
258
+ * Strategy:
259
+ * - Group lessons by normalized whatToChange text
260
+ * - Keep only the most recent + highest importance per group
261
+ * - Delete the rest
262
+ *
263
+ * @returns {{ removed: number, kept: number }}
264
+ */
265
+ function compactLessons(db) {
266
+ const all = db.prepare('SELECT id, whatToChange, importance, timestamp, tags FROM lessons ORDER BY timestamp DESC').all();
267
+ const seen = new Map(); // normalized whatToChange → best record
268
+ const toDelete = [];
269
+ const PRIORITY = { critical: 4, high: 3, medium: 2, low: 1 };
270
+
271
+ for (const row of all) {
272
+ if (!row.whatToChange || !row.whatToChange.trim()) continue;
273
+ const key = row.whatToChange.trim().toLowerCase();
274
+
275
+ if (!seen.has(key)) {
276
+ seen.set(key, row);
277
+ } else {
278
+ const existing = seen.get(key);
279
+ const existingPri = PRIORITY[existing.importance] || 0;
280
+ const newPri = PRIORITY[row.importance] || 0;
281
+ if (newPri > existingPri) {
282
+ toDelete.push(existing.id);
283
+ seen.set(key, row);
284
+ } else {
285
+ toDelete.push(row.id);
286
+ }
287
+ }
288
+ }
289
+
290
+ if (toDelete.length > 0) {
291
+ const deleteStmt = db.prepare('DELETE FROM lessons WHERE id = ?');
292
+ const deleteAll = db.transaction(() => {
293
+ for (const id of toDelete) {
294
+ deleteStmt.run(id);
295
+ }
296
+ });
297
+ deleteAll();
298
+ }
299
+
300
+ return { removed: toDelete.length, kept: all.length - toDelete.length };
301
+ }
302
+
303
+ /**
304
+ * Full-text search across lessons using FTS5.
305
+ *
306
+ * @param {import('better-sqlite3').Database} db
307
+ * @param {string} query - Search text (FTS5 query syntax supported)
308
+ * @param {object} [options]
309
+ * @param {number} [options.limit=10]
310
+ * @param {string} [options.signal] - Filter by 'positive' or 'negative'
311
+ * @param {string[]} [options.tags] - Require ALL tags present
312
+ * @param {string} [options.domain] - Filter by domain
313
+ * @returns {Array<object>}
314
+ */
315
+ function searchLessons(db, query, options = {}) {
316
+ const limit = Math.min(options.limit || 10, 50);
317
+
318
+ if (!query || !query.trim()) {
319
+ // No query — return most recent lessons with optional filters
320
+ let sql = 'SELECT * FROM lessons WHERE 1=1';
321
+ const params = [];
322
+
323
+ if (options.signal) {
324
+ sql += ' AND signal = ?';
325
+ params.push(options.signal);
326
+ }
327
+ if (options.domain) {
328
+ sql += ' AND domain = ?';
329
+ params.push(options.domain);
330
+ }
331
+
332
+ sql += ' ORDER BY timestamp DESC LIMIT ?';
333
+ params.push(limit);
334
+
335
+ const rows = db.prepare(sql).all(...params);
336
+ return rows.map(parseRow);
337
+ }
338
+
339
+ // FTS5 search with ranking
340
+ const safeQuery = sanitizeFtsQuery(query);
341
+ let sql = `
342
+ SELECT l.*, rank
343
+ FROM lessons_fts fts
344
+ JOIN lessons l ON l.rowid = fts.rowid
345
+ WHERE lessons_fts MATCH ?
346
+ `;
347
+ const params = [safeQuery];
348
+
349
+ if (options.signal) {
350
+ sql += ' AND l.signal = ?';
351
+ params.push(options.signal);
352
+ }
353
+ if (options.domain) {
354
+ sql += ' AND l.domain = ?';
355
+ params.push(options.domain);
356
+ }
357
+
358
+ sql += ' ORDER BY rank LIMIT ?';
359
+ params.push(limit);
360
+
361
+ let rows;
362
+ try {
363
+ rows = db.prepare(sql).all(...params);
364
+ } catch (_err) {
365
+ // If FTS query syntax is invalid, fall back to LIKE search
366
+ rows = fallbackLikeSearch(db, query, options, limit);
367
+ }
368
+
369
+ const parsed = rows.map(parseRow);
370
+
371
+ // Post-filter by tags (JSON array stored as text)
372
+ if (options.tags && options.tags.length > 0) {
373
+ return parsed.filter((row) => {
374
+ const rowTags = row.tags || [];
375
+ return options.tags.every((t) => rowTags.includes(t));
376
+ });
377
+ }
378
+
379
+ return parsed;
380
+ }
381
+
382
+ /**
383
+ * Find corrective actions for a negative feedback event by matching
384
+ * similar past failures (by tags and domain).
385
+ *
386
+ * @param {import('better-sqlite3').Database} db
387
+ * @param {object} feedbackEvent
388
+ * @param {number} [limit=3]
389
+ * @returns {string[]} Top corrective actions
390
+ */
391
+ function inferCorrectiveActions(db, feedbackEvent, limit = 3) {
392
+ const tags = Array.isArray(feedbackEvent.tags) ? feedbackEvent.tags : [];
393
+ const domain = feedbackEvent.richContext?.domain || 'general';
394
+
395
+ // Strategy: find past negative lessons with overlapping tags, ranked by recency
396
+ let candidates = [];
397
+
398
+ // 1. Tag overlap search — find lessons sharing tags with this failure
399
+ if (tags.length > 0) {
400
+ const tagPlaceholders = tags.map(() => `l.tags LIKE ?`).join(' OR ');
401
+ const tagParams = tags.map((t) => `%"${t}"%`);
402
+
403
+ const sql = `
404
+ SELECT whatToChange, tags, timestamp
405
+ FROM lessons l
406
+ WHERE l.signal = 'negative'
407
+ AND l.whatToChange IS NOT NULL
408
+ AND l.whatToChange != ''
409
+ AND (${tagPlaceholders})
410
+ ORDER BY l.timestamp DESC
411
+ LIMIT 20
412
+ `;
413
+
414
+ candidates = db.prepare(sql).all(...tagParams);
415
+ }
416
+
417
+ // 2. Domain fallback if no tag matches
418
+ if (candidates.length === 0) {
419
+ const sql = `
420
+ SELECT whatToChange, tags, timestamp
421
+ FROM lessons l
422
+ WHERE l.signal = 'negative'
423
+ AND l.whatToChange IS NOT NULL
424
+ AND l.whatToChange != ''
425
+ AND l.domain = ?
426
+ ORDER BY l.timestamp DESC
427
+ LIMIT 10
428
+ `;
429
+
430
+ candidates = db.prepare(sql).all(domain);
431
+ }
432
+
433
+ if (candidates.length === 0) return [];
434
+
435
+ // Deduplicate and rank by tag overlap count
436
+ const actionMap = new Map();
437
+ for (const c of candidates) {
438
+ const action = c.whatToChange.trim();
439
+ if (actionMap.has(action)) {
440
+ actionMap.get(action).count += 1;
441
+ } else {
442
+ const cTags = safeParseTags(c.tags);
443
+ const overlap = tags.filter((t) => cTags.includes(t)).length;
444
+ actionMap.set(action, { action, count: 1, overlap, timestamp: c.timestamp });
445
+ }
446
+ }
447
+
448
+ return Array.from(actionMap.values())
449
+ .sort((a, b) => b.overlap - a.overlap || b.count - a.count)
450
+ .slice(0, limit)
451
+ .map((a) => a.action);
452
+ }
453
+
454
+ /**
455
+ * Get lesson count and signal breakdown.
456
+ */
457
+ function getStats(db) {
458
+ const total = db.prepare('SELECT COUNT(*) as count FROM lessons').get();
459
+ const bySignal = db.prepare('SELECT signal, COUNT(*) as count FROM lessons GROUP BY signal').all();
460
+ const byDomain = db.prepare('SELECT domain, COUNT(*) as count FROM lessons GROUP BY domain ORDER BY count DESC LIMIT 10').all();
461
+ return { total: total.count, bySignal, byDomain };
462
+ }
463
+
464
+ /**
465
+ * Fast stats computation from SQLite — replaces slow JSONL scan.
466
+ * Used by feedback_stats MCP tool to prevent 300s timeouts.
467
+ */
468
+ function getStatsFromDB(db) {
469
+ const total = db.prepare('SELECT COUNT(*) as count FROM lessons').get().count;
470
+ const positive = db.prepare("SELECT COUNT(*) as count FROM lessons WHERE signal = 'positive'").get().count;
471
+ const negative = db.prepare("SELECT COUNT(*) as count FROM lessons WHERE signal = 'negative'").get().count;
472
+ const byDomain = db.prepare('SELECT domain, COUNT(*) as count FROM lessons GROUP BY domain ORDER BY count DESC').all();
473
+ const byImportance = db.prepare('SELECT importance, COUNT(*) as count FROM lessons GROUP BY importance ORDER BY count DESC').all();
474
+ const recentLessons = db.prepare('SELECT id, signal, context, domain, timestamp FROM lessons ORDER BY timestamp DESC LIMIT 10').all();
475
+ const sessionCount = db.prepare('SELECT COUNT(*) as count FROM sessions').get().count;
476
+
477
+ return {
478
+ source: 'sqlite',
479
+ total,
480
+ positive,
481
+ negative,
482
+ positiveRate: total > 0 ? Math.round((positive / total) * 100) : 0,
483
+ byDomain,
484
+ byImportance,
485
+ recentLessons,
486
+ sessionCount,
487
+ };
488
+ }
489
+
490
+ /**
491
+ * Backfill SQLite from existing JSONL files.
492
+ * Reads feedback-log.jsonl and memory-log.jsonl, upserts all records.
493
+ */
494
+ function backfillFromJsonl(db, feedbackDir) {
495
+ const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
496
+ const memoryLogPath = path.join(feedbackDir, 'memory-log.jsonl');
497
+
498
+ const feedbackEntries = readJsonlSafe(feedbackLogPath);
499
+ const memoryEntries = readJsonlSafe(memoryLogPath);
500
+
501
+ // Index memories by sourceFeedbackId for joining
502
+ const memoryByFeedbackId = new Map();
503
+ for (const m of memoryEntries) {
504
+ if (m.sourceFeedbackId) {
505
+ memoryByFeedbackId.set(m.sourceFeedbackId, m);
506
+ }
507
+ }
508
+
509
+ const insert = db.transaction(() => {
510
+ let count = 0;
511
+ for (const fb of feedbackEntries) {
512
+ if (!fb.id || !fb.signal) continue;
513
+ const mem = memoryByFeedbackId.get(fb.id) || null;
514
+ upsertLesson(db, fb, mem);
515
+ count++;
516
+ }
517
+ return count;
518
+ });
519
+
520
+ return insert();
521
+ }
522
+
523
+ // ---------------------------------------------------------------------------
524
+ // Internal helpers
525
+ // ---------------------------------------------------------------------------
526
+
527
+ function sanitizeFtsQuery(query) {
528
+ // Escape FTS5 special chars, convert to prefix search terms
529
+ return query
530
+ .replace(/[":*()^~]/g, ' ')
531
+ .trim()
532
+ .split(/\s+/)
533
+ .filter(Boolean)
534
+ .map((term) => `"${term}"`)
535
+ .join(' ');
536
+ }
537
+
538
+ function fallbackLikeSearch(db, query, options, limit) {
539
+ const terms = query.split(/\s+/).filter(Boolean);
540
+ if (terms.length === 0) return [];
541
+
542
+ const conditions = terms.map(
543
+ () => `(context LIKE ? OR whatWentWrong LIKE ? OR whatToChange LIKE ? OR whatWorked LIKE ?)`,
544
+ );
545
+ const params = terms.flatMap((t) => {
546
+ const like = `%${t}%`;
547
+ return [like, like, like, like];
548
+ });
549
+
550
+ let sql = `SELECT * FROM lessons WHERE ${conditions.join(' AND ')}`;
551
+
552
+ if (options.signal) {
553
+ sql += ' AND signal = ?';
554
+ params.push(options.signal);
555
+ }
556
+ if (options.domain) {
557
+ sql += ' AND domain = ?';
558
+ params.push(options.domain);
559
+ }
560
+
561
+ sql += ' ORDER BY timestamp DESC LIMIT ?';
562
+ params.push(limit);
563
+
564
+ return db.prepare(sql).all(...params);
565
+ }
566
+
567
+ function parseRow(row) {
568
+ return {
569
+ ...row,
570
+ tags: safeParseTags(row.tags),
571
+ pruned: row.pruned === 1,
572
+ };
573
+ }
574
+
575
+ function safeParseTags(tagsStr) {
576
+ try {
577
+ const parsed = JSON.parse(tagsStr || '[]');
578
+ return Array.isArray(parsed) ? parsed : [];
579
+ } catch {
580
+ return [];
581
+ }
582
+ }
583
+
584
+ function readJsonlSafe(filePath) {
585
+ if (!fs.existsSync(filePath)) return [];
586
+ const raw = fs.readFileSync(filePath, 'utf-8').trim();
587
+ if (!raw) return [];
588
+ return raw
589
+ .split('\n')
590
+ .map((line) => {
591
+ try {
592
+ return JSON.parse(line);
593
+ } catch {
594
+ return null;
595
+ }
596
+ })
597
+ .filter(Boolean);
598
+ }
599
+
600
+ module.exports = {
601
+ initDB,
602
+ upsertLesson,
603
+ upsertSession,
604
+ searchSessions,
605
+ findDuplicate,
606
+ compactLessons,
607
+ searchLessons,
608
+ inferCorrectiveActions,
609
+ getStats,
610
+ getStatsFromDB,
611
+ backfillFromJsonl,
612
+ DEFAULT_DB_PATH,
613
+ };