thumbgate 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/.claude-plugin/marketplace.json +32 -13
  2. package/.claude-plugin/plugin.json +15 -2
  3. package/.well-known/llms.txt +60 -0
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +109 -20
  6. package/adapters/README.md +1 -1
  7. package/adapters/chatgpt/openapi.yaml +168 -0
  8. package/adapters/claude/.mcp.json +2 -2
  9. package/adapters/codex/config.toml +2 -2
  10. package/adapters/mcp/server-stdio.js +84 -1
  11. package/adapters/opencode/opencode.json +1 -1
  12. package/bin/cli.js +200 -13
  13. package/bin/postinstall.js +8 -2
  14. package/config/budget.json +18 -0
  15. package/config/gates/code-edit.json +61 -0
  16. package/config/gates/db-write.json +61 -0
  17. package/config/gates/default.json +154 -3
  18. package/config/gates/deploy.json +61 -0
  19. package/config/github-about.json +2 -1
  20. package/config/merge-quality-checks.json +23 -0
  21. package/openapi/openapi.yaml +168 -0
  22. package/package.json +42 -10
  23. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  24. package/plugins/claude-codex-bridge/.mcp.json +1 -1
  25. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
  26. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  27. package/plugins/codex-profile/.mcp.json +1 -1
  28. package/plugins/codex-profile/INSTALL.md +27 -4
  29. package/plugins/codex-profile/README.md +33 -9
  30. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  31. package/plugins/opencode-profile/INSTALL.md +1 -1
  32. package/public/blog.html +73 -0
  33. package/public/compare/mem0.html +189 -0
  34. package/public/compare/speclock.html +180 -0
  35. package/public/compare.html +10 -2
  36. package/public/guide.html +2 -2
  37. package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
  38. package/public/guides/codex-cli-guardrails.html +158 -0
  39. package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
  40. package/public/guides/pre-action-gates.html +162 -0
  41. package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
  42. package/public/index.html +136 -50
  43. package/public/lessons.html +33 -24
  44. package/public/llm-context.md +140 -0
  45. package/public/pro.html +24 -22
  46. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  47. package/scripts/access-anomaly-detector.js +1 -1
  48. package/scripts/adk-consolidator.js +1 -5
  49. package/scripts/agent-security-hardening.js +4 -6
  50. package/scripts/agentic-data-pipeline.js +1 -3
  51. package/scripts/async-job-runner.js +1 -5
  52. package/scripts/audit-trail.js +1 -5
  53. package/scripts/background-agent-governance.js +2 -10
  54. package/scripts/billing.js +2 -16
  55. package/scripts/budget-enforcer.js +173 -0
  56. package/scripts/build-codex-plugin.js +152 -0
  57. package/scripts/check-congruence.js +132 -14
  58. package/scripts/commercial-offer.js +5 -7
  59. package/scripts/content-engine/linkedin-content-generator.js +154 -0
  60. package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
  61. package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
  62. package/scripts/content-engine/reddit-thread-finder.js +154 -0
  63. package/scripts/context-engine.js +21 -6
  64. package/scripts/contextfs.js +1 -21
  65. package/scripts/dashboard.js +20 -0
  66. package/scripts/decision-journal.js +341 -0
  67. package/scripts/delegation-runtime.js +1 -5
  68. package/scripts/distribution-surfaces.js +26 -0
  69. package/scripts/document-intake.js +927 -0
  70. package/scripts/ephemeral-agent-store.js +1 -8
  71. package/scripts/evolution-state.js +1 -5
  72. package/scripts/experiment-tracker.js +1 -5
  73. package/scripts/export-databricks-bundle.js +1 -5
  74. package/scripts/export-hf-dataset.js +1 -5
  75. package/scripts/export-training.js +1 -5
  76. package/scripts/feedback-attribution.js +1 -16
  77. package/scripts/feedback-history-distiller.js +1 -16
  78. package/scripts/feedback-loop.js +1 -5
  79. package/scripts/feedback-root-consolidator.js +2 -21
  80. package/scripts/feedback-session.js +49 -0
  81. package/scripts/feedback-to-rules.js +188 -28
  82. package/scripts/filesystem-search.js +1 -9
  83. package/scripts/fs-utils.js +104 -0
  84. package/scripts/gates-engine.js +149 -4
  85. package/scripts/github-about.js +32 -8
  86. package/scripts/gtm-revenue-loop.js +1 -5
  87. package/scripts/harness-selector.js +148 -0
  88. package/scripts/hosted-job-launcher.js +1 -5
  89. package/scripts/hybrid-feedback-context.js +7 -33
  90. package/scripts/intervention-policy.js +58 -1
  91. package/scripts/lesson-db.js +3 -18
  92. package/scripts/lesson-inference.js +194 -16
  93. package/scripts/lesson-retrieval.js +60 -24
  94. package/scripts/llm-client.js +59 -0
  95. package/scripts/managed-lesson-agent.js +183 -0
  96. package/scripts/marketing-experiment.js +8 -22
  97. package/scripts/meta-agent-loop.js +624 -0
  98. package/scripts/metered-billing.js +1 -1
  99. package/scripts/money-watcher.js +1 -4
  100. package/scripts/obsidian-export.js +1 -5
  101. package/scripts/operational-integrity.js +15 -3
  102. package/scripts/org-dashboard.js +6 -1
  103. package/scripts/per-step-scoring.js +2 -4
  104. package/scripts/pr-manager.js +201 -19
  105. package/scripts/pro-features.js +3 -2
  106. package/scripts/prompt-dlp.js +3 -3
  107. package/scripts/prove-adapters.js +1 -5
  108. package/scripts/prove-attribution.js +1 -5
  109. package/scripts/prove-automation.js +1 -3
  110. package/scripts/prove-cloudflare-sandbox.js +1 -3
  111. package/scripts/prove-data-pipeline.js +1 -3
  112. package/scripts/prove-intelligence.js +1 -3
  113. package/scripts/prove-lancedb.js +1 -5
  114. package/scripts/prove-local-intelligence.js +1 -3
  115. package/scripts/prove-packaged-runtime.js +75 -9
  116. package/scripts/prove-predictive-insights.js +1 -3
  117. package/scripts/prove-training-export.js +1 -3
  118. package/scripts/prove-workflow-contract.js +1 -5
  119. package/scripts/rate-limiter.js +3 -1
  120. package/scripts/reddit-dm-outreach.js +14 -4
  121. package/scripts/schedule-manager.js +3 -5
  122. package/scripts/security-scanner.js +448 -0
  123. package/scripts/self-distill-agent.js +579 -0
  124. package/scripts/semantic-dedup.js +115 -0
  125. package/scripts/skill-exporter.js +1 -3
  126. package/scripts/skill-generator.js +1 -5
  127. package/scripts/social-analytics/engagement-audit.js +1 -18
  128. package/scripts/social-analytics/pollers/linkedin.js +26 -16
  129. package/scripts/social-analytics/publishers/linkedin.js +1 -1
  130. package/scripts/social-analytics/publishers/zernio.js +51 -0
  131. package/scripts/social-pipeline.js +1 -3
  132. package/scripts/social-post-hourly.js +47 -4
  133. package/scripts/statusline-links.js +6 -5
  134. package/scripts/statusline.sh +29 -153
  135. package/scripts/sync-branch-protection.js +340 -0
  136. package/scripts/tessl-export.js +1 -3
  137. package/scripts/thumbgate-search.js +32 -1
  138. package/scripts/tool-kpi-tracker.js +1 -1
  139. package/scripts/tool-registry.js +106 -2
  140. package/scripts/vector-store.js +1 -5
  141. package/scripts/weekly-auto-post.js +1 -1
  142. package/scripts/workflow-sentinel.js +91 -0
  143. package/skills/thumbgate/SKILL.md +1 -1
  144. package/src/api/server.js +273 -4
  145. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  146. /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
 
