thumbgate 1.3.0 → 1.4.1

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 (156) hide show
  1. package/.claude-plugin/README.md +25 -0
  2. package/.claude-plugin/marketplace.json +32 -13
  3. package/.claude-plugin/plugin.json +15 -2
  4. package/.well-known/llms.txt +60 -0
  5. package/.well-known/mcp/server-card.json +1 -1
  6. package/README.md +242 -126
  7. package/adapters/README.md +1 -1
  8. package/adapters/chatgpt/INSTALL.md +59 -4
  9. package/adapters/chatgpt/openapi.yaml +168 -0
  10. package/adapters/claude/.mcp.json +2 -2
  11. package/adapters/codex/config.toml +2 -2
  12. package/adapters/mcp/server-stdio.js +84 -1
  13. package/adapters/opencode/opencode.json +1 -1
  14. package/bin/cli.js +204 -13
  15. package/bin/postinstall.js +8 -2
  16. package/config/budget.json +18 -0
  17. package/config/gates/code-edit.json +61 -0
  18. package/config/gates/db-write.json +61 -0
  19. package/config/gates/default.json +154 -3
  20. package/config/gates/deploy.json +61 -0
  21. package/config/github-about.json +2 -1
  22. package/config/merge-quality-checks.json +23 -0
  23. package/openapi/openapi.yaml +168 -0
  24. package/package.json +47 -11
  25. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  26. package/plugins/claude-codex-bridge/.mcp.json +1 -1
  27. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
  28. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  29. package/plugins/codex-profile/.mcp.json +1 -1
  30. package/plugins/codex-profile/INSTALL.md +27 -4
  31. package/plugins/codex-profile/README.md +33 -9
  32. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  33. package/plugins/opencode-profile/INSTALL.md +1 -1
  34. package/public/blog.html +73 -0
  35. package/public/compare/mem0.html +189 -0
  36. package/public/compare/speclock.html +180 -0
  37. package/public/compare.html +10 -2
  38. package/public/guide.html +2 -2
  39. package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
  40. package/public/guides/codex-cli-guardrails.html +158 -0
  41. package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
  42. package/public/guides/pre-action-gates.html +162 -0
  43. package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
  44. package/public/index.html +172 -65
  45. package/public/lessons.html +33 -24
  46. package/public/llm-context.md +140 -0
  47. package/public/pro.html +24 -22
  48. package/scripts/access-anomaly-detector.js +1 -1
  49. package/scripts/adk-consolidator.js +1 -5
  50. package/scripts/agent-security-hardening.js +4 -6
  51. package/scripts/agentic-data-pipeline.js +1 -3
  52. package/scripts/async-job-runner.js +1 -5
  53. package/scripts/audit-trail.js +1 -5
  54. package/scripts/auto-promote-gates.js +5 -3
  55. package/scripts/background-agent-governance.js +2 -10
  56. package/scripts/billing-setup.js +109 -0
  57. package/scripts/billing.js +2 -16
  58. package/scripts/budget-enforcer.js +173 -0
  59. package/scripts/build-claude-mcpb.js +71 -5
  60. package/scripts/build-codex-plugin.js +152 -0
  61. package/scripts/check-congruence.js +132 -14
  62. package/scripts/commercial-offer.js +5 -7
  63. package/scripts/content-engine/linkedin-content-generator.js +154 -0
  64. package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
  65. package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
  66. package/scripts/content-engine/reddit-thread-finder.js +154 -0
  67. package/scripts/context-engine.js +21 -6
  68. package/scripts/contextfs.js +1 -21
  69. package/scripts/dashboard.js +20 -0
  70. package/scripts/decision-journal.js +341 -0
  71. package/scripts/delegation-runtime.js +1 -5
  72. package/scripts/distribution-surfaces.js +54 -0
  73. package/scripts/document-intake.js +927 -0
  74. package/scripts/ephemeral-agent-store.js +1 -8
  75. package/scripts/evolution-state.js +1 -5
  76. package/scripts/experiment-tracker.js +1 -5
  77. package/scripts/export-databricks-bundle.js +1 -5
  78. package/scripts/export-hf-dataset.js +1 -5
  79. package/scripts/export-training.js +1 -5
  80. package/scripts/feedback-attribution.js +1 -16
  81. package/scripts/feedback-history-distiller.js +1 -16
  82. package/scripts/feedback-loop.js +1 -5
  83. package/scripts/feedback-root-consolidator.js +2 -21
  84. package/scripts/feedback-session.js +49 -0
  85. package/scripts/feedback-to-rules.js +215 -36
  86. package/scripts/filesystem-search.js +1 -9
  87. package/scripts/fs-utils.js +104 -0
  88. package/scripts/gates-engine.js +200 -11
  89. package/scripts/github-about.js +32 -8
  90. package/scripts/gtm-revenue-loop.js +1 -5
  91. package/scripts/harness-selector.js +148 -0
  92. package/scripts/hosted-config.js +2 -0
  93. package/scripts/hosted-job-launcher.js +1 -5
  94. package/scripts/hybrid-feedback-context.js +33 -49
  95. package/scripts/intervention-policy.js +58 -1
  96. package/scripts/lesson-db.js +3 -18
  97. package/scripts/lesson-inference.js +194 -16
  98. package/scripts/lesson-retrieval.js +60 -24
  99. package/scripts/llm-client.js +59 -0
  100. package/scripts/managed-lesson-agent.js +183 -0
  101. package/scripts/marketing-experiment.js +8 -22
  102. package/scripts/meta-agent-loop.js +624 -0
  103. package/scripts/metered-billing.js +1 -1
  104. package/scripts/money-watcher.js +1 -4
  105. package/scripts/obsidian-export.js +1 -5
  106. package/scripts/operational-integrity.js +15 -3
  107. package/scripts/operational-summary.js +41 -5
  108. package/scripts/org-dashboard.js +6 -1
  109. package/scripts/per-step-scoring.js +2 -4
  110. package/scripts/pr-manager.js +201 -19
  111. package/scripts/pro-features.js +3 -2
  112. package/scripts/prompt-dlp.js +3 -3
  113. package/scripts/prove-adapters.js +1 -5
  114. package/scripts/prove-attribution.js +1 -5
  115. package/scripts/prove-automation.js +1 -3
  116. package/scripts/prove-cloudflare-sandbox.js +1 -3
  117. package/scripts/prove-data-pipeline.js +1 -3
  118. package/scripts/prove-intelligence.js +1 -3
  119. package/scripts/prove-lancedb.js +1 -5
  120. package/scripts/prove-local-intelligence.js +1 -3
  121. package/scripts/prove-packaged-runtime.js +75 -9
  122. package/scripts/prove-predictive-insights.js +1 -3
  123. package/scripts/prove-training-export.js +1 -3
  124. package/scripts/prove-workflow-contract.js +1 -5
  125. package/scripts/ralph-loop.js +376 -0
  126. package/scripts/ralph-mode-ci.js +331 -0
  127. package/scripts/rate-limiter.js +3 -1
  128. package/scripts/reddit-dm-outreach.js +14 -4
  129. package/scripts/rotate-stripe-webhook-secret.js +314 -0
  130. package/scripts/schedule-manager.js +3 -5
  131. package/scripts/security-scanner.js +448 -0
  132. package/scripts/self-distill-agent.js +579 -0
  133. package/scripts/semantic-dedup.js +115 -0
  134. package/scripts/skill-exporter.js +1 -3
  135. package/scripts/skill-generator.js +1 -5
  136. package/scripts/social-analytics/engagement-audit.js +1 -18
  137. package/scripts/social-analytics/pollers/linkedin.js +26 -16
  138. package/scripts/social-analytics/publishers/linkedin.js +1 -1
  139. package/scripts/social-analytics/publishers/zernio.js +51 -0
  140. package/scripts/social-pipeline.js +1 -3
  141. package/scripts/social-post-hourly.js +47 -4
  142. package/scripts/statusline-links.js +6 -5
  143. package/scripts/statusline.sh +29 -153
  144. package/scripts/sync-branch-protection.js +340 -0
  145. package/scripts/tessl-export.js +1 -3
  146. package/scripts/thumbgate-search.js +32 -1
  147. package/scripts/tool-kpi-tracker.js +1 -1
  148. package/scripts/tool-registry.js +106 -2
  149. package/scripts/vector-store.js +1 -5
  150. package/scripts/weekly-auto-post.js +1 -1
  151. package/scripts/workflow-sentinel.js +91 -0
  152. package/skills/thumbgate/SKILL.md +1 -1
  153. package/src/api/server.js +296 -7
  154. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  155. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  156. /package/scripts/social-analytics/db/{social-analytics.db-wal → analytics.sqlite} +0 -0
