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
@@ -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 { ensureParentDir, readJsonl } = require('./fs-utils');
20
21
  const {
21
22
  buildStableId,
22
23
  extractFilePaths,
@@ -39,15 +40,6 @@ function getLessonBaseUrl() {
39
40
  function getLessonsPath() { return path.join(getFeedbackDir(), LESSONS_FILE); }
40
41
  function getRecentLessonPath() { return path.join(getFeedbackDir(), RECENT_LESSON_FILE); }
41
42
 
42
- function readJsonl(fp) {
43
- if (!fs.existsSync(fp)) return [];
44
- const raw = fs.readFileSync(fp, 'utf-8').trim();
45
- if (!raw) return [];
46
- return raw.split('\n').map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
47
- }
48
-
49
- function ensureDir(p) { const d = path.dirname(p); if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true }); }
50
-
51
43
  // ---------------------------------------------------------------------------
52
44
  // 1. Surrounding Message Context Extraction
53
45
  // ---------------------------------------------------------------------------
@@ -146,7 +138,7 @@ function createLesson({ feedbackId, signal, inferredLesson, triggerMessage, prio
146
138
  lesson.link = `${getLessonBaseUrl()}/lessons#${lesson.id}`;
147
139
 
148
140
  const lessonsPath = getLessonsPath();
149
- ensureDir(lessonsPath);
141
+ ensureParentDir(lessonsPath);
150
142
  fs.appendFileSync(lessonsPath, JSON.stringify(lesson) + '\n');
151
143
 
152
144
  // Update recent lesson for statusbar
@@ -165,6 +157,55 @@ function getRecentLesson() {
165
157
  try { return JSON.parse(fs.readFileSync(p, 'utf-8')); } catch { return null; }
166
158
  }
167
159
 
160
+ function isNegativeSignal(signal) {
161
+ return signal === 'negative' || signal === 'down';
162
+ }
163
+
164
+ function isPositiveSignal(signal) {
165
+ return signal === 'positive' || signal === 'up';
166
+ }
167
+
168
+ function selectStatusbarLesson() {
169
+ const lessons = readJsonl(getLessonsPath())
170
+ .slice()
171
+ .sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0));
172
+ const latestNegative = lessons.find((lesson) => isNegativeSignal(lesson.signal));
173
+ if (latestNegative) return latestNegative;
174
+ const latestPositive = lessons.find((lesson) => isPositiveSignal(lesson.signal));
175
+ if (latestPositive) return latestPositive;
176
+ return getRecentLesson();
177
+ }
178
+
179
+ function getLessonKind(lesson = {}) {
180
+ const normalizedTitle = String(lesson.lesson || '').trim();
181
+ if (isNegativeSignal(lesson.signal) || /^MISTAKE:/i.test(normalizedTitle)) return 'mistake';
182
+ if (isPositiveSignal(lesson.signal) || /^SUCCESS:/i.test(normalizedTitle)) return 'success';
183
+ if (/^LEARNING:/i.test(normalizedTitle)) return 'learning';
184
+ if (/^PREFERENCE:/i.test(normalizedTitle)) return 'preference';
185
+ return 'lesson';
186
+ }
187
+
188
+ function stripLessonPrefix(lessonText = '') {
189
+ return String(lessonText || '').replace(/^(MISTAKE|SUCCESS|LEARNING|PREFERENCE):\s*/i, '').trim();
190
+ }
191
+
192
+ function formatLessonTimestamp(createdAt = '') {
193
+ const parsed = new Date(createdAt);
194
+ if (!Number.isFinite(parsed.getTime())) return '';
195
+ return parsed.toISOString().slice(0, 16).replace('T', ' ') + 'Z';
196
+ }
197
+
198
+ function buildStatusbarLessonLabel(lesson = {}) {
199
+ const kind = getLessonKind(lesson);
200
+ const prefix = kind === 'mistake'
201
+ ? 'Latest mistake'
202
+ : kind === 'success'
203
+ ? 'Latest success'
204
+ : 'Latest lesson';
205
+ const timestamp = formatLessonTimestamp(lesson.createdAt);
206
+ return timestamp ? `${prefix} ${timestamp}` : prefix;
207
+ }
208
+
168
209
  /**
169
210
  * Search lessons by query text.
170
211
  */