@@ -138,27 +143,182 @@ function toRules(report) {
138
143
  const lines = ['# Suggested Rules from Feedback Analysis', `# Generated: ${report.generatedAt}`, ''];
139
144
  lines.push(`# Negative rate: ${report.negativeRate} (${report.negativeCount}/${report.totalFeedback})`);
140
145
  lines.push('');
146
+
147
+ if (!report.recurringIssues.length) {
148
+ lines.push('- No recurring issues detected.');
149
+ return lines.join('\n');
150
+ }
151
+
152
+ // Group by severity: critical → high → medium
153
+ const ORDER = ['critical', 'high', 'medium'];
154
+ const bySeverity = { critical: [], high: [], medium: [] };
141
155
  for (const issue of report.recurringIssues) {
142
- lines.push(`- [${issue.severity.toUpperCase()}] (${issue.count}x) ${issue.suggestedRule}`);
156
+ const sev = issue.severity || 'medium';
157
+ (bySeverity[sev] || bySeverity.medium).push(issue);
143
158
  }
144
- if (!report.recurringIssues.length) lines.push('- No recurring issues detected.');
159
+
160
+ for (const sev of ORDER) {
161
+ const issues = bySeverity[sev];
162
+ if (!issues || !issues.length) continue;
163
+ lines.push(`## ${sev.toUpperCase()}`);
164
+ for (const issue of issues) {
165
+ const action = issue.action ? ` [${issue.action.toUpperCase()}]` : '';
166
+ lines.push(`- [${sev.toUpperCase()}]${action} (${issue.count}x) ${issue.suggestedRule}`);
167
+ if (issue.reasoning) lines.push(` > ${issue.reasoning}`);
168
+ }
169
+ lines.push('');
170
+ }
171
+
145
172
  return lines.join('\n');
146
173
  }
