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
|
@@ -14,16 +14,9 @@
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
17
|
+
const { ensureDir, readJsonl } = require('./fs-utils');
|
|
17
18
|
|
|
18
19
|
function getFeedbackDir() { return resolveFeedbackDir(); }
|
|
19
|
-
function ensureDir(p) { if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); }
|
|
20
|
-
|
|
21
|
-
function readJsonl(fp) {
|
|
22
|
-
if (!fs.existsSync(fp)) return [];
|
|
23
|
-
const raw = fs.readFileSync(fp, 'utf-8').trim();
|
|
24
|
-
if (!raw) return [];
|
|
25
|
-
return raw.split('\n').map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
26
|
-
}
|
|
27
20
|
|
|
28
21
|
// ---------------------------------------------------------------------------
|
|
29
22
|
// 1. Per-Agent Namespace Isolation
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
6
|
+
const { ensureDir } = require('./fs-utils');
|
|
6
7
|
|
|
7
8
|
const DEFAULT_SETTINGS = Object.freeze({
|
|
8
9
|
half_life_days: 7,
|
|
@@ -12,11 +13,6 @@ const DEFAULT_SETTINGS = Object.freeze({
|
|
|
12
13
|
dpo_beta: 0.1,
|
|
13
14
|
});
|
|
14
15
|
|
|
15
|
-
function ensureDir(dirPath) {
|
|
16
|
-
if (!fs.existsSync(dirPath)) {
|
|
17
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
16
|
|
|
21
17
|
function appendJSONL(filePath, record) {
|
|
22
18
|
ensureDir(path.dirname(filePath));
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
20
|
const { getFeedbackPaths, readJSONL } = require('./feedback-loop');
|
|
21
|
+
const { ensureDir } = require('./fs-utils');
|
|
21
22
|
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
// Paths
|
|
@@ -31,11 +32,6 @@ function getExperimentPaths() {
|
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
function ensureDir(dirPath) {
|
|
35
|
-
if (!fs.existsSync(dirPath)) {
|
|
36
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
35
|
|
|
40
36
|
function appendJSONL(filePath, record) {
|
|
41
37
|
ensureDir(path.dirname(filePath));
|
|
@@ -5,6 +5,7 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
7
|
const { getFeedbackPaths } = require('./feedback-loop');
|
|
8
|
+
const { ensureDir } = require('./fs-utils');
|
|
8
9
|
|
|
9
10
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
10
11
|
const DEFAULT_PROOF_DIR = process.env.THUMBGATE_PROOF_DIR
|
|
@@ -20,11 +21,6 @@ function parseArgs(argv) {
|
|
|
20
21
|
return args;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
function ensureDir(dirPath) {
|
|
24
|
-
if (!fs.existsSync(dirPath)) {
|
|
25
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
24
|
|
|
29
25
|
function readJSONL(filePath) {
|
|
30
26
|
if (!fs.existsSync(filePath)) return [];
|
|
@@ -26,6 +26,7 @@ const path = require('path');
|
|
|
26
26
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
27
27
|
const { exportDpoFromMemories } = require('./export-dpo-pairs');
|
|
28
28
|
const { getProvenance } = require('./contextfs');
|
|
29
|
+
const { ensureDir } = require('./fs-utils');
|
|
29
30
|
|
|
30
31
|
// ---------------------------------------------------------------------------
|
|
31
32
|
// Helpers
|
|
@@ -43,11 +44,6 @@ function readJSONL(filePath) {
|
|
|
43
44
|
.filter(Boolean);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
function ensureDir(dirPath) {
|
|
47
|
-
if (!fs.existsSync(dirPath)) {
|
|
48
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
47
|
|
|
52
48
|
function writeJSONL(filePath, rows) {
|
|
53
49
|
const content = rows.map((row) => JSON.stringify(row)).join('\n');
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
20
|
+
const { ensureDir } = require('./fs-utils');
|
|
20
21
|
|
|
21
22
|
const SEQUENCE_WINDOW = 10;
|
|
22
23
|
|
|
@@ -40,11 +41,6 @@ function readJSONL(filePath) {
|
|
|
40
41
|
.filter(Boolean);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
function ensureDir(dirPath) {
|
|
44
|
-
if (!fs.existsSync(dirPath)) {
|
|
45
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
44
|
|
|
49
45
|
// ---------------------------------------------------------------------------
|
|
50
46
|
// XPRT-04: validateMemoryStructure
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
7
|
+
const { readJsonl } = require('./fs-utils');
|
|
7
8
|
|
|
8
9
|
function getAttributionPaths(options = {}) {
|
|
9
10
|
const feedbackDir = resolveFeedbackDir({
|
|
@@ -30,22 +31,6 @@ const STOPWORDS = new Set([
|
|
|
30
31
|
'where', 'which', 'while', 'with', 'without', 'would', 'thumbs', 'down', 'up', 'please', 'avoid',
|
|
31
32
|
]);
|
|
32
33
|
|
|
33
|
-
function readJsonl(filePath, maxLines = 500) {
|
|
34
|
-
if (!filePath || !fs.existsSync(filePath)) return [];
|
|
35
|
-
const lines = fs.readFileSync(filePath, 'utf8').split('\n');
|
|
36
|
-
const out = [];
|
|
37
|
-
for (let i = lines.length - 1; i >= 0 && out.length < maxLines; i -= 1) {
|
|
38
|
-
const line = lines[i].trim();
|
|
39
|
-
if (!line) continue;
|
|
40
|
-
try {
|
|
41
|
-
out.push(JSON.parse(line));
|
|
42
|
-
} catch {
|
|
43
|
-
// ignore malformed jsonl lines
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return out.reverse();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
34
|
function appendJsonl(filePath, obj) {
|
|
50
35
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
51
36
|
fs.appendFileSync(filePath, `${JSON.stringify(obj)}\n`);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { resolveFeedbackDir: resolveSharedFeedbackDir } = require('./feedback-paths');
|
|
6
|
+
const { readJsonlTail } = require('./fs-utils');
|
|
6
7
|
|
|
7
8
|
const DEFAULT_HISTORY_LIMIT = 10;
|
|
8
9
|
|
|
@@ -62,22 +63,6 @@ function appendJsonl(filePath, record) {
|
|
|
62
63
|
fs.appendFileSync(filePath, `${JSON.stringify(record)}\n`);
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
function readJsonlTail(filePath, limit = DEFAULT_HISTORY_LIMIT) {
|
|
66
|
-
if (!filePath || !fs.existsSync(filePath)) return [];
|
|
67
|
-
const lines = fs.readFileSync(filePath, 'utf8').split('\n');
|
|
68
|
-
const records = [];
|
|
69
|
-
for (let index = lines.length - 1; index >= 0 && records.length < limit; index -= 1) {
|
|
70
|
-
const line = lines[index].trim();
|
|
71
|
-
if (!line) continue;
|
|
72
|
-
try {
|
|
73
|
-
records.push(JSON.parse(line));
|
|
74
|
-
} catch {
|
|
75
|
-
// ignore malformed lines
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return records.reverse();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
66
|
function resolveFeedbackDir(feedbackDir) {
|
|
82
67
|
if (feedbackDir) {
|
|
83
68
|
return resolveSharedFeedbackDir({ feedbackDir });
|
package/scripts/feedback-loop.js
CHANGED
|
@@ -36,6 +36,7 @@ const {
|
|
|
36
36
|
aggregateFailureDiagnostics,
|
|
37
37
|
} = require('./failure-diagnostics');
|
|
38
38
|
const { getEffectiveSetting } = require('./evolution-state');
|
|
39
|
+
const { ensureDir } = require('./fs-utils');
|
|
39
40
|
const {
|
|
40
41
|
buildFeedbackPathsFromDir,
|
|
41
42
|
getFeedbackPaths: resolveFeedbackPaths,
|
|
@@ -204,11 +205,6 @@ function getMemoryFirewallModule() {
|
|
|
204
205
|
}
|
|
205
206
|
}
|
|
206
207
|
|
|
207
|
-
function ensureDir(dirPath) {
|
|
208
|
-
if (!fs.existsSync(dirPath)) {
|
|
209
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
208
|
|
|
213
209
|
function appendJSONL(filePath, record) {
|
|
214
210
|
ensureDir(path.dirname(filePath));
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { ensureParentDir, readJsonl } = require('./fs-utils');
|
|
6
7
|
const {
|
|
7
8
|
getThumbgateFeedbackDir,
|
|
8
9
|
listFeedbackArtifactPaths,
|
|
@@ -42,10 +43,6 @@ function getScopedProjectOptions(options = {}) {
|
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
function ensureParentDir(filePath) {
|
|
46
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
46
|
function readJsonFile(filePath, fallbackFactory) {
|
|
50
47
|
if (!filePath || !fs.existsSync(filePath)) return fallbackFactory();
|
|
51
48
|
try {
|
|
@@ -56,22 +53,6 @@ function readJsonFile(filePath, fallbackFactory) {
|
|
|
56
53
|
}
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
function readJsonlFile(filePath) {
|
|
60
|
-
if (!filePath || !fs.existsSync(filePath)) return [];
|
|
61
|
-
const raw = fs.readFileSync(filePath, 'utf8').trim();
|
|
62
|
-
if (!raw) return [];
|
|
63
|
-
return raw
|
|
64
|
-
.split('\n')
|
|
65
|
-
.map((line) => {
|
|
66
|
-
try {
|
|
67
|
-
return JSON.parse(line);
|
|
68
|
-
} catch {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
})
|
|
72
|
-
.filter(Boolean);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
56
|
function stableArtifactTimestamp(record = {}) {
|
|
76
57
|
return String(
|
|
77
58
|
record.timestamp ||
|
|
@@ -176,7 +157,7 @@ function consolidateJsonArtifact(fileName, primaryPath, sourcePaths, write) {
|
|
|
176
157
|
}
|
|
177
158
|
|
|
178
159
|
function consolidateJsonlArtifact(fileName, primaryPath, sourcePaths, write) {
|
|
179
|
-
const merged = dedupeJsonlRows(sourcePaths.flatMap((candidate) =>
|
|
160
|
+
const merged = dedupeJsonlRows(sourcePaths.flatMap((candidate) => readJsonl(candidate)));
|
|
180
161
|
const initializedEmpty = sourcePaths.length === 0;
|
|
181
162
|
const writeResult = writeIfChanged(primaryPath, serializeJsonl(merged), write);
|
|
182
163
|
|
|
@@ -161,6 +161,11 @@ function finalizeSession(sessionId) {
|
|
|
161
161
|
persistSession(result);
|
|
162
162
|
} catch (_err) { /* non-critical */ }
|
|
163
163
|
|
|
164
|
+
// Auto-infer lesson from the enriched session context (LangChain continual learning)
|
|
165
|
+
try {
|
|
166
|
+
autoInferLesson(result);
|
|
167
|
+
} catch (_err) { /* non-critical — lesson inference is best-effort */ }
|
|
168
|
+
|
|
164
169
|
// Clean up from active sessions after a delay (allow reads)
|
|
165
170
|
scheduleTimer(() => activeSessions.delete(sessionId), 5000);
|
|
166
171
|
|
|
@@ -261,6 +266,49 @@ function getActiveSession() {
|
|
|
261
266
|
return null;
|
|
262
267
|
}
|
|
263
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Auto-infer a lesson from a finalized feedback session.
|
|
271
|
+
*
|
|
272
|
+
* Implements the Context-layer continual learning pattern identified by
|
|
273
|
+
* LangChain's three-layer framework (Model / Harness / Context):
|
|
274
|
+
* when a session finalizes, the enriched follow-up context is fed to
|
|
275
|
+
* lesson-inference so the next agent session starts with the new lesson
|
|
276
|
+
* already in recall — no retraining required.
|
|
277
|
+
*/
|
|
278
|
+
function autoInferLesson(finalizedResult) {
|
|
279
|
+
const { inferFromSurroundingMessages, createLesson } = require('./lesson-inference');
|
|
280
|
+
|
|
281
|
+
const priorMessages = (finalizedResult.followUpMessages || []).map((m) => ({
|
|
282
|
+
role: m.role,
|
|
283
|
+
content: m.content,
|
|
284
|
+
}));
|
|
285
|
+
|
|
286
|
+
const inference = inferFromSurroundingMessages({
|
|
287
|
+
priorMessages,
|
|
288
|
+
followingMessages: [],
|
|
289
|
+
signal: finalizedResult.signal === 'up' || finalizedResult.signal === 'positive' ? 'positive' : 'negative',
|
|
290
|
+
feedbackContext: finalizedResult.enrichedContext || '',
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
if (!inference || !inference.inferredLesson) return null;
|
|
294
|
+
|
|
295
|
+
return createLesson({
|
|
296
|
+
feedbackId: finalizedResult.feedbackEventId,
|
|
297
|
+
signal: inference.signal,
|
|
298
|
+
inferredLesson: inference.inferredLesson,
|
|
299
|
+
triggerMessage: inference.triggerMessage,
|
|
300
|
+
priorSummary: inference.priorSummary,
|
|
301
|
+
confidence: inference.confidence,
|
|
302
|
+
tags: ['auto-inferred', 'session-finalize'],
|
|
303
|
+
metadata: {
|
|
304
|
+
sessionId: finalizedResult.sessionId,
|
|
305
|
+
followUpCount: finalizedResult.followUpCount,
|
|
306
|
+
duration: finalizedResult.duration,
|
|
307
|
+
source: 'auto-lesson-inference',
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
264
312
|
/**
|
|
265
313
|
* Persist finalized session to disk
|
|
266
314
|
*/
|
|
@@ -278,6 +326,7 @@ module.exports = {
|
|
|
278
326
|
getSession,
|
|
279
327
|
getActiveSession,
|
|
280
328
|
extractComplaints,
|
|
329
|
+
autoInferLesson,
|
|
281
330
|
SESSION_TIMEOUT_MS,
|
|
282
331
|
MAX_FOLLOWUP_MESSAGES,
|
|
283
332
|
scheduleTimer,
|
|
@@ -4,6 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { getAutoGatesPath } = require('./auto-promote-gates');
|
|
6
6
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
7
|
+
const { deduplicateFeedback } = require('./semantic-dedup');
|
|
7
8
|
|
|
8
9
|
const DEFAULT_LOG = path.join(resolveFeedbackDir(), 'feedback-log.jsonl');
|
|
9
10
|
const NEG = new Set(['negative', 'negative_strong', 'down', 'thumbs_down']);
|
|
@@ -51,19 +52,23 @@ function analyze(entries) {
|
|
|
51
52
|
if (cls === 'negative') {
|
|
52
53
|
const tool = e.tool_name || 'unknown';
|
|
53
54
|
toolBuckets[tool] = (toolBuckets[tool] || 0) + 1;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Semantic dedup: cluster near-duplicate negatives into weighted "feedback tokens"
|
|
59
|
+
const negEntries = entries.filter((e) => classifySignal(e) === 'negative');
|
|
60
|
+
const dedupedNeg = deduplicateFeedback(negEntries);
|
|
61
|
+
for (const d of dedupedNeg) {
|
|
62
|
+
const key = normalize(d.context);
|
|
63
|
+
if (key.length > 10 && !contextCounts[key]) {
|
|
64
|
+
const tags = d._mergedTags || d.tags || [];
|
|
65
|
+
contextCounts[key] = {
|
|
66
|
+
raw: d.context,
|
|
67
|
+
count: d._clusterCount || 1,
|
|
68
|
+
tool: d.tool_name || 'unknown',
|
|
69
|
+
tags,
|
|
70
|
+
hasHighRisk: tags.some(t => HIGH_RISK_TAGS.has(t)),
|
|
71
|
+
};
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
74
|
|
|
@@ -102,35 +107,54 @@ function analyze(entries) {
|
|
|
102
107
|
|
|
103
108
|
function promoteToGates(recurringIssues) {
|
|
104
109
|
const autoGatePath = getAutoGatesPath();
|
|
105
|
-
|
|
106
|
-
|
|
110
|
+
|
|
111
|
+
// Load existing auto-gates to MERGE, not overwrite
|
|
112
|
+
let autoGates = { version: 1, gates: [], promotionLog: [] };
|
|
113
|
+
if (fs.existsSync(autoGatePath)) {
|
|
114
|
+
try {
|
|
115
|
+
autoGates = JSON.parse(fs.readFileSync(autoGatePath, 'utf-8'));
|
|
116
|
+
if (!Array.isArray(autoGates.gates)) autoGates.gates = [];
|
|
117
|
+
if (!Array.isArray(autoGates.promotionLog)) autoGates.promotionLog = [];
|
|
118
|
+
} catch { /* start fresh if corrupt */ }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const existingIds = new Set(autoGates.gates.map(g => g.id));
|
|
122
|
+
let added = 0;
|
|
123
|
+
|
|
107
124
|
for (const issue of recurringIssues) {
|
|
108
125
|
if (issue.severity === 'critical') {
|
|
109
|
-
// Extract key nouns/verbs for pattern matching
|
|
110
126
|
const keywords = issue.pattern
|
|
111
127
|
.toLowerCase()
|
|
112
128
|
.replace(/[^a-z0-9\s]/g, '')
|
|
113
129
|
.split(/\s+/)
|
|
114
130
|
.filter(w => w.length > 4)
|
|
115
131
|
.slice(0, 3);
|
|
116
|
-
|
|
132
|
+
|
|
117
133
|
if (keywords.length >= 2) {
|
|
118
134
|
const pattern = keywords.join('.*');
|
|
135
|
+
const id = `auto-${issue.hasHighRisk ? 'hardened' : 'promoted'}-${Date.now().toString(36)}-${added}`;
|
|
136
|
+
|
|
137
|
+
// Skip if a gate with the same pattern already exists
|
|
138
|
+
const patternExists = autoGates.gates.some(g => g.pattern === pattern);
|
|
139
|
+
if (patternExists || existingIds.has(id)) continue;
|
|
140
|
+
|
|
119
141
|
autoGates.gates.push({
|
|
120
|
-
id
|
|
142
|
+
id,
|
|
121
143
|
pattern,
|
|
122
144
|
action: 'block',
|
|
123
145
|
message: `Automatically blocked due to repeated failures: ${issue.suggestedRule}`,
|
|
124
146
|
severity: 'critical',
|
|
125
|
-
source: 'feedback-
|
|
147
|
+
source: 'feedback-to-rules',
|
|
148
|
+
promotedAt: new Date().toISOString(),
|
|
126
149
|
});
|
|
150
|
+
added++;
|
|
127
151
|
}
|
|
128
152
|
}
|
|
129
153
|
}
|
|
130
154
|
|
|
131
|
-
if (
|
|
155
|
+
if (added > 0) {
|
|
132
156
|
fs.mkdirSync(path.dirname(autoGatePath), { recursive: true });
|
|
133
|
-
fs.writeFileSync(autoGatePath, JSON.stringify(autoGates, null, 2));
|
|
157
|
+
fs.writeFileSync(autoGatePath, JSON.stringify(autoGates, null, 2) + '\n');
|
|
134
158
|
}
|
|
135
159
|
}
|
|
136
160
|
|
|
@@ -138,27 +162,182 @@ function toRules(report) {
|
|
|
138
162
|
const lines = ['# Suggested Rules from Feedback Analysis', `# Generated: ${report.generatedAt}`, ''];
|
|
139
163
|
lines.push(`# Negative rate: ${report.negativeRate} (${report.negativeCount}/${report.totalFeedback})`);
|
|
140
164
|
lines.push('');
|
|
165
|
+
|
|
166
|
+
if (!report.recurringIssues.length) {
|
|
167
|
+
lines.push('- No recurring issues detected.');
|
|
168
|
+
return lines.join('\n');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Group by severity: critical → high → medium
|
|
172
|
+
const ORDER = ['critical', 'high', 'medium'];
|
|
173
|
+
const bySeverity = { critical: [], high: [], medium: [] };
|
|
141
174
|
for (const issue of report.recurringIssues) {
|
|
142
|
-
|
|
175
|
+
const sev = issue.severity || 'medium';
|
|
176
|
+
(bySeverity[sev] || bySeverity.medium).push(issue);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (const sev of ORDER) {
|
|
180
|
+
const issues = bySeverity[sev];
|
|
181
|
+
if (!issues || !issues.length) continue;
|
|
182
|
+
lines.push(`## ${sev.toUpperCase()}`);
|
|
183
|
+
for (const issue of issues) {
|
|
184
|
+
const action = issue.action ? ` [${issue.action.toUpperCase()}]` : '';
|
|
185
|
+
lines.push(`- [${sev.toUpperCase()}]${action} (${issue.count}x) ${issue.suggestedRule}`);
|
|
186
|
+
if (issue.reasoning) lines.push(` > ${issue.reasoning}`);
|
|
187
|
+
}
|
|
188
|
+
lines.push('');
|
|
143
189
|
}
|
|
144
|
-
|
|
190
|
+
|
|
145
191
|
return lines.join('\n');
|
|
146
192
|
}
|
|
147
193
|
|
|
148
|
-
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// LLM-Powered Rule Analysis
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
const LLM_RULES_SYSTEM_PROMPT = `You are a senior security engineer and AI agent safety architect at ThumbGate, responsible for creating prevention rules that block dangerous or unwanted AI agent behaviors before they execute.
|
|
199
|
+
|
|
200
|
+
<role>
|
|
201
|
+
You analyze patterns of developer frustration and AI agent failures to generate precise, actionable prevention rules. Your rules are loaded into a real-time PreToolUse gate that intercepts tool calls before they run. A bad rule that over-blocks degrades agent usefulness; a weak rule that under-blocks causes production incidents.
|
|
202
|
+
</role>
|
|
203
|
+
|
|
204
|
+
<chain_of_thought>
|
|
205
|
+
Before generating each rule, reason through:
|
|
206
|
+
1. What is the root-cause pattern across similar failures?
|
|
207
|
+
2. What is the minimum-specific regex that catches it without over-blocking legitimate use?
|
|
208
|
+
3. Is this action irreversible (→ block) or risky-but-recoverable (→ warn)?
|
|
209
|
+
4. What message explains WHY this is dangerous, not just what is blocked?
|
|
210
|
+
</chain_of_thought>
|
|
211
|
+
|
|
212
|
+
<examples>
|
|
213
|
+
Example 1 — Direct push to main:
|
|
214
|
+
Input: Multiple failures where agent pushed directly to main without a PR
|
|
215
|
+
Output rule:
|
|
216
|
+
{
|
|
217
|
+
"pattern": "push.*(?:main|master)(?!.*--dry-run)",
|
|
218
|
+
"action": "block",
|
|
219
|
+
"message": "Direct push to main is forbidden — create a PR and get CI green first",
|
|
220
|
+
"severity": "critical",
|
|
221
|
+
"reasoning": "Bypasses code review and CI gates; irreversible without force-push"
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
Example 2 — Deleting secrets:
|
|
225
|
+
Input: Agent ran rm -rf on production config files
|
|
226
|
+
Output rule:
|
|
227
|
+
{
|
|
228
|
+
"pattern": "rm\\\\s+-rf?\\\\s+(?:\\\\.env|config|credentials|secrets)",
|
|
229
|
+
"action": "block",
|
|
230
|
+
"message": "Deleting config/secrets files is blocked — use git checkout or restore instead",
|
|
231
|
+
"severity": "critical",
|
|
232
|
+
"reasoning": "Permanent deletion of secrets/config causes immediate production outage"
|
|
233
|
+
}
|
|
234
|
+
</examples>
|
|
235
|
+
|
|
236
|
+
Return ONLY a valid JSON array of rule objects:
|
|
237
|
+
[
|
|
238
|
+
{
|
|
239
|
+
"pattern": "<valid JavaScript regex string to match against tool call input>",
|
|
240
|
+
"action": "block" | "warn",
|
|
241
|
+
"message": "<explain WHY this is dangerous, not just what is blocked>",
|
|
242
|
+
"severity": "critical" | "high" | "medium",
|
|
243
|
+
"reasoning": "<root cause and risk analysis from the chain-of-thought>"
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
Constraints:
|
|
248
|
+
- Pattern must be a valid JavaScript regex (used with new RegExp(pattern, 'i')).
|
|
249
|
+
- Prefer specific patterns: "force.*push.*main" beats "push".
|
|
250
|
+
- Use "block" for destructive/irreversible actions, "warn" for risky-but-recoverable.
|
|
251
|
+
- Deduplicate: one rule can cover multiple related failures.
|
|
252
|
+
- Return at most 10 rules, sorted by severity (critical first).
|
|
253
|
+
- Return ONLY the JSON array — no markdown, no explanation outside the array.`;
|
|
254
|
+
|
|
255
|
+
async function analyzeWithLLM(entries) {
|
|
256
|
+
const { isAvailable, callClaude, MODELS } = require('./llm-client');
|
|
257
|
+
if (!isAvailable()) return null;
|
|
258
|
+
|
|
259
|
+
const negativeEntries = entries
|
|
260
|
+
.filter((e) => classifySignal(e) === 'negative')
|
|
261
|
+
.filter((e) => (e.context || '').length > 20)
|
|
262
|
+
.slice(0, 30);
|
|
263
|
+
|
|
264
|
+
if (negativeEntries.length === 0) return null;
|
|
265
|
+
|
|
266
|
+
const batch = negativeEntries.map((e, i) => {
|
|
267
|
+
const ctx = (e.context || '').slice(0, 200);
|
|
268
|
+
const tool = e.tool_name || 'unknown';
|
|
269
|
+
const tags = (e.tags || []).join(', ');
|
|
270
|
+
const wentWrong = (e.what_went_wrong || e.whatWentWrong || '').slice(0, 150);
|
|
271
|
+
const toChange = (e.what_to_change || e.whatToChange || '').slice(0, 100);
|
|
272
|
+
let entry = `${i + 1}. [tool:${tool}] context: ${ctx}`;
|
|
273
|
+
if (wentWrong) entry += `\n what_went_wrong: ${wentWrong}`;
|
|
274
|
+
if (toChange) entry += `\n what_to_change: ${toChange}`;
|
|
275
|
+
if (tags) entry += `\n tags: ${tags}`;
|
|
276
|
+
return entry;
|
|
277
|
+
}).join('\n\n');
|
|
278
|
+
|
|
279
|
+
const raw = await callClaude({
|
|
280
|
+
systemPrompt: LLM_RULES_SYSTEM_PROMPT,
|
|
281
|
+
userPrompt: `Analyze these ${negativeEntries.length} negative feedback entries and generate prevention rules:\n\n${batch}`,
|
|
282
|
+
model: MODELS.SMART,
|
|
283
|
+
maxTokens: 2048,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
if (!raw) return null;
|
|
287
|
+
|
|
149
288
|
try {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
289
|
+
const parsed = JSON.parse(raw);
|
|
290
|
+
if (!Array.isArray(parsed)) return null;
|
|
291
|
+
|
|
292
|
+
return parsed
|
|
293
|
+
.filter((r) => r.pattern && r.action && r.message && r.severity)
|
|
294
|
+
.slice(0, 10)
|
|
295
|
+
.map((r) => ({
|
|
296
|
+
pattern: r.pattern,
|
|
297
|
+
count: negativeEntries.length,
|
|
298
|
+
severity: ['critical', 'high', 'medium'].includes(r.severity) ? r.severity : 'medium',
|
|
299
|
+
hasHighRisk: r.severity === 'critical',
|
|
300
|
+
suggestedRule: r.message,
|
|
301
|
+
reasoning: r.reasoning || '',
|
|
302
|
+
source: 'llm-analysis',
|
|
303
|
+
}));
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
160
306
|
}
|
|
161
|
-
process.exit(0);
|
|
162
307
|
}
|
|
163
308
|
|
|
164
|
-
|
|
309
|
+
if (require.main === module) {
|
|
310
|
+
(async () => {
|
|
311
|
+
try {
|
|
312
|
+
const logPath = process.argv[2] && !process.argv[2].startsWith('--') ? process.argv[2] : DEFAULT_LOG;
|
|
313
|
+
const entries = parseFeedbackFile(logPath);
|
|
314
|
+
const useLLM = process.argv.includes('--llm');
|
|
315
|
+
|
|
316
|
+
let report;
|
|
317
|
+
if (useLLM) {
|
|
318
|
+
const llmIssues = await analyzeWithLLM(entries);
|
|
319
|
+
if (llmIssues) {
|
|
320
|
+
promoteToGates(llmIssues);
|
|
321
|
+
const heuristicReport = analyze(entries);
|
|
322
|
+
report = { ...heuristicReport, recurringIssues: llmIssues, source: 'llm' };
|
|
323
|
+
} else {
|
|
324
|
+
report = analyze(entries);
|
|
325
|
+
report.source = 'heuristic-fallback';
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
report = analyze(entries);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (process.argv.includes('--rules')) {
|
|
332
|
+
console.log(toRules(report));
|
|
333
|
+
} else {
|
|
334
|
+
console.log(JSON.stringify(report, null, 2));
|
|
335
|
+
}
|
|
336
|
+
} catch (err) {
|
|
337
|
+
console.error('Warning:', err.message);
|
|
338
|
+
}
|
|
339
|
+
process.exit(0);
|
|
340
|
+
})();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
module.exports = { parseFeedbackFile, classifySignal, analyze, analyzeWithLLM, promoteToGates, toRules, normalize };
|
|
@@ -20,6 +20,7 @@ const fs = require('node:fs');
|
|
|
20
20
|
const path = require('node:path');
|
|
21
21
|
const crypto = require('node:crypto');
|
|
22
22
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
23
|
+
const { readJsonl } = require('./fs-utils');
|
|
23
24
|
|
|
24
25
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
25
26
|
const DEFAULT_FEEDBACK_DIR = resolveFeedbackDir();
|
|
@@ -37,15 +38,6 @@ function getContextFsDir() {
|
|
|
37
38
|
return process.env.THUMBGATE_CONTEXTFS_DIR || path.join(getFeedbackDir(), 'contextfs');
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function readJsonl(filePath) {
|
|
41
|
-
if (!fs.existsSync(filePath)) return [];
|
|
42
|
-
const raw = fs.readFileSync(filePath, 'utf-8').trim();
|
|
43
|
-
if (!raw) return [];
|
|
44
|
-
return raw.split('\n').map((line) => {
|
|
45
|
-
try { return JSON.parse(line); } catch { return null; }
|
|
46
|
-
}).filter(Boolean);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
41
|
function listJsonFiles(dirPath) {
|
|
50
42
|
if (!fs.existsSync(dirPath)) return [];
|
|
51
43
|
const results = [];
|