thumbgate 1.2.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.
- package/.claude-plugin/README.md +4 -4
- 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 +133 -23
- package/adapters/README.md +1 -1
- 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 +85 -2
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +215 -19
- 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/config/model-tiers.json +11 -0
- package/openapi/openapi.yaml +168 -0
- package/package.json +47 -13
- 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/cursor-marketplace/README.md +2 -2
- package/plugins/cursor-marketplace/commands/capture-feedback.md +2 -2
- package/plugins/cursor-marketplace/rules/feedback-capture.mdc +3 -3
- package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +3 -2
- 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 +12 -4
- package/public/guide.html +5 -5
- 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 +169 -70
- package/public/learn/ai-agent-persistent-memory.html +1 -0
- package/public/lessons.html +334 -17
- package/public/llm-context.md +140 -0
- package/public/pro.html +24 -22
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- 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 +7 -5
- package/scripts/background-agent-governance.js +2 -10
- package/scripts/billing.js +2 -16
- package/scripts/budget-enforcer.js +173 -0
- package/scripts/build-codex-plugin.js +152 -0
- package/scripts/capture-railway-diagnostics.sh +97 -0
- package/scripts/check-congruence.js +133 -15
- package/scripts/claude-feedback-sync.js +320 -0
- package/scripts/cli-telemetry.js +4 -1
- 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 +33 -44
- package/scripts/dashboard.js +104 -0
- package/scripts/decision-journal.js +341 -0
- package/scripts/delegation-runtime.js +1 -5
- package/scripts/distribution-surfaces.js +26 -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 +17 -5
- package/scripts/feedback-root-consolidator.js +2 -21
- package/scripts/feedback-session.js +49 -0
- package/scripts/feedback-to-rules.js +188 -28
- package/scripts/filesystem-search.js +1 -9
- package/scripts/fs-utils.js +104 -0
- package/scripts/gates-engine.js +149 -4
- 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-job-launcher.js +1 -5
- package/scripts/hybrid-feedback-context.js +7 -33
- package/scripts/intervention-policy.js +753 -0
- 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/local-model-profile.js +18 -2
- 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/model-tier-router.js +10 -1
- package/scripts/money-watcher.js +1 -4
- package/scripts/obsidian-export.js +1 -5
- package/scripts/operational-integrity.js +369 -34
- 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 +2 -5
- package/scripts/prove-attribution.js +1 -5
- package/scripts/prove-automation.js +3 -5
- 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 +326 -0
- package/scripts/prove-predictive-insights.js +1 -3
- package/scripts/prove-runtime.js +13 -0
- package/scripts/prove-training-export.js +1 -3
- package/scripts/prove-workflow-contract.js +1 -5
- package/scripts/rate-limiter.js +6 -4
- package/scripts/reddit-dm-outreach.js +14 -4
- 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-local-stats.js +2 -0
- package/scripts/statusline.sh +38 -7
- 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 +108 -4
- package/scripts/vector-store.js +1 -5
- package/scripts/weekly-auto-post.js +1 -1
- package/scripts/workflow-sentinel.js +205 -4
- package/skills/thumbgate/SKILL.md +2 -2
- package/src/api/server.js +273 -4
- package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
- /package/scripts/social-analytics/db/{social-analytics.db-wal → analytics.sqlite} +0 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Self-Distillation Agent — Automatic Self-Improvement for AI Coding Agents
|
|
6
|
+
*
|
|
7
|
+
* Reads recent agent conversation history, evaluates action outcomes,
|
|
8
|
+
* and auto-generates lessons using the if-then-v1 format — no human
|
|
9
|
+
* thumbs-down required.
|
|
10
|
+
*
|
|
11
|
+
* Heuristic signals (always available):
|
|
12
|
+
* - Tool call errors (Error:, FAIL, not ok, exit code != 0)
|
|
13
|
+
* - Reverted edits (same file edited then edited back, user says "undo"/"revert")
|
|
14
|
+
* - Correction patterns (user: "no", "wrong", "that's not", "don't", "stop", "undo")
|
|
15
|
+
* - Test failures ("test failed", "FAIL", "not ok")
|
|
16
|
+
* - Success patterns (pass, All tests passed, user: "good", "perfect", "yes")
|
|
17
|
+
*
|
|
18
|
+
* LLM-powered analysis (when ANTHROPIC_API_KEY is set):
|
|
19
|
+
* Sends conversation windows to Claude for structured lesson extraction.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const os = require('os');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
26
|
+
const { createLesson, inferStructuredLesson } = require('./lesson-inference');
|
|
27
|
+
const { buildStableId } = require('./conversation-context');
|
|
28
|
+
const { ensureParentDir, readJsonl } = require('./fs-utils');
|
|
29
|
+
|
|
30
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || os.homedir() || '';
|
|
31
|
+
const SELF_DISTILL_RUNS_PATH = path.join(HOME, '.thumbgate', 'self-distill-runs.jsonl');
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// 1. Conversation Log Discovery
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
function discoverConversationLogs({ limit = 20 } = {}) {
|
|
38
|
+
const logs = [];
|
|
39
|
+
|
|
40
|
+
// Primary: ~/.claude/projects/*/conversation-log.jsonl
|
|
41
|
+
const claudeProjectsDir = path.join(HOME, '.claude', 'projects');
|
|
42
|
+
if (fs.existsSync(claudeProjectsDir)) {
|
|
43
|
+
try {
|
|
44
|
+
const projects = fs.readdirSync(claudeProjectsDir).filter((name) => {
|
|
45
|
+
const stat = fs.statSync(path.join(claudeProjectsDir, name));
|
|
46
|
+
return stat.isDirectory();
|
|
47
|
+
});
|
|
48
|
+
for (const project of projects) {
|
|
49
|
+
const logPath = path.join(claudeProjectsDir, project, 'conversation-log.jsonl');
|
|
50
|
+
if (fs.existsSync(logPath)) {
|
|
51
|
+
logs.push(logPath);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch { /* permission or read errors — skip */ }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Fallback: feedback dir's conversation-window.jsonl
|
|
58
|
+
try {
|
|
59
|
+
const feedbackDir = resolveFeedbackDir();
|
|
60
|
+
const fallback = path.join(feedbackDir, 'conversation-window.jsonl');
|
|
61
|
+
if (fs.existsSync(fallback)) {
|
|
62
|
+
logs.push(fallback);
|
|
63
|
+
}
|
|
64
|
+
} catch { /* resolve errors — skip */ }
|
|
65
|
+
|
|
66
|
+
return logs.slice(0, limit);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// 2. Heuristic Signal Detection
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
const ERROR_PATTERNS = [
|
|
74
|
+
/\bError:/i,
|
|
75
|
+
/\bFAIL\b/,
|
|
76
|
+
/\bnot ok\b/,
|
|
77
|
+
/exit code\s*(?:!=\s*0|[1-9]\d*)/i,
|
|
78
|
+
/\bERROR\b/,
|
|
79
|
+
/\bTypeError\b/,
|
|
80
|
+
/\bReferenceError\b/,
|
|
81
|
+
/\bSyntaxError\b/,
|
|
82
|
+
/\bcommand failed\b/i,
|
|
83
|
+
/\bexited with\s+[1-9]/i,
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const TEST_FAILURE_PATTERNS = [
|
|
87
|
+
/\btest failed\b/i,
|
|
88
|
+
/\bFAIL\b/,
|
|
89
|
+
/\bnot ok\b/,
|
|
90
|
+
/\btests?\s+failed\b/i,
|
|
91
|
+
/\bfailing\s+tests?\b/i,
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const SUCCESS_PATTERNS = [
|
|
95
|
+
/\u2705/,
|
|
96
|
+
/\bpass(?:ed|ing)?\b/i,
|
|
97
|
+
/\bAll tests passed\b/i,
|
|
98
|
+
/\bok\s+\d/,
|
|
99
|
+
/\bsuccess(?:ful(?:ly)?)?\b/i,
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const CORRECTION_PATTERNS = [
|
|
103
|
+
/\bno[,.]?\s/i,
|
|
104
|
+
/\bwrong\b/i,
|
|
105
|
+
/\bthat'?s?\s+not\b/i,
|
|
106
|
+
/\bdon'?t\b/i,
|
|
107
|
+
/\bstop\b/i,
|
|
108
|
+
/\bundo\b/i,
|
|
109
|
+
/\brevert\b/i,
|
|
110
|
+
/\bactually\b/i,
|
|
111
|
+
/\bwait\b/i,
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
const USER_SUCCESS_PATTERNS = [
|
|
115
|
+
/\bgood\b/i,
|
|
116
|
+
/\bperfect\b/i,
|
|
117
|
+
/\byes\b/i,
|
|
118
|
+
/\bthanks?\b/i,
|
|
119
|
+
/\bgreat\b/i,
|
|
120
|
+
/\bworks?\b/i,
|
|
121
|
+
/\blooks? good\b/i,
|
|
122
|
+
/\bnice\b/i,
|
|
123
|
+
/\u2705/,
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
function detectOutcomeSignals(conversationWindow) {
|
|
127
|
+
const window = Array.isArray(conversationWindow) ? conversationWindow : [];
|
|
128
|
+
|
|
129
|
+
const signals = {
|
|
130
|
+
errors: [],
|
|
131
|
+
testFailures: [],
|
|
132
|
+
successes: [],
|
|
133
|
+
corrections: [],
|
|
134
|
+
revertedEdits: [],
|
|
135
|
+
userSuccessSignals: [],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const editedFiles = [];
|
|
139
|
+
|
|
140
|
+
for (const msg of window) {
|
|
141
|
+
if (!msg || typeof msg !== 'object') continue;
|
|
142
|
+
const role = String(msg.role || '').toLowerCase();
|
|
143
|
+
const content = String(msg.content || '');
|
|
144
|
+
if (!content) continue;
|
|
145
|
+
|
|
146
|
+
// Errors in assistant messages or tool output
|
|
147
|
+
if (role === 'assistant' || role === 'tool') {
|
|
148
|
+
for (const pattern of ERROR_PATTERNS) {
|
|
149
|
+
if (pattern.test(content)) {
|
|
150
|
+
const match = content.match(pattern);
|
|
151
|
+
const lineIdx = content.indexOf(match[0]);
|
|
152
|
+
const lineStart = content.lastIndexOf('\n', lineIdx) + 1;
|
|
153
|
+
const lineEnd = content.indexOf('\n', lineIdx);
|
|
154
|
+
signals.errors.push({
|
|
155
|
+
pattern: pattern.source,
|
|
156
|
+
excerpt: content.slice(lineStart, lineEnd === -1 ? lineStart + 200 : lineEnd).trim().slice(0, 200),
|
|
157
|
+
});
|
|
158
|
+
break; // one error per message
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const pattern of TEST_FAILURE_PATTERNS) {
|
|
163
|
+
if (pattern.test(content)) {
|
|
164
|
+
signals.testFailures.push({
|
|
165
|
+
pattern: pattern.source,
|
|
166
|
+
excerpt: content.slice(0, 200).trim(),
|
|
167
|
+
});
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const pattern of SUCCESS_PATTERNS) {
|
|
173
|
+
if (pattern.test(content)) {
|
|
174
|
+
signals.successes.push({
|
|
175
|
+
pattern: pattern.source,
|
|
176
|
+
excerpt: content.slice(0, 200).trim(),
|
|
177
|
+
});
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Track file edits for revert detection
|
|
183
|
+
const editMatch = content.match(/(?:edited|modified|wrote|created)\s+([^\s,]+\.\w+)/i);
|
|
184
|
+
if (editMatch) {
|
|
185
|
+
editedFiles.push({ file: editMatch[1], msgIndex: window.indexOf(msg), role });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Correction/success patterns in user messages
|
|
190
|
+
if (role === 'user') {
|
|
191
|
+
for (const pattern of CORRECTION_PATTERNS) {
|
|
192
|
+
if (pattern.test(content)) {
|
|
193
|
+
signals.corrections.push({
|
|
194
|
+
pattern: pattern.source,
|
|
195
|
+
excerpt: content.slice(0, 200).trim(),
|
|
196
|
+
});
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const pattern of USER_SUCCESS_PATTERNS) {
|
|
202
|
+
if (pattern.test(content)) {
|
|
203
|
+
signals.userSuccessSignals.push({
|
|
204
|
+
pattern: pattern.source,
|
|
205
|
+
excerpt: content.slice(0, 200).trim(),
|
|
206
|
+
});
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Revert detection: user says "undo"/"revert"
|
|
212
|
+
if (/\b(undo|revert)\b/i.test(content)) {
|
|
213
|
+
signals.revertedEdits.push({
|
|
214
|
+
trigger: 'user_request',
|
|
215
|
+
excerpt: content.slice(0, 200).trim(),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Detect reverted edits: same file edited more than once
|
|
222
|
+
const fileCounts = {};
|
|
223
|
+
for (const edit of editedFiles) {
|
|
224
|
+
fileCounts[edit.file] = (fileCounts[edit.file] || 0) + 1;
|
|
225
|
+
}
|
|
226
|
+
for (const [file, count] of Object.entries(fileCounts)) {
|
|
227
|
+
if (count >= 2) {
|
|
228
|
+
signals.revertedEdits.push({
|
|
229
|
+
trigger: 'repeated_edit',
|
|
230
|
+
file,
|
|
231
|
+
editCount: count,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return signals;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function classifyOutcome(signals) {
|
|
240
|
+
const negativeCount = signals.errors.length + signals.testFailures.length
|
|
241
|
+
+ signals.corrections.length + signals.revertedEdits.length;
|
|
242
|
+
const positiveCount = signals.successes.length + signals.userSuccessSignals.length;
|
|
243
|
+
|
|
244
|
+
if (negativeCount > positiveCount) return 'negative';
|
|
245
|
+
if (positiveCount > 0 && negativeCount === 0) return 'positive';
|
|
246
|
+
if (positiveCount > negativeCount) return 'positive';
|
|
247
|
+
return 'neutral';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
// 3. Heuristic Lesson Generation
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
function generateHeuristicLessons(conversationWindow, signals) {
|
|
255
|
+
const lessons = [];
|
|
256
|
+
const outcome = classifyOutcome(signals);
|
|
257
|
+
|
|
258
|
+
if (outcome === 'neutral') return lessons;
|
|
259
|
+
|
|
260
|
+
// Generate lessons from errors
|
|
261
|
+
for (const error of signals.errors.slice(0, 3)) {
|
|
262
|
+
lessons.push({
|
|
263
|
+
signal: 'negative',
|
|
264
|
+
trigger: { condition: `Tool call produced error: ${error.excerpt.slice(0, 120)}`, type: 'error-report' },
|
|
265
|
+
action: { type: 'avoid', description: `Avoid actions that produce: ${error.excerpt.slice(0, 200)}` },
|
|
266
|
+
confidence: 0.6,
|
|
267
|
+
evidence: error.excerpt,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Generate lessons from test failures
|
|
272
|
+
for (const failure of signals.testFailures.slice(0, 2)) {
|
|
273
|
+
lessons.push({
|
|
274
|
+
signal: 'negative',
|
|
275
|
+
trigger: { condition: `Test failure detected: ${failure.excerpt.slice(0, 120)}`, type: 'error-report' },
|
|
276
|
+
action: { type: 'avoid', description: `Changes caused test failures. Verify tests pass before proceeding.` },
|
|
277
|
+
confidence: 0.7,
|
|
278
|
+
evidence: failure.excerpt,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Generate lessons from corrections
|
|
283
|
+
for (const correction of signals.corrections.slice(0, 2)) {
|
|
284
|
+
lessons.push({
|
|
285
|
+
signal: 'negative',
|
|
286
|
+
trigger: { condition: `User correction: ${correction.excerpt.slice(0, 120)}`, type: 'constraint' },
|
|
287
|
+
action: { type: 'avoid', description: `User indicated the approach was wrong: ${correction.excerpt.slice(0, 200)}` },
|
|
288
|
+
confidence: 0.5,
|
|
289
|
+
evidence: correction.excerpt,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Generate lessons from reverted edits
|
|
294
|
+
for (const revert of signals.revertedEdits.slice(0, 2)) {
|
|
295
|
+
const desc = revert.file
|
|
296
|
+
? `Edit to ${revert.file} was reverted (edited ${revert.editCount} times)`
|
|
297
|
+
: `User requested undo/revert: ${(revert.excerpt || '').slice(0, 120)}`;
|
|
298
|
+
lessons.push({
|
|
299
|
+
signal: 'negative',
|
|
300
|
+
trigger: { condition: desc, type: 'error-report' },
|
|
301
|
+
action: { type: 'avoid', description: `Approach was reverted. Confirm intent before making changes.` },
|
|
302
|
+
confidence: 0.6,
|
|
303
|
+
evidence: revert.excerpt || revert.file || '',
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Generate lessons from successes
|
|
308
|
+
if (outcome === 'positive' && signals.successes.length > 0) {
|
|
309
|
+
const success = signals.successes[0];
|
|
310
|
+
lessons.push({
|
|
311
|
+
signal: 'positive',
|
|
312
|
+
trigger: { condition: `Successful action: ${success.excerpt.slice(0, 120)}`, type: 'general' },
|
|
313
|
+
action: { type: 'do', description: `Repeat this approach: ${success.excerpt.slice(0, 200)}` },
|
|
314
|
+
confidence: 0.5,
|
|
315
|
+
evidence: success.excerpt,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (outcome === 'positive' && signals.userSuccessSignals.length > 0) {
|
|
320
|
+
const userSignal = signals.userSuccessSignals[0];
|
|
321
|
+
lessons.push({
|
|
322
|
+
signal: 'positive',
|
|
323
|
+
trigger: { condition: `User approved action: ${userSignal.excerpt.slice(0, 120)}`, type: 'general' },
|
|
324
|
+
action: { type: 'do', description: `This approach was approved by the user.` },
|
|
325
|
+
confidence: 0.5,
|
|
326
|
+
evidence: userSignal.excerpt,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return lessons;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
// 4. LLM-Powered Analysis
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
const LLM_SYSTEM_PROMPT = `You are a self-improvement agent for AI coding assistants. Analyze the conversation window below and extract lessons the assistant should learn.
|
|
338
|
+
|
|
339
|
+
For each lesson, return:
|
|
340
|
+
- signal: "positive" (something worked well) or "negative" (something failed)
|
|
341
|
+
- trigger: { condition: "...", type: "debugging"|"implementation"|"constraint"|"error-report"|"general" }
|
|
342
|
+
- action: { type: "do" (repeat) or "avoid" (don't repeat), description: "..." }
|
|
343
|
+
- confidence: 0.0 to 1.0
|
|
344
|
+
- evidence: the specific conversation excerpt supporting this lesson
|
|
345
|
+
|
|
346
|
+
Return JSON only, no markdown fences:
|
|
347
|
+
{"lessons": [...]}
|
|
348
|
+
|
|
349
|
+
Focus on actionable, specific lessons. Ignore trivial interactions.`;
|
|
350
|
+
|
|
351
|
+
async function callAnthropicApi(conversationText, model) {
|
|
352
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
353
|
+
if (!apiKey) return null;
|
|
354
|
+
|
|
355
|
+
const body = JSON.stringify({
|
|
356
|
+
model: model || 'claude-sonnet-4-20250514',
|
|
357
|
+
max_tokens: 2048,
|
|
358
|
+
system: LLM_SYSTEM_PROMPT,
|
|
359
|
+
messages: [
|
|
360
|
+
{ role: 'user', content: `Analyze this conversation window and extract lessons:\n\n${conversationText}` },
|
|
361
|
+
],
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const resp = await fetch('https://api.anthropic.com/v1/messages', {
|
|
366
|
+
method: 'POST',
|
|
367
|
+
headers: {
|
|
368
|
+
'Content-Type': 'application/json',
|
|
369
|
+
'x-api-key': apiKey,
|
|
370
|
+
'anthropic-version': '2023-06-01',
|
|
371
|
+
},
|
|
372
|
+
body,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
if (!resp.ok) return null;
|
|
376
|
+
|
|
377
|
+
const data = await resp.json();
|
|
378
|
+
const text = (data.content && data.content[0] && data.content[0].text) || '';
|
|
379
|
+
// Strip markdown fences if present
|
|
380
|
+
const cleaned = text.replace(/^```(?:json)?\s*/m, '').replace(/```\s*$/m, '').trim();
|
|
381
|
+
return JSON.parse(cleaned);
|
|
382
|
+
} catch {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function generateLlmLessons(conversationWindow, model) {
|
|
388
|
+
const text = conversationWindow.map((msg) => {
|
|
389
|
+
const role = String(msg.role || 'unknown');
|
|
390
|
+
const content = String(msg.content || '').slice(0, 500);
|
|
391
|
+
return `[${role}]: ${content}`;
|
|
392
|
+
}).join('\n\n');
|
|
393
|
+
|
|
394
|
+
// Cap to ~4000 chars to stay within token budget
|
|
395
|
+
const truncated = text.slice(0, 4000);
|
|
396
|
+
const result = await callAnthropicApi(truncated, model);
|
|
397
|
+
if (!result || !Array.isArray(result.lessons)) return [];
|
|
398
|
+
|
|
399
|
+
return result.lessons.filter((l) =>
|
|
400
|
+
l && l.signal && l.trigger && l.action && typeof l.confidence === 'number'
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
// 5. Persistence
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
function writeRunManifest(manifest) {
|
|
410
|
+
ensureParentDir(SELF_DISTILL_RUNS_PATH);
|
|
411
|
+
fs.appendFileSync(SELF_DISTILL_RUNS_PATH, JSON.stringify(manifest) + '\n');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function readRunManifests() {
|
|
415
|
+
return readJsonl(SELF_DISTILL_RUNS_PATH);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
// 6. Main Entry Points
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
|
|
422
|
+
async function runSelfDistill({ dryRun = false, limit = 20, model } = {}) {
|
|
423
|
+
const startedAt = new Date().toISOString();
|
|
424
|
+
const logPaths = discoverConversationLogs({ limit });
|
|
425
|
+
const hasApiKey = Boolean(process.env.ANTHROPIC_API_KEY);
|
|
426
|
+
const analysisMode = hasApiKey ? 'llm' : 'heuristic';
|
|
427
|
+
|
|
428
|
+
const allLessons = [];
|
|
429
|
+
let sessionsProcessed = 0;
|
|
430
|
+
let sessionsSkipped = 0;
|
|
431
|
+
|
|
432
|
+
for (const logPath of logPaths) {
|
|
433
|
+
const entries = readJsonl(logPath);
|
|
434
|
+
if (entries.length === 0) {
|
|
435
|
+
sessionsSkipped++;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Treat each log file as one conversation session
|
|
440
|
+
const conversationWindow = entries.slice(-30); // last 30 messages max
|
|
441
|
+
const signals = detectOutcomeSignals(conversationWindow);
|
|
442
|
+
const outcome = classifyOutcome(signals);
|
|
443
|
+
|
|
444
|
+
if (outcome === 'neutral') {
|
|
445
|
+
sessionsSkipped++;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
sessionsProcessed++;
|
|
450
|
+
|
|
451
|
+
let lessons;
|
|
452
|
+
if (hasApiKey) {
|
|
453
|
+
lessons = await generateLlmLessons(conversationWindow, model);
|
|
454
|
+
// Fall back to heuristic if LLM returns nothing
|
|
455
|
+
if (!lessons || lessons.length === 0) {
|
|
456
|
+
lessons = generateHeuristicLessons(conversationWindow, signals);
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
lessons = generateHeuristicLessons(conversationWindow, signals);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
for (const lesson of lessons) {
|
|
463
|
+
if (!dryRun) {
|
|
464
|
+
createLesson({
|
|
465
|
+
feedbackId: null,
|
|
466
|
+
signal: lesson.signal,
|
|
467
|
+
inferredLesson: lesson.action.description,
|
|
468
|
+
triggerMessage: lesson.trigger.condition,
|
|
469
|
+
priorSummary: lesson.evidence || '',
|
|
470
|
+
confidence: Math.round((lesson.confidence || 0.5) * 100),
|
|
471
|
+
tags: ['self-distill', lesson.signal],
|
|
472
|
+
metadata: {
|
|
473
|
+
source: 'self-distill-agent',
|
|
474
|
+
analysisMode,
|
|
475
|
+
triggerType: lesson.trigger.type,
|
|
476
|
+
actionType: lesson.action.type,
|
|
477
|
+
logPath,
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
allLessons.push(lesson);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const manifest = {
|
|
486
|
+
id: buildStableId('distill'),
|
|
487
|
+
startedAt,
|
|
488
|
+
completedAt: new Date().toISOString(),
|
|
489
|
+
dryRun,
|
|
490
|
+
analysisMode,
|
|
491
|
+
sessionsProcessed,
|
|
492
|
+
sessionsSkipped,
|
|
493
|
+
lessonsGenerated: allLessons.length,
|
|
494
|
+
logPaths,
|
|
495
|
+
lessons: allLessons.map((l) => ({
|
|
496
|
+
signal: l.signal,
|
|
497
|
+
trigger: l.trigger,
|
|
498
|
+
action: l.action,
|
|
499
|
+
confidence: l.confidence,
|
|
500
|
+
})),
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
if (!dryRun) {
|
|
504
|
+
writeRunManifest(manifest);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return manifest;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function getSelfDistillStatus() {
|
|
511
|
+
const runs = readRunManifests();
|
|
512
|
+
if (runs.length === 0) return null;
|
|
513
|
+
|
|
514
|
+
const lastRun = runs[runs.length - 1];
|
|
515
|
+
return {
|
|
516
|
+
lastRunId: lastRun.id,
|
|
517
|
+
lastRunAt: lastRun.completedAt,
|
|
518
|
+
totalRuns: runs.length,
|
|
519
|
+
totalLessons: runs.reduce((sum, r) => sum + (r.lessonsGenerated || 0), 0),
|
|
520
|
+
lastAnalysisMode: lastRun.analysisMode,
|
|
521
|
+
lastSessionsProcessed: lastRun.sessionsProcessed,
|
|
522
|
+
lastLessonsGenerated: lastRun.lessonsGenerated,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ---------------------------------------------------------------------------
|
|
527
|
+
// 7. CLI
|
|
528
|
+
// ---------------------------------------------------------------------------
|
|
529
|
+
|
|
530
|
+
async function main() {
|
|
531
|
+
const args = process.argv.slice(2);
|
|
532
|
+
const dryRun = args.includes('--dry-run');
|
|
533
|
+
const limitArg = args.find((a) => a.startsWith('--limit'));
|
|
534
|
+
const limit = limitArg ? Number(limitArg.split('=')[1] || limitArg.split(' ')[1]) || 20 : 20;
|
|
535
|
+
|
|
536
|
+
if (args.includes('--status')) {
|
|
537
|
+
const status = getSelfDistillStatus();
|
|
538
|
+
if (!status) {
|
|
539
|
+
console.log('No self-distill runs found.');
|
|
540
|
+
} else {
|
|
541
|
+
console.log(JSON.stringify(status, null, 2));
|
|
542
|
+
}
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
console.log(`Self-distill agent starting (dryRun=${dryRun}, limit=${limit})...`);
|
|
547
|
+
const manifest = await runSelfDistill({ dryRun, limit });
|
|
548
|
+
console.log(`Sessions processed: ${manifest.sessionsProcessed}`);
|
|
549
|
+
console.log(`Sessions skipped: ${manifest.sessionsSkipped}`);
|
|
550
|
+
console.log(`Lessons generated: ${manifest.lessonsGenerated}`);
|
|
551
|
+
console.log(`Analysis mode: ${manifest.analysisMode}`);
|
|
552
|
+
if (dryRun) {
|
|
553
|
+
console.log('\n[DRY RUN] No lessons persisted.');
|
|
554
|
+
}
|
|
555
|
+
if (manifest.lessons.length > 0) {
|
|
556
|
+
console.log('\nLessons:');
|
|
557
|
+
for (const lesson of manifest.lessons) {
|
|
558
|
+
const icon = lesson.signal === 'positive' ? '+' : '-';
|
|
559
|
+
console.log(` [${icon}] ${lesson.action.type}: ${lesson.action.description.slice(0, 100)}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (require.main === module) {
|
|
565
|
+
main().catch((err) => {
|
|
566
|
+
console.error('Self-distill agent failed:', err.message);
|
|
567
|
+
process.exitCode = 1;
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
module.exports = {
|
|
572
|
+
runSelfDistill,
|
|
573
|
+
getSelfDistillStatus,
|
|
574
|
+
detectOutcomeSignals,
|
|
575
|
+
discoverConversationLogs,
|
|
576
|
+
classifyOutcome,
|
|
577
|
+
generateHeuristicLessons,
|
|
578
|
+
SELF_DISTILL_RUNS_PATH,
|
|
579
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Semantic deduplication for feedback entries.
|
|
6
|
+
*
|
|
7
|
+
* Uses character bigram Jaccard similarity to cluster near-duplicate
|
|
8
|
+
* feedback contexts, then picks the longest entry as the representative.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extracts character bigrams from text after normalization.
|
|
13
|
+
* @param {string} text
|
|
14
|
+
* @returns {Set<string>}
|
|
15
|
+
*/
|
|
16
|
+
function bigrams(text) {
|
|
17
|
+
if (!text) return new Set();
|
|
18
|
+
const normalized = text.toLowerCase().replace(/[^a-z0-9 ]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
19
|
+
const result = new Set();
|
|
20
|
+
for (let i = 0; i < normalized.length - 1; i++) {
|
|
21
|
+
result.add(normalized.slice(i, i + 2));
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Computes Jaccard similarity between two sets.
|
|
28
|
+
* @param {Set<string>} a
|
|
29
|
+
* @param {Set<string>} b
|
|
30
|
+
* @returns {number} 0-1 similarity score
|
|
31
|
+
*/
|
|
32
|
+
function jaccardSimilarity(a, b) {
|
|
33
|
+
if (a.size === 0 && b.size === 0) return 1;
|
|
34
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
35
|
+
let intersection = 0;
|
|
36
|
+
for (const item of a) {
|
|
37
|
+
if (b.has(item)) intersection++;
|
|
38
|
+
}
|
|
39
|
+
const union = a.size + b.size - intersection;
|
|
40
|
+
return union === 0 ? 1 : intersection / union;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Normalizes context strings by stripping volatile data.
|
|
45
|
+
* @param {string} context
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
function normalizeContext(context) {
|
|
49
|
+
if (!context) return '';
|
|
50
|
+
return context
|
|
51
|
+
.replace(/\/Users\/[^\s/]+/g, '')
|
|
52
|
+
.replace(/\/home\/[^\s/]+/g, '')
|
|
53
|
+
.replace(/:\d+/g, '')
|
|
54
|
+
.replace(/\d{4}-\d{2}-\d{2}T[\d:.]+Z?/g, '')
|
|
55
|
+
.replace(/\b[a-f0-9]{8,}\b/g, '')
|
|
56
|
+
.replace(/\s+/g, ' ')
|
|
57
|
+
.trim();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Clusters feedback entries by context similarity.
|
|
62
|
+
* @param {Array<{context: string, tags: string[]}>} entries
|
|
63
|
+
* @param {{ threshold?: number }} options
|
|
64
|
+
* @returns {Array<{representative: object, count: number, mergedTags: string[]}>}
|
|
65
|
+
*/
|
|
66
|
+
function clusterFeedback(entries, options = {}) {
|
|
67
|
+
if (!entries || entries.length === 0) return [];
|
|
68
|
+
const threshold = options.threshold ?? 0.5;
|
|
69
|
+
const clusters = [];
|
|
70
|
+
const assigned = new Set();
|
|
71
|
+
const entryBigrams = entries.map((e) => bigrams(normalizeContext(e.context)));
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < entries.length; i++) {
|
|
74
|
+
if (assigned.has(i)) continue;
|
|
75
|
+
const cluster = [i];
|
|
76
|
+
assigned.add(i);
|
|
77
|
+
|
|
78
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
79
|
+
if (assigned.has(j)) continue;
|
|
80
|
+
const sim = jaccardSimilarity(entryBigrams[i], entryBigrams[j]);
|
|
81
|
+
if (sim >= threshold) {
|
|
82
|
+
cluster.push(j);
|
|
83
|
+
assigned.add(j);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const clusterEntries = cluster.map((idx) => entries[idx]);
|
|
88
|
+
const representative = clusterEntries.reduce((a, b) =>
|
|
89
|
+
(a.context || '').length >= (b.context || '').length ? a : b
|
|
90
|
+
);
|
|
91
|
+
const mergedTags = [...new Set(clusterEntries.flatMap((e) => e.tags || []))];
|
|
92
|
+
|
|
93
|
+
clusters.push({ representative, count: clusterEntries.length, mergedTags });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return clusters;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Deduplicates feedback entries, returning unique entries with cluster metadata.
|
|
101
|
+
* @param {Array<{context: string, tags: string[]}>} entries
|
|
102
|
+
* @param {{ threshold?: number }} options
|
|
103
|
+
* @returns {Array<object>}
|
|
104
|
+
*/
|
|
105
|
+
function deduplicateFeedback(entries, options = {}) {
|
|
106
|
+
if (!entries || entries.length === 0) return [];
|
|
107
|
+
const clusters = clusterFeedback(entries, options);
|
|
108
|
+
return clusters.map((c) => ({
|
|
109
|
+
...c.representative,
|
|
110
|
+
_clusterCount: c.count,
|
|
111
|
+
_mergedTags: c.mergedTags,
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { bigrams, jaccardSimilarity, normalizeContext, clusterFeedback, deduplicateFeedback };
|
|
@@ -15,14 +15,12 @@ const SKILL_SPECS_DIR = path.join(ROOT, 'config', 'skill-specs');
|
|
|
15
15
|
const POLICY_BUNDLES_DIR = path.join(ROOT, 'config', 'policy-bundles');
|
|
16
16
|
const DIST_DIR = path.join(ROOT, 'dist', 'skills');
|
|
17
17
|
const PKG = require(path.join(ROOT, 'package.json'));
|
|
18
|
+
const { ensureDir } = require('./fs-utils');
|
|
18
19
|
|
|
19
20
|
function readJson(filePath) {
|
|
20
21
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
function ensureDir(dirPath) {
|
|
24
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
25
|
-
}
|
|
26
24
|
|
|
27
25
|
/**
|
|
28
26
|
* Load a SkillSpec by name from config/skill-specs/.
|