thumbgate 1.27.18 → 1.27.20
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 +4 -3
- package/.well-known/agentic-verify.txt +1 -0
- package/.well-known/llms.txt +33 -12
- package/.well-known/mcp/server-card.json +8 -8
- package/README.md +246 -30
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/gcp/dfcx-webhook-gate.js +295 -0
- package/adapters/gemini/function-declarations.json +1 -0
- package/adapters/letta/README.md +41 -0
- package/adapters/letta/thumbgate-letta-adapter.js +133 -0
- package/adapters/mcp/server-stdio.js +263 -11
- 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/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +1456 -122
- package/bin/dashboard-cli.js +7 -0
- package/bin/postinstall.js +11 -27
- package/commands/dashboard.md +15 -0
- package/commands/thumbgate-dashboard.md +15 -0
- package/config/gate-classifier-routing.json +98 -0
- package/config/gate-templates.json +216 -0
- package/config/gates/claim-verification.json +12 -0
- package/config/gates/default.json +31 -2
- package/config/github-about.json +2 -2
- package/config/mcp-allowlists.json +23 -13
- package/config/merge-quality-checks.json +0 -1
- package/config/model-candidates.json +121 -6
- package/config/post-deploy-marketing-pages.json +80 -0
- package/config/tessl-tiles.json +1 -3
- package/openapi/openapi.yaml +12 -0
- package/package.json +225 -100
- package/public/about.html +162 -0
- package/public/agent-manager.html +179 -0
- package/public/agents-cost-savings.html +153 -0
- package/public/ai-malpractice-prevention.html +818 -0
- 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-logo-transparent.svg +28 -0
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -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 +21 -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 +4 -4
- package/public/brand/thumbgate-mark.svg +19 -0
- package/public/brand/thumbgate-og.svg +16 -0
- package/public/chatgpt-app.html +330 -0
- package/public/codex-enterprise.html +123 -0
- package/public/codex-plugin.html +72 -20
- package/public/compare.html +31 -8
- package/public/dashboard.html +930 -166
- package/public/diagnostic.html +345 -0
- package/public/federal.html +2 -2
- package/public/guide.html +33 -13
- package/public/index.html +469 -111
- package/public/install.html +193 -0
- package/public/js/buyer-intent.js +672 -0
- package/public/learn.html +183 -18
- package/public/lessons.html +168 -10
- package/public/numbers.html +7 -7
- package/public/pricing.html +399 -0
- package/public/pro.html +34 -11
- package/scripts/action-receipts.js +324 -0
- package/scripts/activation-quickstart.js +187 -0
- package/scripts/agent-memory-lifecycle.js +211 -0
- package/scripts/agent-operations-planner.js +621 -0
- package/scripts/agent-readiness.js +20 -3
- package/scripts/agent-reward-model.js +53 -1
- package/scripts/ai-component-inventory.js +367 -0
- package/scripts/async-eval-observability.js +236 -0
- package/scripts/audit.js +65 -0
- package/scripts/auto-promote-gates.js +82 -10
- package/scripts/auto-wire-hooks.js +14 -0
- package/scripts/aws-blocks-guardrails.js +272 -0
- package/scripts/billing.js +93 -1
- package/scripts/bot-detection.js +61 -3
- package/scripts/build-metadata.js +50 -10
- package/scripts/classifier-routing.js +130 -0
- package/scripts/cli-feedback.js +4 -2
- package/scripts/cli-schema.js +97 -0
- package/scripts/cli-telemetry.js +6 -1
- package/scripts/commercial-offer.js +82 -2
- package/scripts/context-manager.js +74 -6
- package/scripts/dashboard-chat.js +332 -0
- package/scripts/dashboard.js +68 -2
- 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 +123 -1
- package/scripts/feedback-quality.js +87 -0
- package/scripts/feedback-sanitizer.js +105 -0
- package/scripts/filesystem-search.js +35 -10
- package/scripts/gate-stats.js +89 -0
- package/scripts/gates-engine.js +1176 -85
- package/scripts/gemini-embedding-policy.js +2 -1
- package/scripts/hook-runtime.js +20 -14
- package/scripts/hook-stop-anti-claim.js +301 -0
- package/scripts/hook-thumbgate-cache-updater.js +18 -2
- package/scripts/hybrid-feedback-context.js +142 -7
- package/scripts/install-shim.js +87 -0
- package/scripts/lesson-inference.js +8 -3
- package/scripts/lesson-search.js +17 -1
- package/scripts/license.js +10 -10
- package/scripts/llm-client.js +169 -4
- package/scripts/local-model-profile.js +15 -8
- package/scripts/mcp-config.js +7 -1
- package/scripts/mcp-oauth.js +293 -0
- package/scripts/memory-scope-readiness.js +159 -0
- package/scripts/meta-agent-loop.js +36 -0
- package/scripts/noop-detect.js +285 -0
- package/scripts/operational-integrity.js +39 -5
- package/scripts/oss-pr-opportunity-scout.js +35 -5
- package/scripts/parallel-workflow-orchestrator.js +293 -0
- package/scripts/plan-gate.js +243 -0
- package/scripts/plausible-domain-config.js +99 -0
- package/scripts/plausible-server-events.js +9 -6
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/proxy-pointer-rag-guardrails.js +42 -1
- package/scripts/published-cli.js +0 -8
- package/scripts/qa-scenario-planner.js +136 -0
- package/scripts/rate-limiter.js +64 -13
- package/scripts/repeat-metric.js +137 -0
- package/scripts/secret-fixture-tokens.js +61 -0
- package/scripts/secret-redaction.js +166 -0
- package/scripts/secret-scanner.js +44 -5
- package/scripts/security-scanner.js +260 -10
- package/scripts/self-distill-agent.js +3 -1
- package/scripts/self-harness-optimizer.js +141 -0
- package/scripts/self-healing-check.js +193 -0
- package/scripts/self-protection.js +90 -0
- package/scripts/seo-gsd.js +916 -7
- package/scripts/silent-failure-cluster.js +531 -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 +28 -2
- package/scripts/statusline.sh +20 -4
- package/scripts/sync-telemetry-from-prod.js +374 -0
- package/scripts/telemetry-analytics.js +357 -0
- package/scripts/thompson-sampling.js +31 -10
- package/scripts/thumbgate-bench.js +16 -1
- package/scripts/thumbgate-search.js +85 -19
- package/scripts/tool-contract-validator.js +76 -0
- package/scripts/tool-registry.js +169 -1
- package/scripts/trajectory-scorer.js +63 -0
- package/scripts/vector-store.js +45 -0
- package/scripts/verify-marketing-pages-deployed.js +212 -0
- package/scripts/visitor-journey.js +172 -0
- package/scripts/workflow-sentinel.js +286 -53
- package/scripts/workspace-evolver.js +62 -2
- package/src/api/server.js +2683 -319
- package/.claude-plugin/marketplace.json +0 -85
- package/adapters/chatgpt/openapi.yaml +0 -1695
- package/scripts/bot-detector.js +0 -50
|
@@ -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
|
}
|
|
@@ -1059,6 +1143,7 @@ function captureFeedback(params) {
|
|
|
1059
1143
|
: null),
|
|
1060
1144
|
structuredRule: structuredRule || null,
|
|
1061
1145
|
...(reflection && { reflection }),
|
|
1146
|
+
gateAction: params.gateAction || null,
|
|
1062
1147
|
timestamp: now,
|
|
1063
1148
|
};
|
|
1064
1149
|
|
|
@@ -1112,6 +1197,7 @@ function captureFeedback(params) {
|
|
|
1112
1197
|
summary.lastUpdated = now;
|
|
1113
1198
|
saveSummary(summary);
|
|
1114
1199
|
appendJSONL(FEEDBACK_LOG_PATH, feedbackEvent);
|
|
1200
|
+
emitAnonymousFeedbackPing(signal);
|
|
1115
1201
|
try { appendRejectionLedger(feedbackEvent, action.reason); } catch { /* non-critical */ }
|
|
1116
1202
|
try {
|
|
1117
1203
|
appendSequence(historyEntries, feedbackEvent, getFeedbackPaths(), { accepted: false });
|
|
@@ -1153,6 +1239,7 @@ function captureFeedback(params) {
|
|
|
1153
1239
|
...feedbackEvent,
|
|
1154
1240
|
validationIssues: prepared.issues,
|
|
1155
1241
|
});
|
|
1242
|
+
emitAnonymousFeedbackPing(signal);
|
|
1156
1243
|
try { appendRejectionLedger(feedbackEvent, `Schema validation failed: ${prepared.issues.join('; ')}`); } catch { /* non-critical */ }
|
|
1157
1244
|
try {
|
|
1158
1245
|
appendSequence(historyEntries, feedbackEvent, getFeedbackPaths(), { accepted: false });
|
|
@@ -1227,6 +1314,7 @@ function captureFeedback(params) {
|
|
|
1227
1314
|
}
|
|
1228
1315
|
|
|
1229
1316
|
appendJSONL(FEEDBACK_LOG_PATH, feedbackEvent);
|
|
1317
|
+
emitAnonymousFeedbackPing(signal);
|
|
1230
1318
|
|
|
1231
1319
|
// Synthesis: merge similar lessons instead of creating duplicates
|
|
1232
1320
|
let synthesisResult = null;
|
|
@@ -1398,7 +1486,7 @@ function captureFeedback(params) {
|
|
|
1398
1486
|
if (feedbackEvent.signal === 'negative') {
|
|
1399
1487
|
try {
|
|
1400
1488
|
const autoPromote = require('./auto-promote-gates');
|
|
1401
|
-
const promoteResult = autoPromote.promote(FEEDBACK_LOG_PATH);
|
|
1489
|
+
const promoteResult = autoPromote.promote(FEEDBACK_LOG_PATH, { gateAction: feedbackEvent.gateAction });
|
|
1402
1490
|
// First-rule activation telemetry: anonymous ping the first time
|
|
1403
1491
|
// a prevention rule auto-promotes for this install. Idempotent —
|
|
1404
1492
|
// see scripts/activation-tracker.js. Critical for activation funnel
|
|
@@ -1411,10 +1499,44 @@ function captureFeedback(params) {
|
|
|
1411
1499
|
totalGates: promoteResult.totalGates,
|
|
1412
1500
|
});
|
|
1413
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
|
+
}
|
|
1414
1517
|
}
|
|
1415
1518
|
} catch { /* Gate promotion is non-critical */ }
|
|
1416
1519
|
}
|
|
1417
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
|
+
|
|
1418
1540
|
// --- Deferred side-effects (contextFs, RLAIF — non-critical, potentially slow) ---
|
|
1419
1541
|
setImmediate(() => {
|
|
1420
1542
|
try {
|
|
@@ -62,6 +62,92 @@ function normalizeFeedbackText(value) {
|
|
|
62
62
|
.trim();
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function editDistance(a, b) {
|
|
66
|
+
const left = String(a || '');
|
|
67
|
+
const right = String(b || '');
|
|
68
|
+
const dp = Array.from({ length: left.length + 1 }, () => Array(right.length + 1).fill(0));
|
|
69
|
+
for (let i = 0; i <= left.length; i++) dp[i][0] = i;
|
|
70
|
+
for (let j = 0; j <= right.length; j++) dp[0][j] = j;
|
|
71
|
+
for (let i = 1; i <= left.length; i++) {
|
|
72
|
+
for (let j = 1; j <= right.length; j++) {
|
|
73
|
+
const cost = left[i - 1] === right[j - 1] ? 0 : 1;
|
|
74
|
+
dp[i][j] = Math.min(
|
|
75
|
+
dp[i - 1][j] + 1,
|
|
76
|
+
dp[i][j - 1] + 1,
|
|
77
|
+
dp[i - 1][j - 1] + cost,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return dp[left.length][right.length];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isNearThumbToken(token) {
|
|
85
|
+
const value = String(token || '');
|
|
86
|
+
if (value.length < 4) return false;
|
|
87
|
+
return editDistance(value, 'thumb') <= 1 || editDistance(value, 'thumbs') <= 2;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isNearUpToken(token) {
|
|
91
|
+
const value = String(token || '');
|
|
92
|
+
return value === 'up' || editDistance(value, 'up') <= 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isNearDownToken(token) {
|
|
96
|
+
const value = String(token || '');
|
|
97
|
+
if (value.length < 2) return false;
|
|
98
|
+
return editDistance(value, 'down') <= 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function detectFeedbackSignal(value) {
|
|
102
|
+
const raw = String(value || '');
|
|
103
|
+
if (/[👎👎🏻👎🏼👎🏽👎🏾👎🏿]/u.test(raw)) return { signal: 'down', confidence: 'emoji', match: '👎' };
|
|
104
|
+
if (/[👍👍🏻👍🏼👍🏽👍🏾👍🏿]/u.test(raw)) return { signal: 'up', confidence: 'emoji', match: '👍' };
|
|
105
|
+
|
|
106
|
+
const normalized = normalizeFeedbackText(raw);
|
|
107
|
+
if (!normalized) return null;
|
|
108
|
+
|
|
109
|
+
const exactDown = [
|
|
110
|
+
/\bthumbs?\s*down\b/,
|
|
111
|
+
/\bthumbs?down\b/,
|
|
112
|
+
/\bthat failed\b/,
|
|
113
|
+
/\bit failed\b/,
|
|
114
|
+
/\bthat was wrong\b/,
|
|
115
|
+
/\bfix this\b/,
|
|
116
|
+
];
|
|
117
|
+
if (exactDown.some((pattern) => pattern.test(normalized))) {
|
|
118
|
+
return { signal: 'down', confidence: 'exact', match: normalized };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const exactUp = [
|
|
122
|
+
/\bthumbs?\s*up\b/,
|
|
123
|
+
/\bthumbs?up\b/,
|
|
124
|
+
/\bthat worked\b/,
|
|
125
|
+
/\bit worked\b/,
|
|
126
|
+
/\blooks good\b/,
|
|
127
|
+
/\bgood job\b/,
|
|
128
|
+
/\bgood work\b/,
|
|
129
|
+
/\bnice work\b/,
|
|
130
|
+
/\bperfect\b/,
|
|
131
|
+
/\blgtm\b/,
|
|
132
|
+
];
|
|
133
|
+
if (exactUp.some((pattern) => pattern.test(normalized))) {
|
|
134
|
+
return { signal: 'up', confidence: 'exact', match: normalized };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const words = normalized.split(/\s+/).filter(Boolean);
|
|
138
|
+
for (let i = 0; i < words.length - 1; i++) {
|
|
139
|
+
if (!isNearThumbToken(words[i])) continue;
|
|
140
|
+
if (isNearDownToken(words[i + 1])) {
|
|
141
|
+
return { signal: 'down', confidence: 'fuzzy', match: `${words[i]} ${words[i + 1]}` };
|
|
142
|
+
}
|
|
143
|
+
if (isNearUpToken(words[i + 1])) {
|
|
144
|
+
return { signal: 'up', confidence: 'fuzzy', match: `${words[i]} ${words[i + 1]}` };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
65
151
|
function isGenericFeedbackText(value, signal) {
|
|
66
152
|
const normalized = normalizeFeedbackText(value);
|
|
67
153
|
if (!normalized) return false;
|
|
@@ -131,6 +217,7 @@ function buildClarificationMessage(params = {}) {
|
|
|
131
217
|
|
|
132
218
|
module.exports = {
|
|
133
219
|
GENERIC_PHRASE_RULES,
|
|
220
|
+
detectFeedbackSignal,
|
|
134
221
|
normalizeFeedbackSignal,
|
|
135
222
|
normalizeFeedbackText,
|
|
136
223
|
isGenericFeedbackText,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
const TRANSPORT_KEYS = new Set([
|
|
6
|
+
'hookeventname',
|
|
7
|
+
'hook_event_name',
|
|
8
|
+
'sessionid',
|
|
9
|
+
'session_id',
|
|
10
|
+
'transcriptpath',
|
|
11
|
+
'transcript_path',
|
|
12
|
+
'timestamp',
|
|
13
|
+
'createdat',
|
|
14
|
+
'created_at',
|
|
15
|
+
'updatedat',
|
|
16
|
+
'updated_at',
|
|
17
|
+
'cwd',
|
|
18
|
+
'pid',
|
|
19
|
+
'processid',
|
|
20
|
+
'process_id',
|
|
21
|
+
'promptid',
|
|
22
|
+
'prompt_id',
|
|
23
|
+
'traceid',
|
|
24
|
+
'trace_id',
|
|
25
|
+
'requestid',
|
|
26
|
+
'request_id',
|
|
27
|
+
'installid',
|
|
28
|
+
'install_id',
|
|
29
|
+
'visitorsessionid',
|
|
30
|
+
'visitor_session_id',
|
|
31
|
+
'toolinput',
|
|
32
|
+
'tool_input',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const TRANSPORT_WORDS = new Set([
|
|
36
|
+
...TRANSPORT_KEYS,
|
|
37
|
+
'hook',
|
|
38
|
+
'event',
|
|
39
|
+
'userpromptsubmit',
|
|
40
|
+
'user_prompt_submit',
|
|
41
|
+
'pretooluse',
|
|
42
|
+
'pre_tool_use',
|
|
43
|
+
'posttooluse',
|
|
44
|
+
'post_tool_use',
|
|
45
|
+
'claude',
|
|
46
|
+
'codex',
|
|
47
|
+
'projects',
|
|
48
|
+
'redacted',
|
|
49
|
+
'tmp',
|
|
50
|
+
'private',
|
|
51
|
+
'folders',
|
|
52
|
+
'json',
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
function stripEphemeralText(text) {
|
|
56
|
+
if (!text || typeof text !== 'string') return '';
|
|
57
|
+
return String(text)
|
|
58
|
+
.replace(/["']?(?:hook_?event_?name|session_?id|transcript_?path|timestamp|created_?at|updated_?at|cwd|pid|process_?id|prompt_?id|trace_?id|request_?id|install_?id|visitor_?session_?id)["']?\s*[:=]\s*["']?[^"',}\]\s]+["']?/gi, ' ')
|
|
59
|
+
.replace(/\/(?:private\/)?tmp\/[^\s"',}\]]+/gi, ' ')
|
|
60
|
+
.replace(/\/var\/folders\/[^\s"',}\]]+/gi, ' ')
|
|
61
|
+
.replace(/\/Users\/[^/\s]+\/\.(?:claude|codex|thumbgate)\/[^\s"',}\]]+/gi, ' ')
|
|
62
|
+
.replace(/\/Users\/[^/\s]+\/\.config\/thumbgate\/[^\s"',}\]]+/gi, ' ')
|
|
63
|
+
.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, ' ')
|
|
64
|
+
.replace(/\b[0-9a-f]{24,}\b/gi, ' ')
|
|
65
|
+
.replace(/\b\d{4}-\d{2}-\d{2}t\d{2}:\d{2}:\d{2}(?:\.\d+)?z?\b/gi, ' ')
|
|
66
|
+
.replace(/\b\d{10,13}\b/g, ' ')
|
|
67
|
+
.replace(/:\d{4,5}\b/g, ':PORT');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function transportWordsOnly(text) {
|
|
71
|
+
const tokens = String(text || '')
|
|
72
|
+
.toLowerCase()
|
|
73
|
+
.replace(/[^a-z0-9_ -]/g, ' ')
|
|
74
|
+
.split(/\s+/)
|
|
75
|
+
.filter((token) => token.length >= 3);
|
|
76
|
+
if (tokens.length === 0) return true;
|
|
77
|
+
return tokens.every((token) => TRANSPORT_WORDS.has(token));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sanitizeFeedbackText(text) {
|
|
81
|
+
const stripped = stripEphemeralText(text)
|
|
82
|
+
.replace(/\/Users\/[^\s/]+/g, '/Users/redacted')
|
|
83
|
+
.replace(/\s+/g, ' ')
|
|
84
|
+
.trim();
|
|
85
|
+
if (transportWordsOnly(stripped)) return '';
|
|
86
|
+
return stripped;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function actionFingerprint(parts) {
|
|
90
|
+
const raw = Array.isArray(parts) ? parts.join(' ') : String(parts || '');
|
|
91
|
+
const stable = sanitizeFeedbackText(raw)
|
|
92
|
+
.toLowerCase()
|
|
93
|
+
.replace(/[^a-z0-9._:/ -]/g, ' ')
|
|
94
|
+
.replace(/\s+/g, ' ')
|
|
95
|
+
.trim();
|
|
96
|
+
if (!stable || transportWordsOnly(stable)) return null;
|
|
97
|
+
return crypto.createHash('sha256').update(stable).digest('hex').slice(0, 16);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
TRANSPORT_WORDS,
|
|
102
|
+
sanitizeFeedbackText,
|
|
103
|
+
actionFingerprint,
|
|
104
|
+
transportWordsOnly,
|
|
105
|
+
};
|