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.
- package/.claude-plugin/README.md +25 -0
- package/.claude-plugin/marketplace.json +32 -13
- package/.claude-plugin/plugin.json +15 -2
- package/.well-known/llms.txt +60 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +242 -126
- package/adapters/README.md +1 -1
- package/adapters/chatgpt/INSTALL.md +59 -4
- package/adapters/chatgpt/openapi.yaml +168 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +84 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +204 -13
- package/bin/postinstall.js +8 -2
- package/config/budget.json +18 -0
- package/config/gates/code-edit.json +61 -0
- package/config/gates/db-write.json +61 -0
- package/config/gates/default.json +154 -3
- package/config/gates/deploy.json +61 -0
- package/config/github-about.json +2 -1
- package/config/merge-quality-checks.json +23 -0
- package/openapi/openapi.yaml +168 -0
- package/package.json +47 -11
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +1 -1
- package/plugins/codex-profile/INSTALL.md +27 -4
- package/plugins/codex-profile/README.md +33 -9
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/blog.html +73 -0
- package/public/compare/mem0.html +189 -0
- package/public/compare/speclock.html +180 -0
- package/public/compare.html +10 -2
- package/public/guide.html +2 -2
- package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
- package/public/guides/codex-cli-guardrails.html +158 -0
- package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
- package/public/guides/pre-action-gates.html +162 -0
- package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
- package/public/index.html +172 -65
- package/public/lessons.html +33 -24
- package/public/llm-context.md +140 -0
- package/public/pro.html +24 -22
- package/scripts/access-anomaly-detector.js +1 -1
- package/scripts/adk-consolidator.js +1 -5
- package/scripts/agent-security-hardening.js +4 -6
- package/scripts/agentic-data-pipeline.js +1 -3
- package/scripts/async-job-runner.js +1 -5
- package/scripts/audit-trail.js +1 -5
- package/scripts/auto-promote-gates.js +5 -3
- package/scripts/background-agent-governance.js +2 -10
- package/scripts/billing-setup.js +109 -0
- package/scripts/billing.js +2 -16
- package/scripts/budget-enforcer.js +173 -0
- package/scripts/build-claude-mcpb.js +71 -5
- package/scripts/build-codex-plugin.js +152 -0
- package/scripts/check-congruence.js +132 -14
- package/scripts/commercial-offer.js +5 -7
- package/scripts/content-engine/linkedin-content-generator.js +154 -0
- package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
- package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
- package/scripts/content-engine/reddit-thread-finder.js +154 -0
- package/scripts/context-engine.js +21 -6
- package/scripts/contextfs.js +1 -21
- package/scripts/dashboard.js +20 -0
- package/scripts/decision-journal.js +341 -0
- package/scripts/delegation-runtime.js +1 -5
- package/scripts/distribution-surfaces.js +54 -0
- package/scripts/document-intake.js +927 -0
- package/scripts/ephemeral-agent-store.js +1 -8
- package/scripts/evolution-state.js +1 -5
- package/scripts/experiment-tracker.js +1 -5
- package/scripts/export-databricks-bundle.js +1 -5
- package/scripts/export-hf-dataset.js +1 -5
- package/scripts/export-training.js +1 -5
- package/scripts/feedback-attribution.js +1 -16
- package/scripts/feedback-history-distiller.js +1 -16
- package/scripts/feedback-loop.js +1 -5
- package/scripts/feedback-root-consolidator.js +2 -21
- package/scripts/feedback-session.js +49 -0
- package/scripts/feedback-to-rules.js +215 -36
- package/scripts/filesystem-search.js +1 -9
- package/scripts/fs-utils.js +104 -0
- package/scripts/gates-engine.js +200 -11
- package/scripts/github-about.js +32 -8
- package/scripts/gtm-revenue-loop.js +1 -5
- package/scripts/harness-selector.js +148 -0
- package/scripts/hosted-config.js +2 -0
- package/scripts/hosted-job-launcher.js +1 -5
- package/scripts/hybrid-feedback-context.js +33 -49
- package/scripts/intervention-policy.js +58 -1
- package/scripts/lesson-db.js +3 -18
- package/scripts/lesson-inference.js +194 -16
- package/scripts/lesson-retrieval.js +60 -24
- package/scripts/llm-client.js +59 -0
- package/scripts/managed-lesson-agent.js +183 -0
- package/scripts/marketing-experiment.js +8 -22
- package/scripts/meta-agent-loop.js +624 -0
- package/scripts/metered-billing.js +1 -1
- package/scripts/money-watcher.js +1 -4
- package/scripts/obsidian-export.js +1 -5
- package/scripts/operational-integrity.js +15 -3
- package/scripts/operational-summary.js +41 -5
- package/scripts/org-dashboard.js +6 -1
- package/scripts/per-step-scoring.js +2 -4
- package/scripts/pr-manager.js +201 -19
- package/scripts/pro-features.js +3 -2
- package/scripts/prompt-dlp.js +3 -3
- package/scripts/prove-adapters.js +1 -5
- package/scripts/prove-attribution.js +1 -5
- package/scripts/prove-automation.js +1 -3
- package/scripts/prove-cloudflare-sandbox.js +1 -3
- package/scripts/prove-data-pipeline.js +1 -3
- package/scripts/prove-intelligence.js +1 -3
- package/scripts/prove-lancedb.js +1 -5
- package/scripts/prove-local-intelligence.js +1 -3
- package/scripts/prove-packaged-runtime.js +75 -9
- package/scripts/prove-predictive-insights.js +1 -3
- package/scripts/prove-training-export.js +1 -3
- package/scripts/prove-workflow-contract.js +1 -5
- package/scripts/ralph-loop.js +376 -0
- package/scripts/ralph-mode-ci.js +331 -0
- package/scripts/rate-limiter.js +3 -1
- package/scripts/reddit-dm-outreach.js +14 -4
- package/scripts/rotate-stripe-webhook-secret.js +314 -0
- package/scripts/schedule-manager.js +3 -5
- package/scripts/security-scanner.js +448 -0
- package/scripts/self-distill-agent.js +579 -0
- package/scripts/semantic-dedup.js +115 -0
- package/scripts/skill-exporter.js +1 -3
- package/scripts/skill-generator.js +1 -5
- package/scripts/social-analytics/engagement-audit.js +1 -18
- package/scripts/social-analytics/pollers/linkedin.js +26 -16
- package/scripts/social-analytics/publishers/linkedin.js +1 -1
- package/scripts/social-analytics/publishers/zernio.js +51 -0
- package/scripts/social-pipeline.js +1 -3
- package/scripts/social-post-hourly.js +47 -4
- package/scripts/statusline-links.js +6 -5
- package/scripts/statusline.sh +29 -153
- package/scripts/sync-branch-protection.js +340 -0
- package/scripts/tessl-export.js +1 -3
- package/scripts/thumbgate-search.js +32 -1
- package/scripts/tool-kpi-tracker.js +1 -1
- package/scripts/tool-registry.js +106 -2
- package/scripts/vector-store.js +1 -5
- package/scripts/weekly-auto-post.js +1 -1
- package/scripts/workflow-sentinel.js +91 -0
- package/skills/thumbgate/SKILL.md +1 -1
- package/src/api/server.js +296 -7
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
- /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
|
-
|
|
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 =
|
|
302
|
+
const recent = selectStatusbarLesson();
|
|
209
303
|
if (!recent) return { hasLesson: false, text: null, link: null };
|
|
210
304
|
|
|
211
|
-
const
|
|
212
|
-
const truncated =
|
|
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:
|
|
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
|
-
|
|
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
|
-
*
|
|
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;
|
|
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
|
-
|
|
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
|
|
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 =
|
|
49
|
-
const contextLower = (actionContext || '').toLowerCase();
|
|
50
|
-
const toolLower = (toolName || '').toLowerCase();
|
|
81
|
+
const memText = ((memory.title || '') + ' ' + (memory.content || '') + ' ' + (memory.tags || []).join(' ')).toLowerCase();
|
|
51
82
|
|
|
52
|
-
|
|
53
|
-
if (
|
|
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 =
|
|
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 =
|
|
93
|
+
const overlap = sig.tokens.filter((t) => memTokens.includes(t));
|
|
68
94
|
score += Math.min(overlap.length * 0.05, 0.3);
|
|
69
95
|
|
|
70
|
-
//
|
|
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;
|
|
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 = {
|
|
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 };
|