@@ -196,6 +237,59 @@ function getLessonStats() {
196
237
  return { total: lessons.length, positive, negative, avgConfidence };
197
238
  }
198
239
 
240
+ // ---------------------------------------------------------------------------
241
+ // 2b. Context Stuffing — dump all lessons for injection into agent context
242
+ // ---------------------------------------------------------------------------
243
+
244
+ /**
245
+ * Returns ALL lessons condensed for context-window injection.
246
+ * Bypasses RAG/search — just stuff everything into context.
247
+ * For most projects (20-200 lessons), this is 1K-10K tokens.
248
+ * @param {object} opts
249
+ * @param {number} opts.maxTokenBudget - approximate token budget (default 10000)
250
+ * @param {string} opts.signal - filter by 'positive' or 'negative'
251
+ * @param {string} opts.format - 'compact' (default) or 'full'
252
+ * @returns {{ lessons: string, count: number, truncated: boolean }}
253
+ */
254
+ function getAllLessonsForContext({ maxTokenBudget = 10000, signal, format = 'compact' } = {}) {
255
+ let lessons = readJsonl(getLessonsPath());
256
+ if (signal) lessons = lessons.filter((l) => l.signal === signal || (signal === 'negative' && l.signal === 'down') || (signal === 'positive' && l.signal === 'up'));
257
+
258
+ // Sort by confidence descending — most important lessons first
259
+ lessons.sort((a, b) => (b.confidence || 0) - (a.confidence || 0));
260
+
261
+ const lines = [];
262
+ let approxTokens = 0;
263
+ let truncated = false;
264
+
265
+ for (const l of lessons) {
266
+ let line;
267
+ if (format === 'compact') {
268
+ const sig = l.signal === 'positive' || l.signal === 'up' ? 'DO' : 'AVOID';
269
+ line = `[${sig}] ${l.lesson || l.inferredLesson || ''}`;
270
+ } else {
271
+ line = JSON.stringify({ signal: l.signal, lesson: l.lesson || l.inferredLesson, confidence: l.confidence, tags: l.tags, createdAt: l.createdAt });
272
+ }
273
+
274
+ const lineTokens = Math.ceil(line.length / 4); // rough token estimate
275
+ if (approxTokens + lineTokens > maxTokenBudget) {
276
+ truncated = true;
277
+ break;
278
+ }
279
+
280
+ lines.push(line);
281
+ approxTokens += lineTokens;
282
+ }
283
+
284
+ return {
285
+ lessons: lines.join('\n'),
286
+ count: lines.length,
287
+ totalAvailable: lessons.length,
288
+ truncated,
289
+ approxTokens,
290
+ };
291
+ }
292
+
199
293
  // ---------------------------------------------------------------------------
200
294
  // 3. Statusbar Data Provider
201
295
  // ---------------------------------------------------------------------------
