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.
Files changed (160) hide show
  1. package/.claude-plugin/README.md +4 -4
  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 +133 -23
  7. package/adapters/README.md +1 -1
  8. package/adapters/chatgpt/openapi.yaml +168 -0
  9. package/adapters/claude/.mcp.json +2 -2
  10. package/adapters/codex/config.toml +2 -2
  11. package/adapters/mcp/server-stdio.js +85 -2
  12. package/adapters/opencode/opencode.json +1 -1
  13. package/bin/cli.js +215 -19
  14. package/bin/postinstall.js +8 -2
  15. package/config/budget.json +18 -0
  16. package/config/gates/code-edit.json +61 -0
  17. package/config/gates/db-write.json +61 -0
  18. package/config/gates/default.json +154 -3
  19. package/config/gates/deploy.json +61 -0
  20. package/config/github-about.json +2 -1
  21. package/config/merge-quality-checks.json +23 -0
  22. package/config/model-tiers.json +11 -0
  23. package/openapi/openapi.yaml +168 -0
  24. package/package.json +47 -13
  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/cursor-marketplace/README.md +2 -2
  34. package/plugins/cursor-marketplace/commands/capture-feedback.md +2 -2
  35. package/plugins/cursor-marketplace/rules/feedback-capture.mdc +3 -3
  36. package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +3 -2
  37. package/plugins/opencode-profile/INSTALL.md +1 -1
  38. package/public/blog.html +73 -0
  39. package/public/compare/mem0.html +189 -0
  40. package/public/compare/speclock.html +180 -0
  41. package/public/compare.html +12 -4
  42. package/public/guide.html +5 -5
  43. package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
  44. package/public/guides/codex-cli-guardrails.html +158 -0
  45. package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
  46. package/public/guides/pre-action-gates.html +162 -0
  47. package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
  48. package/public/index.html +169 -70
  49. package/public/learn/ai-agent-persistent-memory.html +1 -0
  50. package/public/lessons.html +334 -17
  51. package/public/llm-context.md +140 -0
  52. package/public/pro.html +24 -22
  53. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  54. package/scripts/access-anomaly-detector.js +1 -1
  55. package/scripts/adk-consolidator.js +1 -5
  56. package/scripts/agent-security-hardening.js +4 -6
  57. package/scripts/agentic-data-pipeline.js +1 -3
  58. package/scripts/async-job-runner.js +1 -5
  59. package/scripts/audit-trail.js +7 -5
  60. package/scripts/background-agent-governance.js +2 -10
  61. package/scripts/billing.js +2 -16
  62. package/scripts/budget-enforcer.js +173 -0
  63. package/scripts/build-codex-plugin.js +152 -0
  64. package/scripts/capture-railway-diagnostics.sh +97 -0
  65. package/scripts/check-congruence.js +133 -15
  66. package/scripts/claude-feedback-sync.js +320 -0
  67. package/scripts/cli-telemetry.js +4 -1
  68. package/scripts/commercial-offer.js +5 -7
  69. package/scripts/content-engine/linkedin-content-generator.js +154 -0
  70. package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
  71. package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
  72. package/scripts/content-engine/reddit-thread-finder.js +154 -0
  73. package/scripts/context-engine.js +21 -6
  74. package/scripts/contextfs.js +33 -44
  75. package/scripts/dashboard.js +104 -0
  76. package/scripts/decision-journal.js +341 -0
  77. package/scripts/delegation-runtime.js +1 -5
  78. package/scripts/distribution-surfaces.js +26 -0
  79. package/scripts/document-intake.js +927 -0
  80. package/scripts/ephemeral-agent-store.js +1 -8
  81. package/scripts/evolution-state.js +1 -5
  82. package/scripts/experiment-tracker.js +1 -5
  83. package/scripts/export-databricks-bundle.js +1 -5
  84. package/scripts/export-hf-dataset.js +1 -5
  85. package/scripts/export-training.js +1 -5
  86. package/scripts/feedback-attribution.js +1 -16
  87. package/scripts/feedback-history-distiller.js +1 -16
  88. package/scripts/feedback-loop.js +17 -5
  89. package/scripts/feedback-root-consolidator.js +2 -21
  90. package/scripts/feedback-session.js +49 -0
  91. package/scripts/feedback-to-rules.js +188 -28
  92. package/scripts/filesystem-search.js +1 -9
  93. package/scripts/fs-utils.js +104 -0
  94. package/scripts/gates-engine.js +149 -4
  95. package/scripts/github-about.js +32 -8
  96. package/scripts/gtm-revenue-loop.js +1 -5
  97. package/scripts/harness-selector.js +148 -0
  98. package/scripts/hosted-job-launcher.js +1 -5
  99. package/scripts/hybrid-feedback-context.js +7 -33
  100. package/scripts/intervention-policy.js +753 -0
  101. package/scripts/lesson-db.js +3 -18
  102. package/scripts/lesson-inference.js +194 -16
  103. package/scripts/lesson-retrieval.js +60 -24
  104. package/scripts/llm-client.js +59 -0
  105. package/scripts/local-model-profile.js +18 -2
  106. package/scripts/managed-lesson-agent.js +183 -0
  107. package/scripts/marketing-experiment.js +8 -22
  108. package/scripts/meta-agent-loop.js +624 -0
  109. package/scripts/metered-billing.js +1 -1
  110. package/scripts/model-tier-router.js +10 -1
  111. package/scripts/money-watcher.js +1 -4
  112. package/scripts/obsidian-export.js +1 -5
  113. package/scripts/operational-integrity.js +369 -34
  114. package/scripts/org-dashboard.js +6 -1
  115. package/scripts/per-step-scoring.js +2 -4
  116. package/scripts/pr-manager.js +201 -19
  117. package/scripts/pro-features.js +3 -2
  118. package/scripts/prompt-dlp.js +3 -3
  119. package/scripts/prove-adapters.js +2 -5
  120. package/scripts/prove-attribution.js +1 -5
  121. package/scripts/prove-automation.js +3 -5
  122. package/scripts/prove-cloudflare-sandbox.js +1 -3
  123. package/scripts/prove-data-pipeline.js +1 -3
  124. package/scripts/prove-intelligence.js +1 -3
  125. package/scripts/prove-lancedb.js +1 -5
  126. package/scripts/prove-local-intelligence.js +1 -3
  127. package/scripts/prove-packaged-runtime.js +326 -0
  128. package/scripts/prove-predictive-insights.js +1 -3
  129. package/scripts/prove-runtime.js +13 -0
  130. package/scripts/prove-training-export.js +1 -3
  131. package/scripts/prove-workflow-contract.js +1 -5
  132. package/scripts/rate-limiter.js +6 -4
  133. package/scripts/reddit-dm-outreach.js +14 -4
  134. package/scripts/schedule-manager.js +3 -5
  135. package/scripts/security-scanner.js +448 -0
  136. package/scripts/self-distill-agent.js +579 -0
  137. package/scripts/semantic-dedup.js +115 -0
  138. package/scripts/skill-exporter.js +1 -3
  139. package/scripts/skill-generator.js +1 -5
  140. package/scripts/social-analytics/engagement-audit.js +1 -18
  141. package/scripts/social-analytics/pollers/linkedin.js +26 -16
  142. package/scripts/social-analytics/publishers/linkedin.js +1 -1
  143. package/scripts/social-analytics/publishers/zernio.js +51 -0
  144. package/scripts/social-pipeline.js +1 -3
  145. package/scripts/social-post-hourly.js +47 -4
  146. package/scripts/statusline-links.js +6 -5
  147. package/scripts/statusline-local-stats.js +2 -0
  148. package/scripts/statusline.sh +38 -7
  149. package/scripts/sync-branch-protection.js +340 -0
  150. package/scripts/tessl-export.js +1 -3
  151. package/scripts/thumbgate-search.js +32 -1
  152. package/scripts/tool-kpi-tracker.js +1 -1
  153. package/scripts/tool-registry.js +108 -4
  154. package/scripts/vector-store.js +1 -5
  155. package/scripts/weekly-auto-post.js +1 -1
  156. package/scripts/workflow-sentinel.js +205 -4
  157. package/skills/thumbgate/SKILL.md +2 -2
  158. package/src/api/server.js +273 -4
  159. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  160. /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/.