thumbgate 1.27.6 → 1.27.7
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/commands/thumbgate-blocked.md +27 -0
- package/.claude/commands/thumbgate-doctor.md +30 -0
- package/.claude/commands/thumbgate-guard.md +36 -0
- package/.claude/commands/thumbgate-protect.md +30 -0
- package/.claude/commands/thumbgate-rules.md +30 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/llms.txt +6 -2
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +49 -5
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/letta/README.md +41 -0
- package/adapters/letta/thumbgate-letta-adapter.js +133 -0
- package/adapters/mcp/server-stdio.js +16 -1
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
- package/bench/observability-eval-suite.json +26 -0
- package/bin/cli.js +180 -2
- package/bin/postinstall.js +1 -1
- package/config/gate-templates.json +84 -0
- package/config/gates/claim-verification.json +6 -0
- package/config/gates/default.json +20 -0
- package/config/github-about.json +1 -1
- package/config/model-candidates.json +50 -0
- package/package.json +65 -25
- package/public/agent-manager.html +41 -1
- package/public/agents-cost-savings.html +1 -1
- package/public/ai-malpractice-prevention.html +2 -1
- package/public/assets/brand/github-social-preview.png +0 -0
- package/public/assets/brand/thumbgate-icon-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
- package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
- package/public/assets/brand/thumbgate-mark-inline.svg +15 -0
- package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
- package/public/assets/brand/thumbgate-mark-team.svg +26 -0
- package/public/assets/brand/thumbgate-mark.svg +15 -0
- package/public/assets/brand/thumbgate-wordmark.svg +20 -0
- package/public/assets/claude-thumbgate-statusbar.svg +8 -0
- package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
- package/public/assets/legal-intake-control-flow.svg +66 -0
- package/public/blog.html +1 -1
- package/public/brand/thumbgate-mark.svg +15 -0
- package/public/brand/thumbgate-og.svg +16 -0
- package/public/codex-enterprise.html +1 -1
- package/public/codex-plugin.html +1 -1
- package/public/compare.html +23 -3
- package/public/dashboard.html +312 -30
- package/public/federal.html +1 -1
- package/public/guide.html +5 -4
- package/public/index.html +167 -49
- package/public/js/buyer-intent.js +672 -0
- package/public/learn.html +74 -7
- package/public/lessons.html +2 -1
- package/public/numbers.html +3 -3
- package/public/pricing.html +63 -15
- package/public/pro.html +7 -7
- package/scripts/activation-quickstart.js +187 -0
- package/scripts/agent-memory-lifecycle.js +211 -0
- package/scripts/async-eval-observability.js +236 -0
- package/scripts/auto-promote-gates.js +75 -4
- package/scripts/build-metadata.js +24 -3
- package/scripts/cli-schema.js +22 -0
- package/scripts/dashboard-chat.js +2 -1
- package/scripts/dashboard.js +8 -0
- package/scripts/export-databricks-bundle.js +5 -1
- package/scripts/export-dpo-pairs.js +7 -2
- package/scripts/feedback-aggregate.js +281 -0
- package/scripts/feedback-loop.js +34 -0
- package/scripts/filesystem-search.js +35 -10
- package/scripts/gates-engine.js +198 -6
- package/scripts/gemini-embedding-policy.js +2 -1
- package/scripts/hook-stop-anti-claim.js +227 -0
- package/scripts/hook-thumbgate-cache-updater.js +18 -2
- package/scripts/lesson-inference.js +8 -3
- package/scripts/lesson-search.js +17 -1
- package/scripts/operational-integrity.js +39 -5
- package/scripts/plausible-domain-config.js +4 -2
- package/scripts/rate-limiter.js +12 -6
- package/scripts/secret-redaction.js +166 -0
- package/scripts/security-scanner.js +100 -0
- package/scripts/self-distill-agent.js +3 -1
- package/scripts/self-harness-optimizer.js +141 -0
- package/scripts/seo-gsd.js +635 -0
- package/scripts/statusline-cache-path.js +17 -2
- package/scripts/statusline-cache-read.js +57 -0
- package/scripts/statusline-local-stats.js +9 -1
- package/scripts/statusline-meta.js +5 -2
- package/scripts/statusline.sh +13 -1
- package/scripts/sync-telemetry-from-prod.js +374 -0
- package/scripts/telemetry-analytics.js +9 -0
- package/scripts/thumbgate-search.js +85 -19
- package/scripts/tool-contract-validator.js +76 -0
- package/scripts/vector-store.js +44 -0
- package/scripts/workspace-evolver.js +62 -2
- package/src/api/server.js +715 -86
package/scripts/dashboard.js
CHANGED
|
@@ -49,6 +49,10 @@ const {
|
|
|
49
49
|
readDecisionLog,
|
|
50
50
|
} = require('./decision-journal');
|
|
51
51
|
const { analyzeFeedback } = require('./feedback-loop');
|
|
52
|
+
const {
|
|
53
|
+
collectAggregateLogEntries,
|
|
54
|
+
shouldAggregateFeedback,
|
|
55
|
+
} = require('./feedback-aggregate');
|
|
52
56
|
|
|
53
57
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
54
58
|
const DEFAULT_GATES_PATH = path.join(PROJECT_ROOT, 'config', 'gates', 'default.json');
|
|
@@ -1545,6 +1549,10 @@ function resolveTeamWindowHours(analyticsWindow) {
|
|
|
1545
1549
|
// ---------------------------------------------------------------------------
|
|
1546
1550
|
|
|
1547
1551
|
function collectAllFeedbackEntries(feedbackDir) {
|
|
1552
|
+
if (shouldAggregateFeedback()) {
|
|
1553
|
+
return collectAggregateLogEntries('feedback-log.jsonl', { feedbackDir }).entries;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1548
1556
|
const entries = [];
|
|
1549
1557
|
const seen = new Set();
|
|
1550
1558
|
|
|
@@ -6,6 +6,7 @@ const path = require('path');
|
|
|
6
6
|
|
|
7
7
|
const { getFeedbackPaths } = require('./feedback-loop');
|
|
8
8
|
const { ensureDir } = require('./fs-utils');
|
|
9
|
+
const { redactSecretsDeep } = require('./secret-redaction');
|
|
9
10
|
|
|
10
11
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
11
12
|
const DEFAULT_PROOF_DIR = process.env.THUMBGATE_PROOF_DIR
|
|
@@ -47,7 +48,10 @@ function readJSON(filePath) {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
function writeJSONL(filePath, rows) {
|
|
50
|
-
|
|
51
|
+
// Redact secrets from every bundle row — this is the single choke point for all bundle tables
|
|
52
|
+
// (feedback_events, memory_records, sequences, attributions, proof_reports). A shared/published
|
|
53
|
+
// dataset must never ship a captured credential. See scripts/secret-redaction.js.
|
|
54
|
+
const content = rows.map((row) => JSON.stringify(redactSecretsDeep(row))).join('\n');
|
|
51
55
|
fs.writeFileSync(filePath, content ? `${content}\n` : '');
|
|
52
56
|
}
|
|
53
57
|
|
|
@@ -9,6 +9,7 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const { traceForDpoPair, aggregateTraces } = require('./code-reasoning');
|
|
11
11
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
12
|
+
const { redactSecretsDeep } = require('./secret-redaction');
|
|
12
13
|
|
|
13
14
|
const DEFAULT_LOCAL_MEMORY_LOG = path.join(resolveFeedbackDir(), 'memory-log.jsonl');
|
|
14
15
|
|
|
@@ -201,14 +202,18 @@ function exportDpoFromMemories(memories) {
|
|
|
201
202
|
},
|
|
202
203
|
}));
|
|
203
204
|
|
|
205
|
+
// Redact secrets before the pairs leave this module — they are derived from memory content and
|
|
206
|
+
// are shipped to disk here AND consumed by export-hf-dataset.js. See scripts/secret-redaction.js.
|
|
207
|
+
const redactedPairs = pairsWithTraces.map((pair) => redactSecretsDeep(pair));
|
|
208
|
+
|
|
204
209
|
return {
|
|
205
|
-
pairs:
|
|
210
|
+
pairs: redactedPairs,
|
|
206
211
|
unpairedErrors: result.unpairedErrors,
|
|
207
212
|
unpairedLearnings: result.unpairedLearnings,
|
|
208
213
|
errors,
|
|
209
214
|
learnings,
|
|
210
215
|
reasoning,
|
|
211
|
-
jsonl: toJSONL(
|
|
216
|
+
jsonl: toJSONL(redactedPairs),
|
|
212
217
|
};
|
|
213
218
|
}
|
|
214
219
|
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const crypto = require('node:crypto');
|
|
8
|
+
const {
|
|
9
|
+
getFallbackFeedbackDir,
|
|
10
|
+
getGlobalFeedbackDir,
|
|
11
|
+
getHomeDir,
|
|
12
|
+
getLegacyFeedbackDir,
|
|
13
|
+
getThumbgateFeedbackDir,
|
|
14
|
+
resolveFeedbackDir,
|
|
15
|
+
resolveProjectDir,
|
|
16
|
+
} = require('./feedback-paths');
|
|
17
|
+
const { readJsonl } = require('./fs-utils');
|
|
18
|
+
|
|
19
|
+
const FEEDBACK_LOG = 'feedback-log.jsonl';
|
|
20
|
+
const MEMORY_LOG = 'memory-log.jsonl';
|
|
21
|
+
const STATUSLINE_CACHE = 'statusline_cache.json';
|
|
22
|
+
|
|
23
|
+
function truthyDisabled(value) {
|
|
24
|
+
return value === '0' || value === 'false' || value === 'local';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function shouldAggregateFeedback(options = {}) {
|
|
28
|
+
const env = options.env || process.env;
|
|
29
|
+
return !truthyDisabled(String(env.THUMBGATE_STATUSLINE_AGGREGATE || env.THUMBGATE_AGGREGATE_FEEDBACK || '1').toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizePath(candidate) {
|
|
33
|
+
if (!candidate) return null;
|
|
34
|
+
try {
|
|
35
|
+
return path.resolve(String(candidate));
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function uniquePaths(values = []) {
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const value of values) {
|
|
45
|
+
const resolved = normalizePath(value);
|
|
46
|
+
if (!resolved || seen.has(resolved)) continue;
|
|
47
|
+
seen.add(resolved);
|
|
48
|
+
out.push(resolved);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function safeExists(candidate) {
|
|
54
|
+
try {
|
|
55
|
+
return Boolean(candidate && fs.existsSync(candidate));
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function immediateChildDirs(parentDir) {
|
|
62
|
+
try {
|
|
63
|
+
return fs.readdirSync(parentDir, { withFileTypes: true })
|
|
64
|
+
.filter((entry) => entry.isDirectory())
|
|
65
|
+
.map((entry) => path.join(parentDir, entry.name));
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ancestorProjectFeedbackDirs(projectDir, options = {}) {
|
|
72
|
+
const home = normalizePath(getHomeDir(options));
|
|
73
|
+
const start = normalizePath(projectDir);
|
|
74
|
+
if (!start) return [];
|
|
75
|
+
|
|
76
|
+
const dirs = [];
|
|
77
|
+
let cursor = start;
|
|
78
|
+
while (cursor && cursor !== path.dirname(cursor)) {
|
|
79
|
+
dirs.push(
|
|
80
|
+
path.join(cursor, '.thumbgate'),
|
|
81
|
+
path.join(cursor, '.thumbgate-compat'),
|
|
82
|
+
path.join(cursor, '.claude', 'memory', 'feedback')
|
|
83
|
+
);
|
|
84
|
+
if (home && cursor === home) break;
|
|
85
|
+
const next = path.dirname(cursor);
|
|
86
|
+
if (next === cursor) break;
|
|
87
|
+
cursor = next;
|
|
88
|
+
}
|
|
89
|
+
return dirs;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function listFeedbackStoreDirs(options = {}) {
|
|
93
|
+
const env = options.env || process.env;
|
|
94
|
+
const projectDir = resolveProjectDir({ cwd: options.cwd, env, projectDir: options.projectDir });
|
|
95
|
+
const home = getHomeDir({ env });
|
|
96
|
+
const homeThumbgate = path.join(home, '.thumbgate');
|
|
97
|
+
const projectsDir = path.join(homeThumbgate, 'projects');
|
|
98
|
+
const explicitRoots = String(env.THUMBGATE_AGGREGATE_ROOTS || '')
|
|
99
|
+
.split(path.delimiter)
|
|
100
|
+
.map((value) => value.trim())
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
const explicitFeedbackDir = options.feedbackDir || env.THUMBGATE_FEEDBACK_DIR;
|
|
103
|
+
const normalizedExplicitFeedbackDir = normalizePath(explicitFeedbackDir);
|
|
104
|
+
|
|
105
|
+
if (
|
|
106
|
+
normalizedExplicitFeedbackDir &&
|
|
107
|
+
normalizedExplicitFeedbackDir.startsWith(normalizePath(os.tmpdir()) + path.sep) &&
|
|
108
|
+
explicitRoots.length === 0
|
|
109
|
+
) {
|
|
110
|
+
return uniquePaths([explicitFeedbackDir])
|
|
111
|
+
.filter((dir) => safeExists(path.join(dir, FEEDBACK_LOG)) || safeExists(path.join(dir, MEMORY_LOG)) || safeExists(path.join(dir, STATUSLINE_CACHE)));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return uniquePaths([
|
|
115
|
+
explicitFeedbackDir,
|
|
116
|
+
resolveFeedbackDir({ projectDir, env }),
|
|
117
|
+
getThumbgateFeedbackDir({ projectDir, env }),
|
|
118
|
+
getFallbackFeedbackDir({ projectDir, env }),
|
|
119
|
+
getLegacyFeedbackDir({ projectDir, env }),
|
|
120
|
+
getGlobalFeedbackDir({ projectDir, env }),
|
|
121
|
+
homeThumbgate,
|
|
122
|
+
...immediateChildDirs(projectsDir),
|
|
123
|
+
...ancestorProjectFeedbackDirs(projectDir, { env }),
|
|
124
|
+
...explicitRoots,
|
|
125
|
+
]).filter((dir) => safeExists(path.join(dir, FEEDBACK_LOG)) || safeExists(path.join(dir, MEMORY_LOG)) || safeExists(path.join(dir, STATUSLINE_CACHE)));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizeSignal(signal) {
|
|
129
|
+
const raw = String(signal || '').toLowerCase();
|
|
130
|
+
if (raw === 'positive' || raw === 'up' || raw === 'thumbs_up') return 'positive';
|
|
131
|
+
if (raw === 'negative' || raw === 'down' || raw === 'thumbs_down') return 'negative';
|
|
132
|
+
return raw || null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function stableEntryKey(entry = {}, source = {}) {
|
|
136
|
+
const id = entry.id || entry.feedbackId || entry.sourceFeedbackId;
|
|
137
|
+
if (id) return `id:${id}`;
|
|
138
|
+
const material = JSON.stringify({
|
|
139
|
+
sourcePath: source.logPath || '',
|
|
140
|
+
sourceIndex: Number.isFinite(source.index) ? source.index : -1,
|
|
141
|
+
entry,
|
|
142
|
+
});
|
|
143
|
+
return `sha:${crypto.createHash('sha256').update(material).digest('hex')}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function sortByTimestamp(entries) {
|
|
147
|
+
return entries.sort((a, b) => {
|
|
148
|
+
const at = a.timestamp ? Date.parse(a.timestamp) : 0;
|
|
149
|
+
const bt = b.timestamp ? Date.parse(b.timestamp) : 0;
|
|
150
|
+
return (Number.isFinite(at) ? at : 0) - (Number.isFinite(bt) ? bt : 0);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function collectAggregateLogEntries(fileName, options = {}) {
|
|
155
|
+
const stores = listFeedbackStoreDirs(options);
|
|
156
|
+
const seen = new Set();
|
|
157
|
+
const entries = [];
|
|
158
|
+
|
|
159
|
+
for (const dir of stores) {
|
|
160
|
+
const logPath = path.join(dir, fileName);
|
|
161
|
+
if (!safeExists(logPath)) continue;
|
|
162
|
+
const rows = readJsonl(logPath, { maxLines: 0 }) || [];
|
|
163
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
164
|
+
const rawEntry = rows[index];
|
|
165
|
+
const entry = { ...rawEntry };
|
|
166
|
+
if (entry.signal || entry.feedback) entry.signal = normalizeSignal(entry.signal || entry.feedback);
|
|
167
|
+
const key = stableEntryKey(entry, { logPath, index });
|
|
168
|
+
if (seen.has(key)) continue;
|
|
169
|
+
seen.add(key);
|
|
170
|
+
entries.push({ ...entry, sourceFeedbackDir: dir });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
entries: sortByTimestamp(entries),
|
|
176
|
+
stores,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function summarizeFeedbackEntries(entries) {
|
|
181
|
+
let totalPositive = 0;
|
|
182
|
+
let totalNegative = 0;
|
|
183
|
+
let rubricSamples = 0;
|
|
184
|
+
|
|
185
|
+
for (const entry of entries) {
|
|
186
|
+
if (entry.signal === 'positive') totalPositive += 1;
|
|
187
|
+
if (entry.signal === 'negative') totalNegative += 1;
|
|
188
|
+
if (entry.rubric && entry.rubric.weightedScore != null) rubricSamples += 1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { totalPositive, totalNegative, rubricSamples };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function createRateWindows(total, totalPositive, approvalRate) {
|
|
195
|
+
return {
|
|
196
|
+
'7d': { total: 0, positive: 0, rate: 0 },
|
|
197
|
+
'30d': { total: 0, positive: 0, rate: 0 },
|
|
198
|
+
lifetime: { total, positive: totalPositive, rate: approvalRate },
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function applyEntryToRateWindow(window, entry) {
|
|
203
|
+
window.total += 1;
|
|
204
|
+
if (entry.signal === 'positive') window.positive += 1;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function updateRateWindows(windows, entries, now = Date.now()) {
|
|
208
|
+
const sevenDays = 7 * 24 * 60 * 60 * 1000;
|
|
209
|
+
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
|
|
210
|
+
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
const ts = entry.timestamp ? Date.parse(entry.timestamp) : Number.NaN;
|
|
213
|
+
if (!Number.isFinite(ts)) continue;
|
|
214
|
+
const age = now - ts;
|
|
215
|
+
if (age <= sevenDays) applyEntryToRateWindow(windows['7d'], entry);
|
|
216
|
+
if (age <= thirtyDays) applyEntryToRateWindow(windows['30d'], entry);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function finalizeRateWindows(windows) {
|
|
221
|
+
for (const key of ['7d', '30d']) {
|
|
222
|
+
const window = windows[key];
|
|
223
|
+
window.rate = 0;
|
|
224
|
+
if (window.total > 0) {
|
|
225
|
+
window.rate = Math.round((window.positive / window.total) * 1000) / 1000;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function trendFromRateWindows(windows) {
|
|
231
|
+
const hasTrendData = windows['7d'].total > 0 && windows['30d'].total > 0;
|
|
232
|
+
if (hasTrendData) {
|
|
233
|
+
if (windows['7d'].rate > windows['30d'].rate + 0.05) return 'improving';
|
|
234
|
+
if (windows['7d'].rate < windows['30d'].rate - 0.05) return 'degrading';
|
|
235
|
+
}
|
|
236
|
+
return 'stable';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function computeAggregateFeedbackStats(options = {}) {
|
|
240
|
+
const { entries, stores } = collectAggregateLogEntries(FEEDBACK_LOG, options);
|
|
241
|
+
const memory = collectAggregateLogEntries(MEMORY_LOG, options);
|
|
242
|
+
const { totalPositive, totalNegative, rubricSamples } = summarizeFeedbackEntries(entries);
|
|
243
|
+
const total = totalPositive + totalNegative;
|
|
244
|
+
const approvalRate = total > 0 ? Math.round((totalPositive / total) * 1000) / 1000 : 0;
|
|
245
|
+
const windows = createRateWindows(total, totalPositive, approvalRate);
|
|
246
|
+
updateRateWindows(windows, entries);
|
|
247
|
+
finalizeRateWindows(windows);
|
|
248
|
+
const trend = trendFromRateWindows(windows);
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
total,
|
|
252
|
+
totalPositive,
|
|
253
|
+
totalNegative,
|
|
254
|
+
approvalRate,
|
|
255
|
+
recentRate: windows['7d'].rate || approvalRate,
|
|
256
|
+
trend,
|
|
257
|
+
windows,
|
|
258
|
+
rubric: { samples: rubricSamples || memory.entries.length, blockedPromotions: 0, failingCriteria: {} },
|
|
259
|
+
aggregate: {
|
|
260
|
+
enabled: true,
|
|
261
|
+
stores: stores.length,
|
|
262
|
+
feedbackLogPaths: stores.map((dir) => path.join(dir, FEEDBACK_LOG)),
|
|
263
|
+
memoryStores: memory.stores.length,
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function getAggregateStatuslineCachePath(options = {}) {
|
|
269
|
+
const env = options.env || process.env;
|
|
270
|
+
if (env.THUMBGATE_STATUSLINE_CACHE) return path.resolve(env.THUMBGATE_STATUSLINE_CACHE);
|
|
271
|
+
return path.join(getHomeDir({ env }), '.thumbgate', STATUSLINE_CACHE);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = {
|
|
275
|
+
collectAggregateLogEntries,
|
|
276
|
+
computeAggregateFeedbackStats,
|
|
277
|
+
getAggregateStatuslineCachePath,
|
|
278
|
+
listFeedbackStoreDirs,
|
|
279
|
+
normalizeSignal,
|
|
280
|
+
shouldAggregateFeedback,
|
|
281
|
+
};
|
package/scripts/feedback-loop.js
CHANGED
|
@@ -1499,10 +1499,44 @@ function captureFeedback(params) {
|
|
|
1499
1499
|
totalGates: promoteResult.totalGates,
|
|
1500
1500
|
});
|
|
1501
1501
|
} catch { /* activation telemetry is non-critical */ }
|
|
1502
|
+
|
|
1503
|
+
// Trigger Self-Harness Optimizer to propagate the new rules to prompt files & validate
|
|
1504
|
+
try {
|
|
1505
|
+
const { fork } = require('child_process');
|
|
1506
|
+
const localOptimizerPath = path.join(process.cwd(), 'scripts', 'self-harness-optimizer.js');
|
|
1507
|
+
const packageOptimizerPath = path.join(__dirname, 'self-harness-optimizer.js');
|
|
1508
|
+
|
|
1509
|
+
if (fs.existsSync(localOptimizerPath)) {
|
|
1510
|
+
fork(localOptimizerPath, [], { stdio: 'ignore', detached: true }).unref();
|
|
1511
|
+
} else if (fs.existsSync(packageOptimizerPath)) {
|
|
1512
|
+
fork(packageOptimizerPath, [], { stdio: 'ignore', detached: true }).unref();
|
|
1513
|
+
}
|
|
1514
|
+
} catch (err) {
|
|
1515
|
+
console.error('Failed to trigger self-harness optimizer:', err);
|
|
1516
|
+
}
|
|
1502
1517
|
}
|
|
1503
1518
|
} catch { /* Gate promotion is non-critical */ }
|
|
1504
1519
|
}
|
|
1505
1520
|
|
|
1521
|
+
// Auto-export to Obsidian if configured (deferred but tracked)
|
|
1522
|
+
if (process.env.THUMBGATE_OBSIDIAN_VAULT_PATH) {
|
|
1523
|
+
const exportPromise = new Promise((resolve) => {
|
|
1524
|
+
setImmediate(() => {
|
|
1525
|
+
try {
|
|
1526
|
+
const { exportAll } = require('./obsidian-export');
|
|
1527
|
+
exportAll({
|
|
1528
|
+
feedbackDir: FEEDBACK_DIR,
|
|
1529
|
+
outputDir: process.env.THUMBGATE_OBSIDIAN_VAULT_PATH,
|
|
1530
|
+
});
|
|
1531
|
+
} catch (_err) {
|
|
1532
|
+
// Non-critical, do not crash feedback loop
|
|
1533
|
+
}
|
|
1534
|
+
resolve();
|
|
1535
|
+
});
|
|
1536
|
+
});
|
|
1537
|
+
trackBackgroundSideEffect(exportPromise);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1506
1540
|
// --- Deferred side-effects (contextFs, RLAIF — non-critical, potentially slow) ---
|
|
1507
1541
|
setImmediate(() => {
|
|
1508
1542
|
try {
|
|
@@ -53,10 +53,31 @@ function listJsonFiles(dirPath) {
|
|
|
53
53
|
return results;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
const STOPWORDS = new Set([
|
|
57
|
+
'a', 'about', 'above', 'after', 'again', 'against', 'all', 'am', 'an', 'and', 'any', 'are', 'arent', 'as', 'at',
|
|
58
|
+
'be', 'because', 'been', 'before', 'being', 'below', 'between', 'both', 'but', 'by',
|
|
59
|
+
'cant', 'cannot', 'could', 'couldnt', 'did', 'didnt', 'do', 'does', 'doesnt', 'doing', 'dont', 'down', 'during',
|
|
60
|
+
'each', 'few', 'for', 'from', 'further', 'had', 'hadnt', 'has', 'hasnt', 'have', 'havent', 'having',
|
|
61
|
+
'he', 'hed', 'hell', 'hes', 'her', 'here', 'heres', 'hers', 'herself', 'him', 'himself', 'his',
|
|
62
|
+
'how', 'hows', 'i', 'id', 'ill', 'im', 'ive', 'if', 'in', 'into', 'is', 'isnt', 'it', 'its', 'itself',
|
|
63
|
+
'lets', 'me', 'more', 'most', 'mustnt', 'my', 'myself',
|
|
64
|
+
'no', 'nor', 'not', 'of', 'off', 'on', 'once', 'only', 'or', 'other', 'ought', 'our', 'ours', 'ourselves', 'out', 'over', 'own',
|
|
65
|
+
'same', 'shant', 'she', 'shed', 'shell', 'shes', 'should', 'shouldnt', 'so', 'some', 'such',
|
|
66
|
+
'than', 'that', 'thats', 'the', 'their', 'theirs', 'them', 'themselves', 'then', 'there', 'theres', 'these', 'they', 'theyd', 'theyll', 'theyre', 'theyve', 'this', 'those', 'through', 'to', 'too', 'under', 'until', 'up', 'very',
|
|
67
|
+
'was', 'wasnt', 'we', 'wed', 'well', 'were', 'weve', 'werent', 'what', 'whats', 'when', 'whens', 'where', 'wheres', 'which', 'while', 'who', 'whos', 'whom', 'why', 'whys',
|
|
68
|
+
'with', 'wont', 'would', 'wouldnt', 'you', 'youd', 'youll', 'youre', 'youve', 'your', 'yours', 'yourself', 'yourselves'
|
|
69
|
+
]);
|
|
70
|
+
|
|
56
71
|
function tokenize(text) {
|
|
57
72
|
return String(text || '').toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
58
73
|
}
|
|
59
74
|
|
|
75
|
+
function getSearchTokens(queryText) {
|
|
76
|
+
const allTokens = tokenize(queryText);
|
|
77
|
+
const contentTokens = allTokens.filter(t => !STOPWORDS.has(t));
|
|
78
|
+
return contentTokens.length > 0 ? contentTokens : allTokens;
|
|
79
|
+
}
|
|
80
|
+
|
|
60
81
|
function unique(arr) {
|
|
61
82
|
return [...new Set(arr)];
|
|
62
83
|
}
|
|
@@ -82,7 +103,7 @@ function substringBoost(query, text) {
|
|
|
82
103
|
const q = query.toLowerCase();
|
|
83
104
|
const t = text.toLowerCase();
|
|
84
105
|
if (t.includes(q)) return 0.3;
|
|
85
|
-
const words = q.split(/\s+/).filter((w) => w.length > 2);
|
|
106
|
+
const words = q.split(/\s+/).filter((w) => w.length > 2 && !STOPWORDS.has(w));
|
|
86
107
|
const matched = words.filter((w) => t.includes(w)).length;
|
|
87
108
|
return words.length > 0 ? (matched / words.length) * 0.2 : 0;
|
|
88
109
|
}
|
|
@@ -117,8 +138,12 @@ function scoreRecord(queryTokens, queryText, record) {
|
|
|
117
138
|
const recency = recencyScore(record.timestamp);
|
|
118
139
|
const signalBoost = record.signal === 'down' ? 0.05 : 0;
|
|
119
140
|
|
|
141
|
+
const matchScore = jaccard + substr;
|
|
142
|
+
const isWildcard = !queryText || queryText === '*';
|
|
143
|
+
const score = isWildcard ? (recency + signalBoost || 0.01) : (matchScore > 0 ? matchScore + recency + signalBoost : 0);
|
|
144
|
+
|
|
120
145
|
return {
|
|
121
|
-
score:
|
|
146
|
+
score: Number(score.toFixed(4)),
|
|
122
147
|
record,
|
|
123
148
|
matchedTokens: unique(queryTokens).filter((t) => new Set(recordTokens).has(t)),
|
|
124
149
|
};
|
|
@@ -129,7 +154,7 @@ function scoreRecord(queryTokens, queryText, record) {
|
|
|
129
154
|
// ---------------------------------------------------------------------------
|
|
130
155
|
|
|
131
156
|
function searchFeedbackLog(queryText, limit = 5, options = {}) {
|
|
132
|
-
const logPath = path.join(getFeedbackDir(), 'feedback-log.jsonl');
|
|
157
|
+
const logPath = path.join(options.feedbackDir || getFeedbackDir(), 'feedback-log.jsonl');
|
|
133
158
|
let records = readJsonl(logPath);
|
|
134
159
|
|
|
135
160
|
// SQLite fallback: if JSONL is empty/tiny, pull records from the lesson DB
|
|
@@ -156,7 +181,7 @@ function searchFeedbackLog(queryText, limit = 5, options = {}) {
|
|
|
156
181
|
|
|
157
182
|
// Wildcard query: return all records sorted by recency
|
|
158
183
|
const isWildcard = queryText === '*' || queryText === '';
|
|
159
|
-
const queryTokens = isWildcard ? [] :
|
|
184
|
+
const queryTokens = isWildcard ? [] : getSearchTokens(queryText);
|
|
160
185
|
|
|
161
186
|
let scored = isWildcard
|
|
162
187
|
? records.map((r) => ({ score: recencyScore(r.timestamp) || 0.01, record: r, matchedTokens: [] }))
|
|
@@ -186,9 +211,9 @@ function searchFeedbackLog(queryText, limit = 5, options = {}) {
|
|
|
186
211
|
}
|
|
187
212
|
|
|
188
213
|
function searchContextFs(queryText, limit = 5, options = {}) {
|
|
189
|
-
const contextDir = getContextFsDir();
|
|
214
|
+
const contextDir = options.contextDir || (options.feedbackDir ? path.join(options.feedbackDir, 'contextfs') : getContextFsDir());
|
|
190
215
|
const namespaces = options.namespaces || ['memory/error', 'memory/learning', 'rules', 'raw_history'];
|
|
191
|
-
const queryTokens =
|
|
216
|
+
const queryTokens = getSearchTokens(queryText);
|
|
192
217
|
const scored = [];
|
|
193
218
|
|
|
194
219
|
for (const ns of namespaces) {
|
|
@@ -221,12 +246,12 @@ function searchContextFs(queryText, limit = 5, options = {}) {
|
|
|
221
246
|
}));
|
|
222
247
|
}
|
|
223
248
|
|
|
224
|
-
function searchPreventionRules(queryText, limit = 5) {
|
|
225
|
-
const rulesPath = path.join(getFeedbackDir(), 'prevention-rules.md');
|
|
249
|
+
function searchPreventionRules(queryText, limit = 5, options = {}) {
|
|
250
|
+
const rulesPath = path.join(options.feedbackDir || getFeedbackDir(), 'prevention-rules.md');
|
|
226
251
|
if (!fs.existsSync(rulesPath)) return [];
|
|
227
252
|
|
|
228
253
|
const content = fs.readFileSync(rulesPath, 'utf-8');
|
|
229
|
-
const queryTokens =
|
|
254
|
+
const queryTokens = getSearchTokens(queryText);
|
|
230
255
|
const blocks = content.split(/^#{1,3}\s+/m).filter(Boolean);
|
|
231
256
|
|
|
232
257
|
return blocks
|
|
@@ -252,7 +277,7 @@ function searchPreventionRules(queryText, limit = 5) {
|
|
|
252
277
|
function searchAll(queryText, limit = 10, options = {}) {
|
|
253
278
|
const feedbackResults = searchFeedbackLog(queryText, limit, options);
|
|
254
279
|
const contextResults = searchContextFs(queryText, limit, options);
|
|
255
|
-
const ruleResults = searchPreventionRules(queryText, limit);
|
|
280
|
+
const ruleResults = searchPreventionRules(queryText, limit, options);
|
|
256
281
|
|
|
257
282
|
const merged = [
|
|
258
283
|
...feedbackResults.map((r) => ({ ...r, _source_type: 'feedback' })),
|