@@ -205,19 +299,21 @@ function getLessonStats() {
205
299
  * Returns the most recent lesson with link, formatted for display.
206
300
  */
207
301
  function getStatusbarLessonData() {
208
- const recent = getRecentLesson();
302
+ const recent = selectStatusbarLesson();
209
303
  if (!recent) return { hasLesson: false, text: null, link: null };
210
304
 
211
- const emoji = (recent.signal === 'negative' || recent.signal === 'down') ? '👎' : '👍';
212
- const truncated = recent.lesson.length > 60 ? recent.lesson.slice(0, 57) + '...' : recent.lesson;
305
+ const normalizedLesson = stripLessonPrefix(recent.lesson || '');
306
+ const truncated = normalizedLesson.length > 48 ? normalizedLesson.slice(0, 45) + '...' : normalizedLesson;
213
307
 
214
308
  return {
215
309
  hasLesson: true,
216
- text: `${emoji} ${truncated}`,
310
+ text: truncated,
217
311
  link: recent.link,
218
312
  lessonId: recent.id,
219
313
  confidence: recent.confidence,
220
314
  createdAt: recent.createdAt,
315
+ label: buildStatusbarLessonLabel(recent),
316
+ kind: getLessonKind(recent),
221
317
  };
222
318
  }
223
319
 
@@ -306,10 +402,92 @@ function consumePhrase(lower, original, phrases) {
306
402
  return null;
307
403
  }
308
404
 
405
+ // ---------------------------------------------------------------------------
406
+ // 6. LLM-Powered Structured Lesson Extraction
407
+ // ---------------------------------------------------------------------------
408
+
409
+ const LLM_LESSON_SYSTEM_PROMPT = `You are a lesson extraction engine for an AI coding agent safety system called ThumbGate.
410
+
411
+ Given a conversation window and a feedback signal (positive or negative), extract a structured lesson.
412
+
413
+ Return ONLY valid JSON matching this exact schema:
414
+ {
415
+ "trigger": { "condition": "<when this lesson applies>", "type": "<one of: debugging, implementation, question, error-report, constraint>" },
416
+ "action": { "type": "<do or avoid>", "description": "<specific action to take or avoid>" },
417
+ "confidence": <0.0 to 1.0>,
418
+ "scope": "<global, file-level, or project-level>",
419
+ "tags": ["<relevant tags>"]
420
+ }
421
+
422
+ Guidelines:
423
+ - Be specific and actionable. "Avoid: editing files without reading them first" is better than "Avoid: bad edits".
424
+ - confidence should reflect how clear the lesson is from the conversation context.
425
+ - tags should include tool names, file types, or domain areas mentioned.
426
+ - Do NOT include any text outside the JSON object.`;
427
+
428
+ async function inferStructuredLessonLLM(conversationWindow, signal, context) {
429
+ const { isAvailable, callClaude, MODELS } = require('./llm-client');
430
+ if (!isAvailable()) return null;
431
+
432
+ const normalizedWindow = Array.isArray(conversationWindow) ? conversationWindow : [];
433
+ if (normalizedWindow.length === 0 && !context) return null;
434
+
435
+ const windowText = normalizedWindow
436
+ .slice(-10)
437
+ .map((m) => `[${m.role}]: ${(m.content || '').slice(0, 400)}`)
438
+ .join('\n')
439
+ .slice(0, 4000);
440
+
441
+ const userPrompt = [
442
+ `Signal: ${signal === 'positive' || signal === 'up' ? 'positive (thumbs up — something worked well)' : 'negative (thumbs down — something went wrong)'}`,
443
+ context ? `User context: ${context}` : '',
444
+ `\nConversation:\n${windowText}`,
445
+ ].filter(Boolean).join('\n');
446
+
447
+ const raw = await callClaude({
448
+ systemPrompt: LLM_LESSON_SYSTEM_PROMPT,
449
+ userPrompt,
450
+ model: MODELS.FAST,
451
+ maxTokens: 512,
452
+ });
453
+
454
+ if (!raw) return null;
455
+
456
+ try {
457
+ const parsed = JSON.parse(raw);
458
+ if (!parsed.trigger || !parsed.action) return null;
459
+
460
+ const filePaths = extractFilePaths(normalizedWindow);
461
+ const toolCalls = extractToolCalls(normalizedWindow);
462
+ const errorPatterns = extractErrors(normalizedWindow);
463
+ const userMessages = normalizedWindow.filter((m) => m.role === 'user');
464
+ const assistantMessages = normalizedWindow.filter((m) => m.role === 'assistant');
465
+ const lastUser = userMessages[userMessages.length - 1]?.content || '';
466
+ const lastAssistant = assistantMessages[assistantMessages.length - 1]?.content || '';
467
+
468
+ return {
469
+ format: 'if-then-v1-llm',
470
+ trigger: parsed.trigger,
471
+ action: parsed.action,
472
+ signal: signal === 'positive' || signal === 'up' ? 'positive' : 'negative',
473
+ confidence: Math.max(0, Math.min(1, Number(parsed.confidence) || 0.5)),
474
+ scope: parsed.scope || inferScope(filePaths, toolCalls),
475
+ examples: [{ userIntent: lastUser.slice(0, 300), assistantAction: lastAssistant.slice(0, 300), outcome: signal === 'positive' || signal === 'up' ? 'approved' : 'rejected' }],
476
+ metadata: { toolsUsed: toolCalls, filesInvolved: filePaths.slice(0, 10), errorPatterns: errorPatterns.slice(0, 5), conversationLength: normalizedWindow.length, inferredAt: new Date().toISOString(), llmModel: MODELS.FAST },
477
+ tags: Array.isArray(parsed.tags) ? parsed.tags : [],
478
+ };
479
+ } catch {
480
+ return null;
481
+ }
482
+ }
483
+
309
484
  module.exports = {
310
485
  inferFromSurroundingMessages, createLesson, getRecentLesson,
311
- searchLessons, getLessonStats, getStatusbarLessonData,
486
+ searchLessons, getLessonStats, getStatusbarLessonData, getAllLessonsForContext,
312
487
  getLessonsPath, getRecentLessonPath,
313
- inferStructuredLesson, extractTrigger, extractAction, extractToolCalls,
488
+ selectStatusbarLesson, getLessonKind, stripLessonPrefix,
489
+ formatLessonTimestamp, buildStatusbarLessonLabel,
490
+ inferStructuredLesson, inferStructuredLessonLLM,
491
+ extractTrigger, extractAction, extractToolCalls,
314
492
  extractFilePaths, extractErrors, calculateConfidence, inferScope,
315
493
  };
@@ -3,11 +3,10 @@
3
3
 
4
4
  /**
5
5
  * Per-action lesson retrieval.
6
- * Given a tool name + context, returns the top-K most relevant lessons
7
- * using keyword matching + recency decay + signal weighting.
6
+ * v2: backward retrieval + bigram Jaccard fuzzy matching
8
7
  */
9
8
 
10
- const RECENCY_DECAY_DAYS = 30; // lessons older than this get down-weighted
9
+ const RECENCY_DECAY_DAYS = 30;
11
10
 
12
11
  function retrieveRelevantLessons(toolName, actionContext, options = {}) {
13
12
  const { maxResults = 5, feedbackDir } = options;
@@ -20,13 +19,13 @@ function retrieveRelevantLessons(toolName, actionContext, options = {}) {
20
19
  const memories = readJSONL(paths.MEMORY_LOG_PATH, { maxLines: 200 });
21
20
  if (memories.length === 0) return [];
22
21
 
23
- // Score each memory against the current action
22
+ const actionSig = buildActionSignature(toolName, actionContext);
23
+
24
24
  const scored = memories.map((mem) => ({
25
25
  ...mem,
26
- relevanceScore: scoreRelevance(mem, toolName, actionContext),
26
+ relevanceScore: scoreRelevance(mem, toolName, actionContext, actionSig),
27
27
  }));
28
28
 
29
- // Sort by relevance, return top-K
30
29
  return scored
31
30
  .filter((m) => m.relevanceScore > 0.1)
32
31
  .sort((a, b) => b.relevanceScore - a.relevanceScore)
@@ -42,43 +41,74 @@ function retrieveRelevantLessons(toolName, actionContext, options = {}) {
42
41
  }));
43
42
  }
