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.
Files changed (170) hide show
  1. package/.claude/commands/dashboard.md +15 -0
  2. package/.claude/commands/thumbgate-blocked.md +27 -0
  3. package/.claude/commands/thumbgate-dashboard.md +15 -0
  4. package/.claude/commands/thumbgate-doctor.md +30 -0
  5. package/.claude/commands/thumbgate-guard.md +36 -0
  6. package/.claude/commands/thumbgate-protect.md +30 -0
  7. package/.claude/commands/thumbgate-rules.md +30 -0
  8. package/.claude-plugin/plugin.json +4 -3
  9. package/.well-known/agentic-verify.txt +1 -0
  10. package/.well-known/llms.txt +33 -12
  11. package/.well-known/mcp/server-card.json +8 -8
  12. package/README.md +246 -30
  13. package/adapters/claude/.mcp.json +2 -2
  14. package/adapters/codex/config.toml +2 -2
  15. package/adapters/gcp/dfcx-webhook-gate.js +295 -0
  16. package/adapters/gemini/function-declarations.json +1 -0
  17. package/adapters/letta/README.md +41 -0
  18. package/adapters/letta/thumbgate-letta-adapter.js +133 -0
  19. package/adapters/mcp/server-stdio.js +263 -11
  20. package/adapters/opencode/opencode.json +1 -1
  21. package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
  22. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
  23. package/bench/observability-eval-suite.json +26 -0
  24. package/bench/thumbgate-bench.json +2 -2
  25. package/bin/cli.js +1456 -122
  26. package/bin/dashboard-cli.js +7 -0
  27. package/bin/postinstall.js +11 -27
  28. package/commands/dashboard.md +15 -0
  29. package/commands/thumbgate-dashboard.md +15 -0
  30. package/config/gate-classifier-routing.json +98 -0
  31. package/config/gate-templates.json +216 -0
  32. package/config/gates/claim-verification.json +12 -0
  33. package/config/gates/default.json +31 -2
  34. package/config/github-about.json +2 -2
  35. package/config/mcp-allowlists.json +23 -13
  36. package/config/merge-quality-checks.json +0 -1
  37. package/config/model-candidates.json +121 -6
  38. package/config/post-deploy-marketing-pages.json +80 -0
  39. package/config/tessl-tiles.json +1 -3
  40. package/openapi/openapi.yaml +12 -0
  41. package/package.json +225 -100
  42. package/public/about.html +162 -0
  43. package/public/agent-manager.html +179 -0
  44. package/public/agents-cost-savings.html +153 -0
  45. package/public/ai-malpractice-prevention.html +818 -0
  46. package/public/assets/brand/github-social-preview.png +0 -0
  47. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  48. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  49. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  50. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  51. package/public/assets/brand/thumbgate-logo-transparent.svg +28 -0
  52. package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -0
  53. package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
  54. package/public/assets/brand/thumbgate-mark-team.svg +26 -0
  55. package/public/assets/brand/thumbgate-mark.svg +21 -0
  56. package/public/assets/brand/thumbgate-wordmark.svg +20 -0
  57. package/public/assets/claude-thumbgate-statusbar.svg +8 -0
  58. package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
  59. package/public/assets/legal-intake-control-flow.svg +66 -0
  60. package/public/blog.html +4 -4
  61. package/public/brand/thumbgate-mark.svg +19 -0
  62. package/public/brand/thumbgate-og.svg +16 -0
  63. package/public/chatgpt-app.html +330 -0
  64. package/public/codex-enterprise.html +123 -0
  65. package/public/codex-plugin.html +72 -20
  66. package/public/compare.html +31 -8
  67. package/public/dashboard.html +930 -166
  68. package/public/diagnostic.html +345 -0
  69. package/public/federal.html +2 -2
  70. package/public/guide.html +33 -13
  71. package/public/index.html +469 -111
  72. package/public/install.html +193 -0
  73. package/public/js/buyer-intent.js +672 -0
  74. package/public/learn.html +183 -18
  75. package/public/lessons.html +168 -10
  76. package/public/numbers.html +7 -7
  77. package/public/pricing.html +399 -0
  78. package/public/pro.html +34 -11
  79. package/scripts/action-receipts.js +324 -0
  80. package/scripts/activation-quickstart.js +187 -0
  81. package/scripts/agent-memory-lifecycle.js +211 -0
  82. package/scripts/agent-operations-planner.js +621 -0
  83. package/scripts/agent-readiness.js +20 -3
  84. package/scripts/agent-reward-model.js +53 -1
  85. package/scripts/ai-component-inventory.js +367 -0
  86. package/scripts/async-eval-observability.js +236 -0
  87. package/scripts/audit.js +65 -0
  88. package/scripts/auto-promote-gates.js +82 -10
  89. package/scripts/auto-wire-hooks.js +14 -0
  90. package/scripts/aws-blocks-guardrails.js +272 -0
  91. package/scripts/billing.js +93 -1
  92. package/scripts/bot-detection.js +61 -3
  93. package/scripts/build-metadata.js +50 -10
  94. package/scripts/classifier-routing.js +130 -0
  95. package/scripts/cli-feedback.js +4 -2
  96. package/scripts/cli-schema.js +97 -0
  97. package/scripts/cli-telemetry.js +6 -1
  98. package/scripts/commercial-offer.js +82 -2
  99. package/scripts/context-manager.js +74 -6
  100. package/scripts/dashboard-chat.js +332 -0
  101. package/scripts/dashboard.js +68 -2
  102. package/scripts/export-databricks-bundle.js +5 -1
  103. package/scripts/export-dpo-pairs.js +7 -2
  104. package/scripts/feedback-aggregate.js +281 -0
  105. package/scripts/feedback-loop.js +123 -1
  106. package/scripts/feedback-quality.js +87 -0
  107. package/scripts/feedback-sanitizer.js +105 -0
  108. package/scripts/filesystem-search.js +35 -10
  109. package/scripts/gate-stats.js +89 -0
  110. package/scripts/gates-engine.js +1176 -85
  111. package/scripts/gemini-embedding-policy.js +2 -1
  112. package/scripts/hook-runtime.js +20 -14
  113. package/scripts/hook-stop-anti-claim.js +301 -0
  114. package/scripts/hook-thumbgate-cache-updater.js +18 -2
  115. package/scripts/hybrid-feedback-context.js +142 -7
  116. package/scripts/install-shim.js +87 -0
  117. package/scripts/lesson-inference.js +8 -3
  118. package/scripts/lesson-search.js +17 -1
  119. package/scripts/license.js +10 -10
  120. package/scripts/llm-client.js +169 -4
  121. package/scripts/local-model-profile.js +15 -8
  122. package/scripts/mcp-config.js +7 -1
  123. package/scripts/mcp-oauth.js +293 -0
  124. package/scripts/memory-scope-readiness.js +159 -0
  125. package/scripts/meta-agent-loop.js +36 -0
  126. package/scripts/noop-detect.js +285 -0
  127. package/scripts/operational-integrity.js +39 -5
  128. package/scripts/oss-pr-opportunity-scout.js +35 -5
  129. package/scripts/parallel-workflow-orchestrator.js +293 -0
  130. package/scripts/plan-gate.js +243 -0
  131. package/scripts/plausible-domain-config.js +99 -0
  132. package/scripts/plausible-server-events.js +9 -6
  133. package/scripts/pro-local-dashboard.js +4 -4
  134. package/scripts/proxy-pointer-rag-guardrails.js +42 -1
  135. package/scripts/published-cli.js +0 -8
  136. package/scripts/qa-scenario-planner.js +136 -0
  137. package/scripts/rate-limiter.js +64 -13
  138. package/scripts/repeat-metric.js +137 -0
  139. package/scripts/secret-fixture-tokens.js +61 -0
  140. package/scripts/secret-redaction.js +166 -0
  141. package/scripts/secret-scanner.js +44 -5
  142. package/scripts/security-scanner.js +260 -10
  143. package/scripts/self-distill-agent.js +3 -1
  144. package/scripts/self-harness-optimizer.js +141 -0
  145. package/scripts/self-healing-check.js +193 -0
  146. package/scripts/self-protection.js +90 -0
  147. package/scripts/seo-gsd.js +916 -7
  148. package/scripts/silent-failure-cluster.js +531 -0
  149. package/scripts/statusline-cache-path.js +17 -2
  150. package/scripts/statusline-cache-read.js +57 -0
  151. package/scripts/statusline-local-stats.js +9 -1
  152. package/scripts/statusline-meta.js +28 -2
  153. package/scripts/statusline.sh +20 -4
  154. package/scripts/sync-telemetry-from-prod.js +374 -0
  155. package/scripts/telemetry-analytics.js +357 -0
  156. package/scripts/thompson-sampling.js +31 -10
  157. package/scripts/thumbgate-bench.js +16 -1
  158. package/scripts/thumbgate-search.js +85 -19
  159. package/scripts/tool-contract-validator.js +76 -0
  160. package/scripts/tool-registry.js +169 -1
  161. package/scripts/trajectory-scorer.js +63 -0
  162. package/scripts/vector-store.js +45 -0
  163. package/scripts/verify-marketing-pages-deployed.js +212 -0
  164. package/scripts/visitor-journey.js +172 -0
  165. package/scripts/workflow-sentinel.js +286 -53
  166. package/scripts/workspace-evolver.js +62 -2
  167. package/src/api/server.js +2683 -319
  168. package/.claude-plugin/marketplace.json +0 -85
  169. package/adapters/chatgpt/openapi.yaml +0 -1695
  170. 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
+ };
@@ -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
+ };