147
174
 
148
- if (require.main === module) {
175
+ // ---------------------------------------------------------------------------
176
+ // LLM-Powered Rule Analysis
177
+ // ---------------------------------------------------------------------------
178
+
179
+ 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.
180
+
181
+ <role>
182
+ 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.
183
+ </role>
184
+
185
+ <chain_of_thought>
186
+ Before generating each rule, reason through:
187
+ 1. What is the root-cause pattern across similar failures?
188
+ 2. What is the minimum-specific regex that catches it without over-blocking legitimate use?
189
+ 3. Is this action irreversible (→ block) or risky-but-recoverable (→ warn)?
190
+ 4. What message explains WHY this is dangerous, not just what is blocked?
191
+ </chain_of_thought>
192
+
193
+ <examples>
194
+ Example 1 — Direct push to main:
195
+ Input: Multiple failures where agent pushed directly to main without a PR
196
+ Output rule:
197
+ {
198
+ "pattern": "push.*(?:main|master)(?!.*--dry-run)",
199
+ "action": "block",
200
+ "message": "Direct push to main is forbidden — create a PR and get CI green first",
201
+ "severity": "critical",
202
+ "reasoning": "Bypasses code review and CI gates; irreversible without force-push"
203
+ }
204
+
205
+ Example 2 — Deleting secrets:
206
+ Input: Agent ran rm -rf on production config files
207
+ Output rule:
208
+ {
209
+ "pattern": "rm\\\\s+-rf?\\\\s+(?:\\\\.env|config|credentials|secrets)",
210
+ "action": "block",
211
+ "message": "Deleting config/secrets files is blocked — use git checkout or restore instead",
212
+ "severity": "critical",
213
+ "reasoning": "Permanent deletion of secrets/config causes immediate production outage"
214
+ }
215
+ </examples>
216
+
217
+ Return ONLY a valid JSON array of rule objects:
218
+ [
219
+ {
220
+ "pattern": "<valid JavaScript regex string to match against tool call input>",
221
+ "action": "block" | "warn",
222
+ "message": "<explain WHY this is dangerous, not just what is blocked>",
223
+ "severity": "critical" | "high" | "medium",
224
+ "reasoning": "<root cause and risk analysis from the chain-of-thought>"
225
+ }
226
+ ]
227
+
228
+ Constraints:
229
+ - Pattern must be a valid JavaScript regex (used with new RegExp(pattern, 'i')).
230
+ - Prefer specific patterns: "force.*push.*main" beats "push".
231
+ - Use "block" for destructive/irreversible actions, "warn" for risky-but-recoverable.
232
+ - Deduplicate: one rule can cover multiple related failures.
233
+ - Return at most 10 rules, sorted by severity (critical first).
234
+ - Return ONLY the JSON array — no markdown, no explanation outside the array.`;
235
+
236
+ async function analyzeWithLLM(entries) {
237
+ const { isAvailable, callClaude, MODELS } = require('./llm-client');
238
+ if (!isAvailable()) return null;
239
+
240
+ const negativeEntries = entries
241
+ .filter((e) => classifySignal(e) === 'negative')
242
+ .filter((e) => (e.context || '').length > 20)
243
+ .slice(0, 30);
244
+
245
+ if (negativeEntries.length === 0) return null;
246
+
247
+ const batch = negativeEntries.map((e, i) => {
248
+ const ctx = (e.context || '').slice(0, 200);
249
+ const tool = e.tool_name || 'unknown';
250
+ const tags = (e.tags || []).join(', ');
251
+ const wentWrong = (e.what_went_wrong || e.whatWentWrong || '').slice(0, 150);
252
+ const toChange = (e.what_to_change || e.whatToChange || '').slice(0, 100);
253
+ let entry = `${i + 1}. [tool:${tool}] context: ${ctx}`;
254
+ if (wentWrong) entry += `\n what_went_wrong: ${wentWrong}`;
255
+ if (toChange) entry += `\n what_to_change: ${toChange}`;
256
+ if (tags) entry += `\n tags: ${tags}`;
257
+ return entry;
258
+ }).join('\n\n');
259
+
260
+ const raw = await callClaude({
261
+ systemPrompt: LLM_RULES_SYSTEM_PROMPT,
262
+ userPrompt: `Analyze these ${negativeEntries.length} negative feedback entries and generate prevention rules:\n\n${batch}`,
263
+ model: MODELS.SMART,
264
+ maxTokens: 2048,
265
+ });
266
+
267
+ if (!raw) return null;
268
+
149
269
  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);
270
+ const parsed = JSON.parse(raw);
271
+ if (!Array.isArray(parsed)) return null;
272
+
273
+ return parsed
274
+ .filter((r) => r.pattern && r.action && r.message && r.severity)
275
+ .slice(0, 10)
276
+ .map((r) => ({
277
+ pattern: r.pattern,
278
+ count: negativeEntries.length,
279
+ severity: ['critical', 'high', 'medium'].includes(r.severity) ? r.severity : 'medium',
280
+ hasHighRisk: r.severity === 'critical',
281
+ suggestedRule: r.message,
282
+ reasoning: r.reasoning || '',
283
+ source: 'llm-analysis',
284
+ }));
285
+ } catch {
286
+ return null;
160
287
  }
161
- process.exit(0);
162
288
  }
163
289
 
164
- module.exports = { parseFeedbackFile, classifySignal, analyze, toRules, normalize };
290
+ if (require.main === module) {
291
+ (async () => {
292
+ try {
293
+ const logPath = process.argv[2] && !process.argv[2].startsWith('--') ? process.argv[2] : DEFAULT_LOG;
294
+ const entries = parseFeedbackFile(logPath);
295
+ const useLLM = process.argv.includes('--llm');
296
+
297
+ let report;
298
+ if (useLLM) {
299
+ const llmIssues = await analyzeWithLLM(entries);
300
+ if (llmIssues) {
301
+ promoteToGates(llmIssues);
302
+ const heuristicReport = analyze(entries);
303
+ report = { ...heuristicReport, recurringIssues: llmIssues, source: 'llm' };
304
+ } else {
305
+ report = analyze(entries);
306
+ report.source = 'heuristic-fallback';
307
+ }
308
+ } else {
309
+ report = analyze(entries);
310
+ }
311
+
312
+ if (process.argv.includes('--rules')) {
313
+ console.log(toRules(report));
314
+ } else {
315
+ console.log(JSON.stringify(report, null, 2));
316
+ }
317
+ } catch (err) {
318
+ console.error('Warning:', err.message);
319
+ }
320
+ process.exit(0);
321
+ })();
322
+ }
323
+
324
+ 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 = [];