44
43
 
45
- function scoreRelevance(memory, toolName, actionContext) {
44
+ function buildActionSignature(toolName, actionContext) {
45
+ const toolLower = (toolName || '').toLowerCase();
46
+ const contextLower = (actionContext || '').toLowerCase();
47
+ const sigPaths = extractPaths(actionContext);
48
+ const tokens = tokenize(contextLower);
49
+ const ngramSet = textBigrams(contextLower);
50
+ return { toolLower, contextLower, paths: sigPaths, tokens, ngramSet };
51
+ }
52
+
53
+ function textBigrams(text) {
54
+ const normalized = (text || '')
55
+ .toLowerCase()
56
+ .replace(/[^a-z0-9\s]/g, ' ')
57
+ .replace(/\s+/g, ' ')
58
+ .trim();
59
+ const set = new Set();
60
+ for (let i = 0; i < normalized.length - 1; i++) {
61
+ set.add(normalized.slice(i, i + 2));
62
+ }
63
+ return set;
64
+ }
65
+
66
+ function bigramJaccard(setA, setB) {
67
+ if (setA.size === 0 && setB.size === 0) return 1;
68
+ if (setA.size === 0 || setB.size === 0) return 0;
69
+ let intersection = 0;
70
+ for (const item of setA) {
71
+ if (setB.has(item)) intersection++;
72
+ }
73
+ const union = setA.size + setB.size - intersection;
74
+ return union === 0 ? 0 : intersection / union;
75
+ }
76
+
77
+ function scoreRelevance(memory, toolName, actionContext, actionSig) {
78
+ const sig = actionSig || buildActionSignature(toolName, actionContext);
46
79
  let score = 0;
47
80
 
48
- const memText = `${memory.title || ''} ${memory.content || ''} ${(memory.tags || []).join(' ')}`.toLowerCase();
49
- const contextLower = (actionContext || '').toLowerCase();
50
- const toolLower = (toolName || '').toLowerCase();
81
+ const memText = ((memory.title || '') + ' ' + (memory.content || '') + ' ' + (memory.tags || []).join(' ')).toLowerCase();
51
82
 
52
- // 1. Tool name match (high weight)
53
- if (memory.metadata?.toolsUsed?.some((t) => t.toLowerCase() === toolLower)) score += 0.4;
54
- if (memText.includes(toolLower)) score += 0.2;
83
+ if (memory.metadata?.toolsUsed?.some((t) => t.toLowerCase() === sig.toolLower)) score += 0.4;
84
+ if (memText.includes(sig.toolLower)) score += 0.2;
55
85
 
56
- // 2. File path overlap
57
- const contextPaths = extractPaths(actionContext);
58
86
  const memPaths = memory.metadata?.filesInvolved || extractPaths(memText);
59
- const pathOverlap = contextPaths.filter((p) =>
87
+ const pathOverlap = sig.paths.filter((p) =>
60
88
  memPaths.some((mp) => mp.includes(p) || p.includes(mp)),
61
89
  );
62
90
  if (pathOverlap.length > 0) score += 0.3;
63
91
 
64
- // 3. Keyword overlap (TF-IDF-lite)
65
- const contextTokens = tokenize(contextLower);
66
92
  const memTokens = tokenize(memText);
67
- const overlap = contextTokens.filter((t) => memTokens.includes(t));
93
+ const overlap = sig.tokens.filter((t) => memTokens.includes(t));
68
94
  score += Math.min(overlap.length * 0.05, 0.3);
69
95
 
70
- // 4. Signal weighting negative lessons are more important to surface
96
+ // Fuzzy n-gram matching (only when there is already signal)
97
+ if (score > 0) {
98
+ const memBigrams = textBigrams(memText);
99
+ const fuzzyScore = bigramJaccard(sig.ngramSet, memBigrams);
100
+ score += fuzzyScore * 0.2;
101
+ }
102
+
71
103
  if (memory.tags?.includes('negative')) score += 0.1;
72
104
 
73
- // 5. Recency decay
74
105
  if (memory.timestamp) {
75
106
  const ageMs = Date.now() - new Date(memory.timestamp).getTime();
76
107
  const ageDays = ageMs / (1000 * 60 * 60 * 24);
77
108
  const decay = Math.max(0, 1 - ageDays / RECENCY_DECAY_DAYS);
78
- score *= 0.5 + 0.5 * decay; // 50% base + 50% recency
109
+ score *= 0.5 + 0.5 * decay;
79
110
  }
80
111
 
81
- // 6. Structured rule bonus — IF/THEN rules are more actionable
82
112
  if (memory.structuredRule) score += 0.15;
83
113
 
84
114
  return score;
@@ -92,4 +122,10 @@ function tokenize(text) {
92
122
  return (text || '').split(/[\s.,;:!?()\[\]{}"'`]+/).filter((t) => t.length > 3);
93
123
  }
94
124
 
95
- module.exports = { retrieveRelevantLessons, scoreRelevance };
125
+ module.exports = {
126
+ retrieveRelevantLessons,
127
+ scoreRelevance,
128
+ buildActionSignature,
129
+ textBigrams,
130
+ bigramJaccard,
131
+ };
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const MODELS = {
5
+ FAST: 'claude-haiku-4-5-20251001',
6
+ SMART: 'claude-sonnet-4-6',
7
+ };
8
+
9
+ const DEFAULT_MODEL = MODELS.FAST;
10
+ const DEFAULT_MAX_TOKENS = 1024;
11
+
12
+ let _client = null;
13
+
14
+ function isAvailable() {
15
+ return Boolean(process.env.ANTHROPIC_API_KEY);
16
+ }
17
+
18
+ function getClient() {
19
+ if (_client) return _client;
20
+ if (!isAvailable()) return null;
21
+ try {
22
+ const Anthropic = require('@anthropic-ai/sdk');
23
+ _client = new Anthropic();
24
+ return _client;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ function stripCodeFences(text) {
31
+ if (!text) return text;
32
+ const fenced = text.match(/^```(?:json)?\s*\n?([\s\S]*?)```\s*$/);
33
+ return fenced ? fenced[1].trim() : text.trim();
34
+ }
35
+
36
+ async function callClaude({ systemPrompt, userPrompt, model, maxTokens } = {}) {
37
+ const client = getClient();
38
+ if (!client) return null;
39
+
40
+ try {
41
+ const response = await client.messages.create({
42
+ model: model || DEFAULT_MODEL,
43
+ max_tokens: maxTokens || DEFAULT_MAX_TOKENS,
44
+ system: systemPrompt || undefined,
45
+ messages: [{ role: 'user', content: userPrompt }],
46
+ });
47
+
48
+ const text = response.content
49
+ .filter((b) => b.type === 'text')
50
+ .map((b) => b.text)
51
+ .join('');
52
+
53
+ return stripCodeFences(text);
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ module.exports = { isAvailable, callClaude, stripCodeFences, MODELS };
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const { resolveFeedbackDir } = require('./feedback-paths');
8
+ const { parseFeedbackFile, classifySignal, analyzeWithLLM, analyze, promoteToGates } = require('./feedback-to-rules');
9
+ const { inferStructuredLessonLLM, inferStructuredLesson, createLesson } = require('./lesson-inference');
10
+ const { isAvailable } = require('./llm-client');
11
+
12
+ const MAX_ENTRIES_PER_RUN = 20;
13
+ const DELAY_BETWEEN_CALLS_MS = 500;
14
+ const MANIFEST_DIR = path.join(os.homedir(), '.thumbgate');
15
+ const MANIFEST_PATH = path.join(MANIFEST_DIR, 'managed-agent-runs.jsonl');
16
+
17
+ function sleep(ms) {
18
+ return new Promise((resolve) => setTimeout(resolve, ms));
19
+ }
20
+
21
+ function getProcessedIds() {
22
+ if (!fs.existsSync(MANIFEST_PATH)) return new Set();
23
+ const ids = new Set();
24
+ for (const line of fs.readFileSync(MANIFEST_PATH, 'utf8').split('\n')) {
25
+ const trimmed = line.trim();
26
+ if (!trimmed) continue;
27
+ try {
28
+ const run = JSON.parse(trimmed);
29
+ if (Array.isArray(run.processedIds)) {
30
+ for (const id of run.processedIds) ids.add(id);
31
+ }
32
+ } catch { /* skip */ }
33
+ }
34
+ return ids;
35
+ }
36
+
37
+ function writeManifest(manifest) {
38
+ fs.mkdirSync(MANIFEST_DIR, { recursive: true });
39
+ fs.appendFileSync(MANIFEST_PATH, JSON.stringify(manifest) + '\n');
40
+ }
41
+
42
+ function getManagedAgentStatus() {
43
+ if (!fs.existsSync(MANIFEST_PATH)) return null;
44
+ const lines = fs.readFileSync(MANIFEST_PATH, 'utf8').split('\n').filter(Boolean);
45
+ if (lines.length === 0) return null;
46
+ try {
47
+ const last = JSON.parse(lines[lines.length - 1]);
48
+ return {
49
+ lastRun: last.runAt,
50
+ entriesProcessed: last.entriesProcessed,
51
+ lessonsCreated: last.lessonsCreated,
52
+ gatesPromoted: last.gatesPromoted,
53
+ model: last.model,
54
+ durationMs: last.durationMs,
55
+ totalRuns: lines.length,
56
+ };
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ async function runManagedAgent({ dryRun = false, limit, model } = {}) {
63
+ const startTime = Date.now();
64
+ const feedbackDir = resolveFeedbackDir();
65
+ const logPath = path.join(feedbackDir, 'feedback-log.jsonl');
66
+ const entries = parseFeedbackFile(logPath);
67
+
68
+ if (entries.length === 0) {
69
+ return { entriesProcessed: 0, lessonsCreated: 0, gatesPromoted: 0, model: 'none', durationMs: 0, message: 'No feedback entries found' };
70
+ }
71
+
72
+ const processedIds = getProcessedIds();
73
+ const pending = entries
74
+ .filter((e) => {
75
+ const id = e.id || e.feedbackId || e.timestamp;
76
+ return id && !processedIds.has(id);
77
+ })
78
+ .slice(0, limit || MAX_ENTRIES_PER_RUN);
79
+
80
+ if (pending.length === 0) {
81
+ return { entriesProcessed: 0, lessonsCreated: 0, gatesPromoted: 0, model: 'none', durationMs: Date.now() - startTime, message: 'All entries already processed' };
82
+ }
83
+
84
+ const useLLM = isAvailable();
85
+ const modelUsed = useLLM ? 'claude-haiku-4-5' : 'heuristic';
86
+ let lessonsCreated = 0;
87
+ const newProcessedIds = [];
88
+
89
+ for (const entry of pending) {
90
+ const id = entry.id || entry.feedbackId || entry.timestamp;
91
+ const signal = classifySignal(entry);
92
+ if (!signal) {
93
+ newProcessedIds.push(id);
94
+ continue;
95
+ }
96
+
97
+ const window = Array.isArray(entry.conversationWindow) ? entry.conversationWindow : [];
98
+ const context = entry.context || '';
99
+
100
+ let structuredLesson = null;
101
+ if (useLLM) {
102
+ structuredLesson = await inferStructuredLessonLLM(window, signal, context);
103
+ if (structuredLesson && !dryRun) {
104
+ await sleep(DELAY_BETWEEN_CALLS_MS);
105
+ }
106
+ }
107
+
108
+ if (!structuredLesson) {
109
+ structuredLesson = inferStructuredLesson(window, signal, context);
110
+ }
111
+
112
+ if (!dryRun && structuredLesson) {
113
+ try {
114
+ createLesson({
115
+ feedbackId: id,
116
+ signal,
117
+ inferredLesson: structuredLesson.action?.description || '',
118
+ triggerMessage: structuredLesson.examples?.[0]?.assistantAction || '',
119
+ priorSummary: '',
120
+ confidence: structuredLesson.confidence || 0.5,
121
+ tags: structuredLesson.tags || entry.tags || [],
122
+ metadata: { ...structuredLesson.metadata, managedAgent: true, format: structuredLesson.format },
123
+ });
124
+ lessonsCreated++;
125
+ } catch { /* lesson creation is best-effort */ }
126
+ } else if (dryRun && structuredLesson) {
127
+ lessonsCreated++;
128
+ }
129
+
130
+ newProcessedIds.push(id);
131
+ }
132
+
133
+ // Rule generation pass
134
+ let gatesPromoted = 0;
135
+ if (useLLM) {
136
+ const llmIssues = await analyzeWithLLM(entries);
137
+ if (llmIssues && llmIssues.length > 0) {
138
+ if (!dryRun) {
139
+ promoteToGates(llmIssues);
140
+ }
141
+ gatesPromoted = llmIssues.filter((i) => i.severity === 'critical').length;
142
+ }
143
+ } else {
144
+ const report = analyze(entries);
145
+ gatesPromoted = report.recurringIssues.filter((i) => i.severity === 'critical').length;
146
+ }
147
+
148
+ const manifest = {
149
+ runAt: new Date().toISOString(),
150
+ entriesProcessed: pending.length,
151
+ lessonsCreated,
152
+ gatesPromoted,
153
+ model: modelUsed,
154
+ dryRun,
155
+ durationMs: Date.now() - startTime,
156
+ processedIds: newProcessedIds,
157
+ };
158
+
159
+ if (!dryRun) {
160
+ writeManifest(manifest);
161
+ }
162
+
163
+ return manifest;
164
+ }
165
+
166
+ if (require.main === module) {
167
+ const args = process.argv.slice(2);
168
+ const dryRun = args.includes('--dry-run');
169
+ const limitFlag = args.find((a) => a.startsWith('--limit'));
170
+ const limit = limitFlag ? Number(args[args.indexOf(limitFlag) + 1]) || MAX_ENTRIES_PER_RUN : undefined;
171
+
172
+ runManagedAgent({ dryRun, limit })
173
+ .then((result) => {
174
+ console.log(JSON.stringify(result, null, 2));
175
+ process.exit(0);
176
+ })
177
+ .catch((err) => {
178
+ console.error('Managed agent error:', err.message);
179
+ process.exit(1);
180
+ });
181
+ }
182
+
183
+ module.exports = { runManagedAgent, getManagedAgentStatus };