@@ -14,16 +14,9 @@
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
16
  const { resolveFeedbackDir } = require('./feedback-paths');
17
+ const { ensureDir, readJsonl } = require('./fs-utils');
17
18
 
18
19
  function getFeedbackDir() { return resolveFeedbackDir(); }
19
- function ensureDir(p) { if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); }
20
-
21
- function readJsonl(fp) {
22
- if (!fs.existsSync(fp)) return [];
23
- const raw = fs.readFileSync(fp, 'utf-8').trim();
24
- if (!raw) return [];
25
- return raw.split('\n').map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
26
- }
27
20
 
28
21
  // ---------------------------------------------------------------------------
29
22
  // 1. Per-Agent Namespace Isolation
@@ -3,6 +3,7 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
  const { resolveFeedbackDir } = require('./feedback-paths');
6
+ const { ensureDir } = require('./fs-utils');
6
7
 
7
8
  const DEFAULT_SETTINGS = Object.freeze({
8
9
  half_life_days: 7,
@@ -12,11 +13,6 @@ const DEFAULT_SETTINGS = Object.freeze({
12
13
  dpo_beta: 0.1,
13
14
  });
14
15
 
15
- function ensureDir(dirPath) {
16
- if (!fs.existsSync(dirPath)) {
17
- fs.mkdirSync(dirPath, { recursive: true });
18
- }
19
- }
20
16
 
21
17
  function appendJSONL(filePath, record) {
22
18
  ensureDir(path.dirname(filePath));
@@ -18,6 +18,7 @@
18
18
  const fs = require('fs');
19
19
  const path = require('path');
20
20
  const { getFeedbackPaths, readJSONL } = require('./feedback-loop');
21
+ const { ensureDir } = require('./fs-utils');
21
22
 
22
23
  // ---------------------------------------------------------------------------
23
24
  // Paths
@@ -31,11 +32,6 @@ function getExperimentPaths() {
31
32
  };
32
33
  }
33
34
 
34
- function ensureDir(dirPath) {
35
- if (!fs.existsSync(dirPath)) {
36
- fs.mkdirSync(dirPath, { recursive: true });
37
- }
38
- }
39
35
 
40
36
  function appendJSONL(filePath, record) {
41
37
  ensureDir(path.dirname(filePath));
@@ -5,6 +5,7 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
 
7
7
  const { getFeedbackPaths } = require('./feedback-loop');
8
+ const { ensureDir } = require('./fs-utils');
8
9
 
9
10
  const PROJECT_ROOT = path.join(__dirname, '..');
10
11
  const DEFAULT_PROOF_DIR = process.env.THUMBGATE_PROOF_DIR
@@ -20,11 +21,6 @@ function parseArgs(argv) {
20
21
  return args;
21
22
  }
22
23
 
23
- function ensureDir(dirPath) {
24
- if (!fs.existsSync(dirPath)) {
25
- fs.mkdirSync(dirPath, { recursive: true });
26
- }
27
- }
28
24
 
29
25
  function readJSONL(filePath) {
30
26
  if (!fs.existsSync(filePath)) return [];
@@ -26,6 +26,7 @@ const path = require('path');
26
26
  const { resolveFeedbackDir } = require('./feedback-paths');
27
27
  const { exportDpoFromMemories } = require('./export-dpo-pairs');
28
28
  const { getProvenance } = require('./contextfs');
29
+ const { ensureDir } = require('./fs-utils');
29
30
 
30
31
  // ---------------------------------------------------------------------------
31
32
  // Helpers
@@ -43,11 +44,6 @@ function readJSONL(filePath) {
43
44
  .filter(Boolean);
44
45
  }
45
46
 
46
- function ensureDir(dirPath) {
47
- if (!fs.existsSync(dirPath)) {
48
- fs.mkdirSync(dirPath, { recursive: true });
49
- }
50
- }
51
47
 
52
48
  function writeJSONL(filePath, rows) {
53
49
  const content = rows.map((row) => JSON.stringify(row)).join('\n');
@@ -17,6 +17,7 @@
17
17
  const fs = require('fs');
18
18
  const path = require('path');
19
19
  const { resolveFeedbackDir } = require('./feedback-paths');
20
+ const { ensureDir } = require('./fs-utils');
20
21
 
21
22
  const SEQUENCE_WINDOW = 10;
22
23
 
@@ -40,11 +41,6 @@ function readJSONL(filePath) {
40
41
  .filter(Boolean);
41
42
  }
42
43
 
43
- function ensureDir(dirPath) {
44
- if (!fs.existsSync(dirPath)) {
45
- fs.mkdirSync(dirPath, { recursive: true });
46
- }
47
- }
48
44
 
49
45
  // ---------------------------------------------------------------------------
50
46
  // XPRT-04: validateMemoryStructure
@@ -4,6 +4,7 @@
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const { resolveFeedbackDir } = require('./feedback-paths');
7
+ const { readJsonl } = require('./fs-utils');
7
8
 
8
9
  function getAttributionPaths(options = {}) {
9
10
  const feedbackDir = resolveFeedbackDir({
@@ -30,22 +31,6 @@ const STOPWORDS = new Set([
30
31
  'where', 'which', 'while', 'with', 'without', 'would', 'thumbs', 'down', 'up', 'please', 'avoid',
31
32
  ]);
32
33
 
33
- function readJsonl(filePath, maxLines = 500) {
34
- if (!filePath || !fs.existsSync(filePath)) return [];
35
- const lines = fs.readFileSync(filePath, 'utf8').split('\n');
36
- const out = [];
37
- for (let i = lines.length - 1; i >= 0 && out.length < maxLines; i -= 1) {
38
- const line = lines[i].trim();
39
- if (!line) continue;
40
- try {
41
- out.push(JSON.parse(line));
42
- } catch {
43
- // ignore malformed jsonl lines
44
- }
45
- }
46
- return out.reverse();
47
- }
48
-
49
34
  function appendJsonl(filePath, obj) {
50
35
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
51
36
  fs.appendFileSync(filePath, `${JSON.stringify(obj)}\n`);
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { resolveFeedbackDir: resolveSharedFeedbackDir } = require('./feedback-paths');
6
+ const { readJsonlTail } = require('./fs-utils');
6
7
 
7
8
  const DEFAULT_HISTORY_LIMIT = 10;
8
9
 
@@ -62,22 +63,6 @@ function appendJsonl(filePath, record) {
62
63
  fs.appendFileSync(filePath, `${JSON.stringify(record)}\n`);
63
64
  }
64
65
 
65
- function readJsonlTail(filePath, limit = DEFAULT_HISTORY_LIMIT) {
66
- if (!filePath || !fs.existsSync(filePath)) return [];
67
- const lines = fs.readFileSync(filePath, 'utf8').split('\n');
68
- const records = [];
69
- for (let index = lines.length - 1; index >= 0 && records.length < limit; index -= 1) {
70
- const line = lines[index].trim();
71
- if (!line) continue;
72
- try {
73
- records.push(JSON.parse(line));
74
- } catch {
75
- // ignore malformed lines
76
- }
77
- }
78
- return records.reverse();
79
- }
80
-
81
66
  function resolveFeedbackDir(feedbackDir) {
82
67
  if (feedbackDir) {
83
68
  return resolveSharedFeedbackDir({ feedbackDir });
@@ -36,6 +36,7 @@ const {
36
36
  aggregateFailureDiagnostics,
37
37
  } = require('./failure-diagnostics');
38
38
  const { getEffectiveSetting } = require('./evolution-state');
39
+ const { ensureDir } = require('./fs-utils');
39
40
  const {
40
41
  buildFeedbackPathsFromDir,
41
42
  getFeedbackPaths: resolveFeedbackPaths,
@@ -204,11 +205,6 @@ function getMemoryFirewallModule() {
204
205
  }
205
206
  }
206
207
 
207
- function ensureDir(dirPath) {
208
- if (!fs.existsSync(dirPath)) {
209
- fs.mkdirSync(dirPath, { recursive: true });
210
- }
211
- }
212
208
 
213
209
  function appendJSONL(filePath, record) {
214
210
  ensureDir(path.dirname(filePath));
@@ -3,6 +3,7 @@
3
3
 
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
+ const { ensureParentDir, readJsonl } = require('./fs-utils');
6
7
  const {
7
8
  getThumbgateFeedbackDir,
8
9
  listFeedbackArtifactPaths,
@@ -42,10 +43,6 @@ function getScopedProjectOptions(options = {}) {
42
43
  };
43
44
  }
44
45
 
45
- function ensureParentDir(filePath) {
46
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
47
- }
48
-
49
46
  function readJsonFile(filePath, fallbackFactory) {
50
47
  if (!filePath || !fs.existsSync(filePath)) return fallbackFactory();
51
48
  try {
@@ -56,22 +53,6 @@ function readJsonFile(filePath, fallbackFactory) {
56
53
  }
57
54
  }
58
55
 
59
- function readJsonlFile(filePath) {
60
- if (!filePath || !fs.existsSync(filePath)) return [];
61
- const raw = fs.readFileSync(filePath, 'utf8').trim();
62
- if (!raw) return [];
63
- return raw
64
- .split('\n')
65
- .map((line) => {
66
- try {
67
- return JSON.parse(line);
68
- } catch {
69
- return null;
70
- }
71
- })
72
- .filter(Boolean);
73
- }
74
-
75
56
  function stableArtifactTimestamp(record = {}) {
76
57
  return String(
77
58
  record.timestamp ||
@@ -176,7 +157,7 @@ function consolidateJsonArtifact(fileName, primaryPath, sourcePaths, write) {
176
157
  }
177
158
 
178
159
  function consolidateJsonlArtifact(fileName, primaryPath, sourcePaths, write) {
179
- const merged = dedupeJsonlRows(sourcePaths.flatMap((candidate) => readJsonlFile(candidate)));
160
+ const merged = dedupeJsonlRows(sourcePaths.flatMap((candidate) => readJsonl(candidate)));
180
161
  const initializedEmpty = sourcePaths.length === 0;
181
162
  const writeResult = writeIfChanged(primaryPath, serializeJsonl(merged), write);
182
163
 
@@ -161,6 +161,11 @@ function finalizeSession(sessionId) {
161
161
  persistSession(result);
162
162
  } catch (_err) { /* non-critical */ }
163
163
 
164
+ // Auto-infer lesson from the enriched session context (LangChain continual learning)
165
+ try {
166
+ autoInferLesson(result);
167
+ } catch (_err) { /* non-critical — lesson inference is best-effort */ }
168
+
164
169
  // Clean up from active sessions after a delay (allow reads)
165
170
  scheduleTimer(() => activeSessions.delete(sessionId), 5000);
166
171
 
@@ -261,6 +266,49 @@ function getActiveSession() {
261
266
  return null;
262
267
  }
263
268
 
269
+ /**
270
+ * Auto-infer a lesson from a finalized feedback session.
271
+ *
272
+ * Implements the Context-layer continual learning pattern identified by
273
+ * LangChain's three-layer framework (Model / Harness / Context):
274
+ * when a session finalizes, the enriched follow-up context is fed to
275
+ * lesson-inference so the next agent session starts with the new lesson
276
+ * already in recall — no retraining required.
277
+ */
278
+ function autoInferLesson(finalizedResult) {
279
+ const { inferFromSurroundingMessages, createLesson } = require('./lesson-inference');
280
+
281
+ const priorMessages = (finalizedResult.followUpMessages || []).map((m) => ({
282
+ role: m.role,
283
+ content: m.content,
284
+ }));
285
+
286
+ const inference = inferFromSurroundingMessages({
287
+ priorMessages,
288
+ followingMessages: [],
289
+ signal: finalizedResult.signal === 'up' || finalizedResult.signal === 'positive' ? 'positive' : 'negative',
290
+ feedbackContext: finalizedResult.enrichedContext || '',
291
+ });
292
+
293
+ if (!inference || !inference.inferredLesson) return null;
294
+
295
+ return createLesson({
296
+ feedbackId: finalizedResult.feedbackEventId,
297
+ signal: inference.signal,
298
+ inferredLesson: inference.inferredLesson,
299
+ triggerMessage: inference.triggerMessage,
300
+ priorSummary: inference.priorSummary,
301
+ confidence: inference.confidence,
302
+ tags: ['auto-inferred', 'session-finalize'],
303
+ metadata: {
304
+ sessionId: finalizedResult.sessionId,
305
+ followUpCount: finalizedResult.followUpCount,
306
+ duration: finalizedResult.duration,
307
+ source: 'auto-lesson-inference',
308
+ },
309
+ });
310
+ }
311
+
264
312
  /**
265
313
  * Persist finalized session to disk
266
314
  */
@@ -278,6 +326,7 @@ module.exports = {
278
326
  getSession,
279
327
  getActiveSession,
280
328
  extractComplaints,
329
+ autoInferLesson,
281
330
  SESSION_TIMEOUT_MS,
282
331
  MAX_FOLLOWUP_MESSAGES,
283
332
  scheduleTimer,
@@ -4,6 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { getAutoGatesPath } = require('./auto-promote-gates');
6
6
  const { resolveFeedbackDir } = require('./feedback-paths');
7
+ const { deduplicateFeedback } = require('./semantic-dedup');
7
8
 
8
9
  const DEFAULT_LOG = path.join(resolveFeedbackDir(), 'feedback-log.jsonl');
9
10
  const NEG = new Set(['negative', 'negative_strong', 'down', 'thumbs_down']);
@@ -51,19 +52,23 @@ function analyze(entries) {
51
52
  if (cls === 'negative') {
52
53
  const tool = e.tool_name || 'unknown';
53
54
  toolBuckets[tool] = (toolBuckets[tool] || 0) + 1;
54
- const key = normalize(e.context);
55
- if (key.length > 10) {
56
- if (!contextCounts[key]) {
57
- contextCounts[key] = {
58
- raw: e.context,
59
- count: 0,
60
- tool,
61
- tags: e.tags || [],
62
- hasHighRisk: (e.tags || []).some(t => HIGH_RISK_TAGS.has(t))
63
- };
64
- }
65
- contextCounts[key].count++;
66
- }
55
+ }
56
+ }
57
+
58
+ // Semantic dedup: cluster near-duplicate negatives into weighted "feedback tokens"
59
+ const negEntries = entries.filter((e) => classifySignal(e) === 'negative');
60
+ const dedupedNeg = deduplicateFeedback(negEntries);
61
+ for (const d of dedupedNeg) {
62
+ const key = normalize(d.context);
63
+ if (key.length > 10 && !contextCounts[key]) {
64
+ const tags = d._mergedTags || d.tags || [];
65
+ contextCounts[key] = {
66
+ raw: d.context,
67
+ count: d._clusterCount || 1,
68
+ tool: d.tool_name || 'unknown',
69
+ tags,
70
+ hasHighRisk: tags.some(t => HIGH_RISK_TAGS.has(t)),
71
+ };
67
72
  }
68
73
  }
69
74
 
@@ -102,35 +107,54 @@ function analyze(entries) {
102
107
 
103
108
  function promoteToGates(recurringIssues) {
104
109
  const autoGatePath = getAutoGatesPath();
105
- const autoGates = { version: 1, gates: [] };
106
-
110
+
111
+ // Load existing auto-gates to MERGE, not overwrite
112
+ let autoGates = { version: 1, gates: [], promotionLog: [] };
113
+ if (fs.existsSync(autoGatePath)) {
114
+ try {
115
+ autoGates = JSON.parse(fs.readFileSync(autoGatePath, 'utf-8'));
116
+ if (!Array.isArray(autoGates.gates)) autoGates.gates = [];
117
+ if (!Array.isArray(autoGates.promotionLog)) autoGates.promotionLog = [];
118
+ } catch { /* start fresh if corrupt */ }
119
+ }
120
+
121
+ const existingIds = new Set(autoGates.gates.map(g => g.id));
122
+ let added = 0;
123
+
107
124
  for (const issue of recurringIssues) {
108
125
  if (issue.severity === 'critical') {
109
- // Extract key nouns/verbs for pattern matching
110
126
  const keywords = issue.pattern
111
127
  .toLowerCase()
112
128
  .replace(/[^a-z0-9\s]/g, '')
113
129
  .split(/\s+/)
114
130
  .filter(w => w.length > 4)
115
131
  .slice(0, 3);
116
-
132
+
117
133
  if (keywords.length >= 2) {
118
134
  const pattern = keywords.join('.*');
135
+ const id = `auto-${issue.hasHighRisk ? 'hardened' : 'promoted'}-${Date.now().toString(36)}-${added}`;
136
+
137
+ // Skip if a gate with the same pattern already exists
138
+ const patternExists = autoGates.gates.some(g => g.pattern === pattern);
139
+ if (patternExists || existingIds.has(id)) continue;
140
+
119
141
  autoGates.gates.push({
120
- id: `auto-${issue.hasHighRisk ? 'hardened' : 'promoted'}-${Date.now().toString(36)}`,
142
+ id,
121
143
  pattern,
122
144
  action: 'block',
123
145
  message: `Automatically blocked due to repeated failures: ${issue.suggestedRule}`,
124
146
  severity: 'critical',
125
- source: 'feedback-auto-promotion'
147
+ source: 'feedback-to-rules',
148
+ promotedAt: new Date().toISOString(),
126
149
  });
150
+ added++;
127
151
  }
128
152
  }
129
153
  }
130
154
 
131
- if (autoGates.gates.length > 0) {
155
+ if (added > 0) {
132
156
  fs.mkdirSync(path.dirname(autoGatePath), { recursive: true });
133
- fs.writeFileSync(autoGatePath, JSON.stringify(autoGates, null, 2));
157
+ fs.writeFileSync(autoGatePath, JSON.stringify(autoGates, null, 2) + '\n');
134
158
  }
135
159
  }
136
160
 
@@ -138,27 +162,182 @@ function toRules(report) {
138
162
  const lines = ['# Suggested Rules from Feedback Analysis', `# Generated: ${report.generatedAt}`, ''];
139
163
  lines.push(`# Negative rate: ${report.negativeRate} (${report.negativeCount}/${report.totalFeedback})`);
140
164
  lines.push('');
165
+
166
+ if (!report.recurringIssues.length) {
167
+ lines.push('- No recurring issues detected.');
168
+ return lines.join('\n');
169
+ }
170
+
171
+ // Group by severity: critical → high → medium
172
+ const ORDER = ['critical', 'high', 'medium'];
173
+ const bySeverity = { critical: [], high: [], medium: [] };
141
174
  for (const issue of report.recurringIssues) {
142
- lines.push(`- [${issue.severity.toUpperCase()}] (${issue.count}x) ${issue.suggestedRule}`);
175
+ const sev = issue.severity || 'medium';
176
+ (bySeverity[sev] || bySeverity.medium).push(issue);
177
+ }
178
+
179
+ for (const sev of ORDER) {
180
+ const issues = bySeverity[sev];
181
+ if (!issues || !issues.length) continue;
182
+ lines.push(`## ${sev.toUpperCase()}`);
183
+ for (const issue of issues) {
184
+ const action = issue.action ? ` [${issue.action.toUpperCase()}]` : '';
185
+ lines.push(`- [${sev.toUpperCase()}]${action} (${issue.count}x) ${issue.suggestedRule}`);
186
+ if (issue.reasoning) lines.push(` > ${issue.reasoning}`);
187
+ }
188
+ lines.push('');
143
189
  }
144
- if (!report.recurringIssues.length) lines.push('- No recurring issues detected.');
190
+
145
191
  return lines.join('\n');
146
192
  }
147
193
 
148
- if (require.main === module) {
194
+ // ---------------------------------------------------------------------------
195
+ // LLM-Powered Rule Analysis
196
+ // ---------------------------------------------------------------------------
197
+
198
+ const LLM_RULES_SYSTEM_PROMPT = `You are a senior security engineer and AI agent safety architect at ThumbGate, responsible for creating prevention rules that block dangerous or unwanted AI agent behaviors before they execute.
199
+
200
+ <role>
201
+ You analyze patterns of developer frustration and AI agent failures to generate precise, actionable prevention rules. Your rules are loaded into a real-time PreToolUse gate that intercepts tool calls before they run. A bad rule that over-blocks degrades agent usefulness; a weak rule that under-blocks causes production incidents.
202
+ </role>
203
+
204
+ <chain_of_thought>
205
+ Before generating each rule, reason through:
206
+ 1. What is the root-cause pattern across similar failures?
207
+ 2. What is the minimum-specific regex that catches it without over-blocking legitimate use?
208
+ 3. Is this action irreversible (→ block) or risky-but-recoverable (→ warn)?
209
+ 4. What message explains WHY this is dangerous, not just what is blocked?
210
+ </chain_of_thought>
211
+
212
+ <examples>
213
+ Example 1 — Direct push to main:
214
+ Input: Multiple failures where agent pushed directly to main without a PR
215
+ Output rule:
216
+ {
217
+ "pattern": "push.*(?:main|master)(?!.*--dry-run)",
218
+ "action": "block",
219
+ "message": "Direct push to main is forbidden — create a PR and get CI green first",
220
+ "severity": "critical",
221
+ "reasoning": "Bypasses code review and CI gates; irreversible without force-push"
222
+ }
223
+
224
+ Example 2 — Deleting secrets:
225
+ Input: Agent ran rm -rf on production config files
226
+ Output rule:
227
+ {
228
+ "pattern": "rm\\\\s+-rf?\\\\s+(?:\\\\.env|config|credentials|secrets)",
229
+ "action": "block",
230
+ "message": "Deleting config/secrets files is blocked — use git checkout or restore instead",
231
+ "severity": "critical",
232
+ "reasoning": "Permanent deletion of secrets/config causes immediate production outage"
233
+ }
234
+ </examples>
235
+
236
+ Return ONLY a valid JSON array of rule objects:
237
+ [
238
+ {
239
+ "pattern": "<valid JavaScript regex string to match against tool call input>",
240
+ "action": "block" | "warn",
241
+ "message": "<explain WHY this is dangerous, not just what is blocked>",
242
+ "severity": "critical" | "high" | "medium",
243
+ "reasoning": "<root cause and risk analysis from the chain-of-thought>"
244
+ }
245
+ ]
246
+
247
+ Constraints:
248
+ - Pattern must be a valid JavaScript regex (used with new RegExp(pattern, 'i')).
249
+ - Prefer specific patterns: "force.*push.*main" beats "push".
250
+ - Use "block" for destructive/irreversible actions, "warn" for risky-but-recoverable.
251
+ - Deduplicate: one rule can cover multiple related failures.
252
+ - Return at most 10 rules, sorted by severity (critical first).
253
+ - Return ONLY the JSON array — no markdown, no explanation outside the array.`;
254
+
255
+ async function analyzeWithLLM(entries) {
256
+ const { isAvailable, callClaude, MODELS } = require('./llm-client');
257
+ if (!isAvailable()) return null;
258
+
259
+ const negativeEntries = entries
260
+ .filter((e) => classifySignal(e) === 'negative')
261
+ .filter((e) => (e.context || '').length > 20)
262
+ .slice(0, 30);
263
+
264
+ if (negativeEntries.length === 0) return null;
265
+
266
+ const batch = negativeEntries.map((e, i) => {
267
+ const ctx = (e.context || '').slice(0, 200);
268
+ const tool = e.tool_name || 'unknown';
269
+ const tags = (e.tags || []).join(', ');
270
+ const wentWrong = (e.what_went_wrong || e.whatWentWrong || '').slice(0, 150);
271
+ const toChange = (e.what_to_change || e.whatToChange || '').slice(0, 100);
272
+ let entry = `${i + 1}. [tool:${tool}] context: ${ctx}`;
273
+ if (wentWrong) entry += `\n what_went_wrong: ${wentWrong}`;
274
+ if (toChange) entry += `\n what_to_change: ${toChange}`;
275
+ if (tags) entry += `\n tags: ${tags}`;
276
+ return entry;
277
+ }).join('\n\n');
278
+
279
+ const raw = await callClaude({
280
+ systemPrompt: LLM_RULES_SYSTEM_PROMPT,
281
+ userPrompt: `Analyze these ${negativeEntries.length} negative feedback entries and generate prevention rules:\n\n${batch}`,
282
+ model: MODELS.SMART,
283
+ maxTokens: 2048,
284
+ });
285
+
286
+ if (!raw) return null;
287
+
149
288
  try {
150
- const logPath = process.argv[2] && !process.argv[2].startsWith('--') ? process.argv[2] : DEFAULT_LOG;
151
- const entries = parseFeedbackFile(logPath);
152
- const report = analyze(entries);
153
- if (process.argv.includes('--rules')) {
154
- console.log(toRules(report));
155
- } else {
156
- console.log(JSON.stringify(report, null, 2));
157
- }
158
- } catch (err) {
159
- console.error('Warning:', err.message);
289
+ const parsed = JSON.parse(raw);
290
+ if (!Array.isArray(parsed)) return null;
291
+
292
+ return parsed
293
+ .filter((r) => r.pattern && r.action && r.message && r.severity)
294
+ .slice(0, 10)
295
+ .map((r) => ({
296
+ pattern: r.pattern,
297
+ count: negativeEntries.length,
298
+ severity: ['critical', 'high', 'medium'].includes(r.severity) ? r.severity : 'medium',
299
+ hasHighRisk: r.severity === 'critical',
300
+ suggestedRule: r.message,
301
+ reasoning: r.reasoning || '',
302
+ source: 'llm-analysis',
303
+ }));
304
+ } catch {
305
+ return null;
160
306
  }
161
- process.exit(0);
162
307
  }
163
308
 
164
- module.exports = { parseFeedbackFile, classifySignal, analyze, toRules, normalize };
309
+ if (require.main === module) {
310
+ (async () => {
311
+ try {
312
+ const logPath = process.argv[2] && !process.argv[2].startsWith('--') ? process.argv[2] : DEFAULT_LOG;
313
+ const entries = parseFeedbackFile(logPath);
314
+ const useLLM = process.argv.includes('--llm');
315
+
316
+ let report;
317
+ if (useLLM) {
318
+ const llmIssues = await analyzeWithLLM(entries);
319
+ if (llmIssues) {
320
+ promoteToGates(llmIssues);
321
+ const heuristicReport = analyze(entries);
322
+ report = { ...heuristicReport, recurringIssues: llmIssues, source: 'llm' };
323
+ } else {
324
+ report = analyze(entries);
325
+ report.source = 'heuristic-fallback';
326
+ }
327
+ } else {
328
+ report = analyze(entries);
329
+ }
330
+
331
+ if (process.argv.includes('--rules')) {
332
+ console.log(toRules(report));
333
+ } else {
334
+ console.log(JSON.stringify(report, null, 2));
335
+ }
336
+ } catch (err) {
337
+ console.error('Warning:', err.message);
338
+ }
339
+ process.exit(0);
340
+ })();
341
+ }
342
+
343
+ module.exports = { parseFeedbackFile, classifySignal, analyze, analyzeWithLLM, promoteToGates, toRules, normalize };
@@ -20,6 +20,7 @@ const fs = require('node:fs');
20
20
  const path = require('node:path');
21
21
  const crypto = require('node:crypto');
22
22
  const { resolveFeedbackDir } = require('./feedback-paths');
23
+ const { readJsonl } = require('./fs-utils');
23
24
 
24
25
  const PROJECT_ROOT = path.join(__dirname, '..');
25
26
  const DEFAULT_FEEDBACK_DIR = resolveFeedbackDir();
@@ -37,15 +38,6 @@ function getContextFsDir() {
37
38
  return process.env.THUMBGATE_CONTEXTFS_DIR || path.join(getFeedbackDir(), 'contextfs');
38
39
  }
39
40
 
40
- function readJsonl(filePath) {
41
- if (!fs.existsSync(filePath)) return [];
42
- const raw = fs.readFileSync(filePath, 'utf-8').trim();
43
- if (!raw) return [];
44
- return raw.split('\n').map((line) => {
45
- try { return JSON.parse(line); } catch { return null; }
46
- }).filter(Boolean);
47
- }
48
-
49
41
  function listJsonFiles(dirPath) {
50
42
  if (!fs.existsSync(dirPath)) return [];
51
43
  const results = [];