thumbgate 1.27.4 → 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/dashboard.md +15 -0
- package/.claude/commands/thumbgate-blocked.md +27 -0
- package/.claude/commands/thumbgate-dashboard.md +15 -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 +2 -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 +230 -6
- package/bin/postinstall.js +1 -1
- package/commands/dashboard.md +15 -0
- package/commands/thumbgate-dashboard.md +15 -0
- package/config/gate-templates.json +84 -0
- package/config/gates/claim-verification.json +12 -0
- package/config/gates/default.json +20 -0
- package/config/github-about.json +1 -1
- package/config/model-candidates.json +50 -0
- package/config/post-deploy-marketing-pages.json +5 -0
- package/package.json +67 -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 +316 -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 +88 -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/billing.js +12 -1
- package/scripts/build-metadata.js +24 -3
- package/scripts/cli-schema.js +42 -10
- package/scripts/dashboard-chat.js +53 -7
- package/scripts/dashboard.js +12 -17
- 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 +121 -0
- package/scripts/filesystem-search.js +35 -10
- package/scripts/gates-engine.js +234 -7
- 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/hybrid-feedback-context.js +1 -0
- 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 +15 -2
- package/scripts/plausible-server-events.js +4 -4
- 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 +862 -146
|
@@ -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
|
@@ -56,6 +56,90 @@ const {
|
|
|
56
56
|
|
|
57
57
|
const AUDIT_TRAIL_TAG = 'audit-trail';
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Anonymous fire-and-forget CLI feedback telemetry.
|
|
61
|
+
*
|
|
62
|
+
* Pings the hosted /v1/telemetry/ping endpoint exactly once per successful
|
|
63
|
+
* local feedback capture so the dashboard can measure CLI-side lesson volume.
|
|
64
|
+
*
|
|
65
|
+
* Hard contract (do NOT widen without explicit approval):
|
|
66
|
+
* - ONE event type: `feedback_captured`
|
|
67
|
+
* - Payload: { installId, signal: 'up'|'down', tier, ts } only.
|
|
68
|
+
* No context strings, tags, file paths, or content of any kind.
|
|
69
|
+
* - Opt-out: THUMBGATE_DISABLE_TELEMETRY=1 (or 'true') short-circuits
|
|
70
|
+
* immediately. Legacy THUMBGATE_NO_TELEMETRY=1 / DO_NOT_TRACK=1 are
|
|
71
|
+
* also honored for parity with cli-telemetry.js.
|
|
72
|
+
* - Fire-and-forget: NEVER await this call. Errors are swallowed.
|
|
73
|
+
* - 2-second timeout via AbortSignal.timeout.
|
|
74
|
+
*/
|
|
75
|
+
function emitAnonymousFeedbackPing(signal) {
|
|
76
|
+
try {
|
|
77
|
+
const env = process.env || {};
|
|
78
|
+
if (
|
|
79
|
+
env.THUMBGATE_DISABLE_TELEMETRY === '1' ||
|
|
80
|
+
env.THUMBGATE_DISABLE_TELEMETRY === 'true' ||
|
|
81
|
+
env.THUMBGATE_NO_TELEMETRY === '1' ||
|
|
82
|
+
env.DO_NOT_TRACK === '1'
|
|
83
|
+
) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const normalizedSignal = signal === 'positive' ? 'up' : signal === 'negative' ? 'down' : null;
|
|
88
|
+
if (!normalizedSignal) return;
|
|
89
|
+
|
|
90
|
+
// Reuse the canonical installId from cli-telemetry.js (persisted at
|
|
91
|
+
// ~/.thumbgate/install-id). Falls back to a fresh UUID if that module
|
|
92
|
+
// is unavailable — better to ship an event we can dedup on the server
|
|
93
|
+
// than to drop the ping entirely.
|
|
94
|
+
let installId = null;
|
|
95
|
+
try {
|
|
96
|
+
const { getInstallId } = require('./cli-telemetry');
|
|
97
|
+
installId = getInstallId();
|
|
98
|
+
} catch (_) { /* fall through */ }
|
|
99
|
+
if (!installId) {
|
|
100
|
+
try {
|
|
101
|
+
installId = require('crypto').randomUUID();
|
|
102
|
+
} catch (_) {
|
|
103
|
+
return; // no crypto, no install id → drop silently
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let tier = 'free';
|
|
108
|
+
try {
|
|
109
|
+
const { getStatuslineMeta } = require('./statusline-meta');
|
|
110
|
+
const meta = getStatuslineMeta({ env });
|
|
111
|
+
const rawTier = String(meta && meta.tier ? meta.tier : 'free').toLowerCase();
|
|
112
|
+
if (rawTier === 'pro' || rawTier === 'enterprise' || rawTier === 'free') {
|
|
113
|
+
tier = rawTier;
|
|
114
|
+
}
|
|
115
|
+
} catch (_) { /* default to 'free' */ }
|
|
116
|
+
|
|
117
|
+
const base = env.THUMBGATE_PUBLIC_APP_ORIGIN
|
|
118
|
+
|| env.THUMBGATE_API_URL
|
|
119
|
+
|| 'https://thumbgate-production.up.railway.app';
|
|
120
|
+
|
|
121
|
+
const body = JSON.stringify({
|
|
122
|
+
eventType: 'feedback_captured',
|
|
123
|
+
clientType: 'cli',
|
|
124
|
+
installId,
|
|
125
|
+
signal: normalizedSignal,
|
|
126
|
+
tier,
|
|
127
|
+
ts: new Date().toISOString(),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Fire-and-forget. No await. AbortSignal.timeout enforces the 2s cap.
|
|
131
|
+
if (typeof fetch !== 'function' || typeof AbortSignal === 'undefined' || typeof AbortSignal.timeout !== 'function') {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
fetch(`${base.replace(/\/+$/, '')}/v1/telemetry/ping`, {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: { 'Content-Type': 'application/json' },
|
|
137
|
+
body,
|
|
138
|
+
signal: AbortSignal.timeout(2000),
|
|
139
|
+
}).catch(() => { /* fire-and-forget */ });
|
|
140
|
+
} catch (_) { /* telemetry must never disrupt CLI */ }
|
|
141
|
+
}
|
|
142
|
+
|
|
59
143
|
function isAuditTrailEntry(entry = {}) {
|
|
60
144
|
return Array.isArray(entry.tags) && entry.tags.includes(AUDIT_TRAIL_TAG);
|
|
61
145
|
}
|
|
@@ -1113,6 +1197,7 @@ function captureFeedback(params) {
|
|
|
1113
1197
|
summary.lastUpdated = now;
|
|
1114
1198
|
saveSummary(summary);
|
|
1115
1199
|
appendJSONL(FEEDBACK_LOG_PATH, feedbackEvent);
|
|
1200
|
+
emitAnonymousFeedbackPing(signal);
|
|
1116
1201
|
try { appendRejectionLedger(feedbackEvent, action.reason); } catch { /* non-critical */ }
|
|
1117
1202
|
try {
|
|
1118
1203
|
appendSequence(historyEntries, feedbackEvent, getFeedbackPaths(), { accepted: false });
|
|
@@ -1154,6 +1239,7 @@ function captureFeedback(params) {
|
|
|
1154
1239
|
...feedbackEvent,
|
|
1155
1240
|
validationIssues: prepared.issues,
|
|
1156
1241
|
});
|
|
1242
|
+
emitAnonymousFeedbackPing(signal);
|
|
1157
1243
|
try { appendRejectionLedger(feedbackEvent, `Schema validation failed: ${prepared.issues.join('; ')}`); } catch { /* non-critical */ }
|
|
1158
1244
|
try {
|
|
1159
1245
|
appendSequence(historyEntries, feedbackEvent, getFeedbackPaths(), { accepted: false });
|
|
@@ -1228,6 +1314,7 @@ function captureFeedback(params) {
|
|
|
1228
1314
|
}
|
|
1229
1315
|
|
|
1230
1316
|
appendJSONL(FEEDBACK_LOG_PATH, feedbackEvent);
|
|
1317
|
+
emitAnonymousFeedbackPing(signal);
|
|
1231
1318
|
|
|
1232
1319
|
// Synthesis: merge similar lessons instead of creating duplicates
|
|
1233
1320
|
let synthesisResult = null;
|
|
@@ -1412,10 +1499,44 @@ function captureFeedback(params) {
|
|
|
1412
1499
|
totalGates: promoteResult.totalGates,
|
|
1413
1500
|
});
|
|
1414
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
|
+
}
|
|
1415
1517
|
}
|
|
1416
1518
|
} catch { /* Gate promotion is non-critical */ }
|
|
1417
1519
|
}
|
|
1418
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
|
+
|
|
1419
1540
|
// --- Deferred side-effects (contextFs, RLAIF — non-critical, potentially slow) ---
|
|
1420
1541
|
setImmediate(() => {
|
|
1421
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' })),
|