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
|
@@ -58,6 +58,47 @@ function readJSONL(filePath) {
|
|
|
58
58
|
}).filter(Boolean);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// --- Self-Harness stage 3: regression-gated promotion -----------------------
|
|
62
|
+
// Inspired by "Self-Harness: Harnesses That Improve Themselves" (arXiv 2606.09498).
|
|
63
|
+
// Stages 1-2 (weakness mining -> rule extraction) already exist via lesson
|
|
64
|
+
// inference + this promoter. Stage 3 — accept a harness change only after
|
|
65
|
+
// regression-testing it does not degrade behavior — was missing: a noisy 3x
|
|
66
|
+
// capture could hard-block an over-broad pattern with no check that it wouldn't
|
|
67
|
+
// have wrongly blocked actions that were previously ALLOWED. This replays a
|
|
68
|
+
// candidate BLOCK rule against the audit trail's prior `allow` decisions; if it
|
|
69
|
+
// would have blocked safe actions, the caller quarantines it to `warn` instead.
|
|
70
|
+
const REGRESSION_FALSE_BLOCK_LIMIT = 0; // any prior safe action it would block => quarantine
|
|
71
|
+
|
|
72
|
+
function getAuditTrailPath() {
|
|
73
|
+
return path.join(path.dirname(getFeedbackLogPath()), 'audit-trail.jsonl');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Returns { falseBlocks, allowSampleSize } or null when there is no history /
|
|
77
|
+
// matcher available — in which case the caller promotes as usual (fail-open to
|
|
78
|
+
// existing behavior, since regression gating is an enhancement, not a hard gate).
|
|
79
|
+
function regressionCheck(gate, options = {}) {
|
|
80
|
+
const auditPath = options.auditTrailPath || getAuditTrailPath();
|
|
81
|
+
const entries = readJSONL(auditPath);
|
|
82
|
+
if (!entries.length) return null;
|
|
83
|
+
// Lazy-require to avoid the gates-engine <-> auto-promote-gates require cycle.
|
|
84
|
+
let matchesGate;
|
|
85
|
+
try { ({ matchesGate } = require('./gates-engine')); } catch { return null; }
|
|
86
|
+
if (typeof matchesGate !== 'function') return null;
|
|
87
|
+
const allowed = entries.filter((e) => e && e.decision === 'allow' && e.toolName);
|
|
88
|
+
if (!allowed.length) return null;
|
|
89
|
+
let falseBlocks = 0;
|
|
90
|
+
for (const e of allowed) {
|
|
91
|
+
try {
|
|
92
|
+
if (matchesGate(gate, e.toolName, e.toolInput || {})) falseBlocks += 1;
|
|
93
|
+
} catch { /* a bad pattern/entry never counts as a false block */ }
|
|
94
|
+
}
|
|
95
|
+
return { falseBlocks, allowSampleSize: allowed.length };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function safeRegressionCheck(gate, options) {
|
|
99
|
+
try { return regressionCheck(gate, options); } catch { return null; }
|
|
100
|
+
}
|
|
101
|
+
|
|
61
102
|
function loadAutoGates() {
|
|
62
103
|
const autoGatesPath = getAutoGatesPath();
|
|
63
104
|
if (!fs.existsSync(autoGatesPath)) {
|
|
@@ -358,9 +399,16 @@ function promote(feedbackLogPath, options) {
|
|
|
358
399
|
const existing = data.gates[existingIdx];
|
|
359
400
|
const newAction = group.count >= BLOCK_THRESHOLD ? 'block' : 'warn';
|
|
360
401
|
if (existing.action !== newAction && newAction === 'block') {
|
|
361
|
-
//
|
|
362
|
-
|
|
363
|
-
|
|
402
|
+
// Self-Harness stage 3: regression-test before upgrading warn -> block.
|
|
403
|
+
const regression = opts.skipRegression ? null : safeRegressionCheck(buildGateRule(group, 'block'), opts);
|
|
404
|
+
if (regression && regression.falseBlocks > REGRESSION_FALSE_BLOCK_LIMIT) {
|
|
405
|
+
// Would block prior safe actions — hold at warn instead of upgrading.
|
|
406
|
+
promotions.push({ type: 'upgrade-quarantined', gateId, from: existing.action, occurrences: group.count, falseBlocks: regression.falseBlocks });
|
|
407
|
+
} else {
|
|
408
|
+
// Upgrade from warn to block
|
|
409
|
+
data.gates[existingIdx] = { ...existing, action: 'block', severity: 'critical', occurrences: group.count, upgradedAt: new Date().toISOString() };
|
|
410
|
+
promotions.push({ type: 'upgrade', gateId, from: existing.action, to: 'block', occurrences: group.count });
|
|
411
|
+
}
|
|
364
412
|
}
|
|
365
413
|
// Update occurrence count even if no action change
|
|
366
414
|
data.gates[existingIdx].occurrences = group.count;
|
|
@@ -370,6 +418,20 @@ function promote(feedbackLogPath, options) {
|
|
|
370
418
|
// New gate — respect explicit gateAction override (e.g. 'approve' for human-approval rules)
|
|
371
419
|
const gate = buildGateRule(group, opts.gateAction);
|
|
372
420
|
|
|
421
|
+
// Self-Harness stage 3: before a feedback rule goes live as a hard block,
|
|
422
|
+
// regression-test it against prior allowed actions. If it would have blocked
|
|
423
|
+
// safe actions, quarantine it to `warn` instead of `block`.
|
|
424
|
+
let regression = null;
|
|
425
|
+
if (gate.action === 'block' && !opts.gateAction && !opts.skipRegression) {
|
|
426
|
+
regression = safeRegressionCheck(gate, opts);
|
|
427
|
+
if (regression && regression.falseBlocks > REGRESSION_FALSE_BLOCK_LIMIT) {
|
|
428
|
+
gate.action = 'warn';
|
|
429
|
+
gate.severity = 'medium';
|
|
430
|
+
gate.quarantined = true;
|
|
431
|
+
gate.regression = regression;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
373
435
|
// Enforce max limit — rotate oldest
|
|
374
436
|
if (data.gates.length >= MAX_AUTO_GATES) {
|
|
375
437
|
const removed = data.gates.shift();
|
|
@@ -377,7 +439,13 @@ function promote(feedbackLogPath, options) {
|
|
|
377
439
|
}
|
|
378
440
|
|
|
379
441
|
data.gates.push(gate);
|
|
380
|
-
promotions.push({
|
|
442
|
+
promotions.push({
|
|
443
|
+
type: gate.quarantined ? 'new-quarantined' : 'new',
|
|
444
|
+
gateId: gate.id,
|
|
445
|
+
action: gate.action,
|
|
446
|
+
occurrences: group.count,
|
|
447
|
+
...(gate.quarantined ? { falseBlocks: regression.falseBlocks, allowSampleSize: regression.allowSampleSize } : {}),
|
|
448
|
+
});
|
|
381
449
|
}
|
|
382
450
|
|
|
383
451
|
// Log promotions
|
|
@@ -438,6 +506,9 @@ module.exports = {
|
|
|
438
506
|
groupNegativeFeedback,
|
|
439
507
|
patternToGateId,
|
|
440
508
|
buildGateRule,
|
|
509
|
+
regressionCheck,
|
|
510
|
+
getAuditTrailPath,
|
|
511
|
+
REGRESSION_FALSE_BLOCK_LIMIT,
|
|
441
512
|
extractPatternKey,
|
|
442
513
|
normalizeCommandSignature,
|
|
443
514
|
isNegative,
|
package/scripts/billing.js
CHANGED
|
@@ -139,6 +139,10 @@ const IS_TEST = !!(
|
|
|
139
139
|
process.env.NODE_ENV === 'test'
|
|
140
140
|
);
|
|
141
141
|
|
|
142
|
+
function allowUnsignedStripeWebhooks() {
|
|
143
|
+
return IS_TEST && process.env.THUMBGATE_ALLOW_UNSIGNED_STRIPE_WEBHOOKS === '1';
|
|
144
|
+
}
|
|
145
|
+
|
|
142
146
|
function shouldMergeLegacyBillingData() {
|
|
143
147
|
return process.env._TEST_INCLUDE_LEGACY_BILLING_DATA === '1'
|
|
144
148
|
|| process.env.THUMBGATE_INCLUDE_LEGACY_BILLING_DATA === '1';
|
|
@@ -2901,7 +2905,7 @@ function disableCustomerKeys(customerId) {
|
|
|
2901
2905
|
}
|
|
2902
2906
|
|
|
2903
2907
|
function verifyWebhookSignature(rawBody, signature) {
|
|
2904
|
-
if (!CONFIG.STRIPE_WEBHOOK_SECRET) return
|
|
2908
|
+
if (!CONFIG.STRIPE_WEBHOOK_SECRET) return allowUnsignedStripeWebhooks();
|
|
2905
2909
|
if (!signature || !rawBody) return false;
|
|
2906
2910
|
|
|
2907
2911
|
// Stripe signature format: t=<timestamp>,v1=<hmac>,...
|
|
@@ -2931,6 +2935,13 @@ function verifyWebhookSignature(rawBody, signature) {
|
|
|
2931
2935
|
|
|
2932
2936
|
async function handleWebhook(rawBody, signature) {
|
|
2933
2937
|
if (LOCAL_MODE()) return { handled: false, reason: 'local_mode' };
|
|
2938
|
+
if (!CONFIG.STRIPE_WEBHOOK_SECRET && !allowUnsignedStripeWebhooks()) {
|
|
2939
|
+
return {
|
|
2940
|
+
handled: false,
|
|
2941
|
+
reason: 'invalid_signature',
|
|
2942
|
+
error: 'STRIPE_WEBHOOK_SECRET is required before Stripe webhooks can be processed.',
|
|
2943
|
+
};
|
|
2944
|
+
}
|
|
2934
2945
|
let event;
|
|
2935
2946
|
try {
|
|
2936
2947
|
if (CONFIG.STRIPE_WEBHOOK_SECRET) {
|
|
@@ -5,6 +5,11 @@ const PROJECT_ROOT = path.resolve(__dirname, '..');
|
|
|
5
5
|
const DEFAULT_BUILD_METADATA_PATH = path.join(PROJECT_ROOT, 'config', 'build-metadata.json');
|
|
6
6
|
const BUILD_SHA_ENV_KEY = 'THUMBGATE_BUILD_SHA';
|
|
7
7
|
const BUILD_GENERATED_AT_ENV_KEY = 'THUMBGATE_BUILD_GENERATED_AT';
|
|
8
|
+
// Railway injects this automatically for GitHub-connected deployments: the git
|
|
9
|
+
// SHA of the commit that triggered the deploy. It is the ground truth for what
|
|
10
|
+
// code is actually live, and unlike THUMBGATE_BUILD_SHA it cannot drift (Railway
|
|
11
|
+
// sets it per deploy). https://docs.railway.com/reference/variables
|
|
12
|
+
const RAILWAY_GIT_COMMIT_SHA_ENV_KEY = 'RAILWAY_GIT_COMMIT_SHA';
|
|
8
13
|
|
|
9
14
|
function normalizeNullableText(value) {
|
|
10
15
|
if (typeof value !== 'string') {
|
|
@@ -28,6 +33,7 @@ function resolveBuildMetadata({ env = process.env, filePath } = {}) {
|
|
|
28
33
|
normalizeNullableText(env.THUMBGATE_BUILD_METADATA_PATH) ||
|
|
29
34
|
DEFAULT_BUILD_METADATA_PATH;
|
|
30
35
|
const envBuildSha = normalizeNullableText(env[BUILD_SHA_ENV_KEY]);
|
|
36
|
+
const railwayGitSha = normalizeNullableText(env[RAILWAY_GIT_COMMIT_SHA_ENV_KEY]);
|
|
31
37
|
const envGeneratedAt = normalizeNullableText(env[BUILD_GENERATED_AT_ENV_KEY]);
|
|
32
38
|
|
|
33
39
|
let fileBuildSha = null;
|
|
@@ -48,9 +54,23 @@ function resolveBuildMetadata({ env = process.env, filePath } = {}) {
|
|
|
48
54
|
};
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
// No SHA
|
|
52
|
-
//
|
|
53
|
-
//
|
|
57
|
+
// No SHA baked into the image. Prefer Railway's own per-deploy commit SHA over
|
|
58
|
+
// THUMBGATE_BUILD_SHA: the latter is set out-of-band by the deploy workflow and
|
|
59
|
+
// has drifted in prod (stuck reporting an old commit while newer code was live,
|
|
60
|
+
// because RAILWAY_SYNC_VARIABLES is off and `railway up` stamping is unreliable).
|
|
61
|
+
// RAILWAY_GIT_COMMIT_SHA is injected by Railway per deploy, so it always matches
|
|
62
|
+
// the code actually serving traffic on a GitHub-connected service.
|
|
63
|
+
if (railwayGitSha) {
|
|
64
|
+
return {
|
|
65
|
+
path: resolvedPath,
|
|
66
|
+
buildSha: railwayGitSha,
|
|
67
|
+
generatedAt: envGeneratedAt,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Last resort: the workflow-managed env var. Only trust it when an explicit SHA
|
|
72
|
+
// is set. (Previously a bare GENERATED_AT with no SHA could short-circuit and
|
|
73
|
+
// return { buildSha: null }, losing both signals; now we require the SHA.)
|
|
54
74
|
if (envBuildSha) {
|
|
55
75
|
return {
|
|
56
76
|
path: resolvedPath,
|
|
@@ -124,6 +144,7 @@ if (require.main === module) {
|
|
|
124
144
|
module.exports = {
|
|
125
145
|
BUILD_GENERATED_AT_ENV_KEY,
|
|
126
146
|
BUILD_SHA_ENV_KEY,
|
|
147
|
+
RAILWAY_GIT_COMMIT_SHA_ENV_KEY,
|
|
127
148
|
DEFAULT_BUILD_METADATA_PATH,
|
|
128
149
|
resolveBuildMetadata,
|
|
129
150
|
writeBuildMetadataFile,
|
package/scripts/cli-schema.js
CHANGED
|
@@ -94,6 +94,25 @@ const CLI_COMMANDS = [
|
|
|
94
94
|
{ name: 'remote', type: 'boolean', description: 'Fetch from hosted Railway instance' },
|
|
95
95
|
],
|
|
96
96
|
},
|
|
97
|
+
{
|
|
98
|
+
name: 'brain',
|
|
99
|
+
aliases: ['customer-brain', 'repo-brain'],
|
|
100
|
+
description: 'Scaffold, query, or build the governed customer/repo context brain',
|
|
101
|
+
group: 'discovery',
|
|
102
|
+
flags: [
|
|
103
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON' },
|
|
104
|
+
{ name: 'task', type: 'string', description: 'Task description for routed context loading' },
|
|
105
|
+
{ name: 'type', type: 'string', description: 'Memory type for remember: decision | pattern | feedback | log' },
|
|
106
|
+
{ name: 'title', type: 'string', description: 'Title for a sourced memory entry' },
|
|
107
|
+
{ name: 'content', type: 'string', description: 'Body for a sourced memory entry' },
|
|
108
|
+
{ name: 'source', type: 'string', description: 'Required provenance for factual memory writes' },
|
|
109
|
+
{ name: 'tags', type: 'string', description: 'Comma-separated memory tags' },
|
|
110
|
+
{ name: 'text', type: 'string', description: 'Text/action to check against never-do rules' },
|
|
111
|
+
{ name: 'stale-days', type: 'number', description: 'Age threshold for cleanup report (default 60)' },
|
|
112
|
+
{ name: 'write', type: 'boolean', description: 'Save to .thumbgate/BRAIN.md (versioned, deterministic)' },
|
|
113
|
+
{ name: 'limit', type: 'number', description: 'Max lessons to include (default 15)' },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
97
116
|
{
|
|
98
117
|
name: 'stats',
|
|
99
118
|
description: 'Feedback analytics — approval rate, Revenue-at-Risk, recent trend',
|
|
@@ -486,6 +505,12 @@ const CLI_COMMANDS = [
|
|
|
486
505
|
group: 'gates',
|
|
487
506
|
flags: [],
|
|
488
507
|
},
|
|
508
|
+
{
|
|
509
|
+
name: 'hermes-gate',
|
|
510
|
+
description: 'Hermes Agent pre_tool_call hook: gate runtime tool calls (incl. skill_manage) before they run',
|
|
511
|
+
group: 'gates',
|
|
512
|
+
flags: [],
|
|
513
|
+
},
|
|
489
514
|
{
|
|
490
515
|
name: 'force-gate',
|
|
491
516
|
description: 'Immediately create a blocking gate from a pattern string',
|
|
@@ -617,16 +642,7 @@ const CLI_COMMANDS = [
|
|
|
617
642
|
{ name: 'info', type: 'boolean', description: 'Show Pro feature list' },
|
|
618
643
|
],
|
|
619
644
|
},
|
|
620
|
-
|
|
621
|
-
name: 'brain',
|
|
622
|
-
description: 'Build the agent-readable context brain (lessons + rules + gates + project context)',
|
|
623
|
-
group: 'ops',
|
|
624
|
-
flags: [
|
|
625
|
-
{ name: 'write', type: 'boolean', description: 'Save to .thumbgate/BRAIN.md (versioned, deterministic)' },
|
|
626
|
-
{ name: 'limit', type: 'number', description: 'Max lessons to include (default 15)' },
|
|
627
|
-
{ name: 'json', type: 'boolean', description: 'Output the structured model as JSON' },
|
|
628
|
-
],
|
|
629
|
-
},
|
|
645
|
+
|
|
630
646
|
{
|
|
631
647
|
name: 'workflow',
|
|
632
648
|
aliases: ['swarm'],
|
|
@@ -640,6 +656,22 @@ const CLI_COMMANDS = [
|
|
|
640
656
|
{ name: 'json', type: 'boolean', description: 'Output results as JSON' },
|
|
641
657
|
],
|
|
642
658
|
},
|
|
659
|
+
{
|
|
660
|
+
name: 'check-update',
|
|
661
|
+
aliases: ['upgrade-check'],
|
|
662
|
+
description: 'Check for newer versions of ThumbGate from npm or GitHub',
|
|
663
|
+
group: 'ops',
|
|
664
|
+
flags: [
|
|
665
|
+
{ name: 'json', type: 'boolean', description: 'Output results as JSON' },
|
|
666
|
+
],
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
name: 'self-update',
|
|
670
|
+
aliases: ['upgrade-cli'],
|
|
671
|
+
description: 'Automatically install the latest version of ThumbGate globally',
|
|
672
|
+
group: 'ops',
|
|
673
|
+
flags: [],
|
|
674
|
+
},
|
|
643
675
|
];
|
|
644
676
|
|
|
645
677
|
/**
|
|
@@ -129,6 +129,45 @@ async function retrieveVectorContext(question, opts = {}) {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// Live numeric snapshot from gate-stats + feedback analyzer. Always injected
|
|
133
|
+
// (~120 tokens) so the LLM can answer count/quantity questions like
|
|
134
|
+
// "how many were blocked today" without hallucinating.
|
|
135
|
+
function retrieveMetricsContext() {
|
|
136
|
+
const snapshot = {};
|
|
137
|
+
try {
|
|
138
|
+
const gs = require(path.join(__dirname, 'gate-stats'));
|
|
139
|
+
const s = gs.calculateStats();
|
|
140
|
+
snapshot.gates = {
|
|
141
|
+
total: s.totalGates,
|
|
142
|
+
blockRules: s.blockGates,
|
|
143
|
+
warnRules: s.warnGates,
|
|
144
|
+
totalBlockedEvents: s.totalBlocked,
|
|
145
|
+
totalWarnedEvents: s.totalWarned,
|
|
146
|
+
estimatedHoursSaved: s.estimatedHoursSaved,
|
|
147
|
+
topBlockedTrigger: s.topBlocked?.trigger || null,
|
|
148
|
+
topBlockedOccurrences: s.topBlocked?.occurrences || 0,
|
|
149
|
+
};
|
|
150
|
+
} catch (err) {
|
|
151
|
+
debugChatFallback('gate-stats unavailable', err);
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const fl = require(path.join(__dirname, 'feedback-loop'));
|
|
155
|
+
const a = fl.analyzeFeedback();
|
|
156
|
+
snapshot.feedback = {
|
|
157
|
+
total: a.total,
|
|
158
|
+
positive: a.totalPositive,
|
|
159
|
+
negative: a.totalNegative,
|
|
160
|
+
approvalRate: a.approvalRate,
|
|
161
|
+
last7d: a.windows?.['7d'],
|
|
162
|
+
last30d: a.windows?.['30d'],
|
|
163
|
+
trend: a.trend,
|
|
164
|
+
};
|
|
165
|
+
} catch (err) {
|
|
166
|
+
debugChatFallback('feedback-loop analyze unavailable', err);
|
|
167
|
+
}
|
|
168
|
+
return snapshot;
|
|
169
|
+
}
|
|
170
|
+
|
|
132
171
|
// Retrieve relevant stored lessons and optional raw feedback vector matches.
|
|
133
172
|
async function retrieveContext(question, opts = {}) {
|
|
134
173
|
const lessons = retrieveLessonContext(question, opts);
|
|
@@ -137,7 +176,7 @@ async function retrieveContext(question, opts = {}) {
|
|
|
137
176
|
}
|
|
138
177
|
|
|
139
178
|
// Build a grounded RAG prompt. Pure function (testable).
|
|
140
|
-
function buildChatPrompt(question, lessons) {
|
|
179
|
+
function buildChatPrompt(question, lessons, metrics) {
|
|
141
180
|
const q = String(question || '').slice(0, MAX_QUESTION_CHARS).trim();
|
|
142
181
|
const context = (lessons || []).map((l, i) => {
|
|
143
182
|
const mark = /pos|up/i.test(l.signal) ? 'WORKED' : (/neg|down/i.test(l.signal) ? 'MISTAKE' : 'NOTE');
|
|
@@ -145,14 +184,19 @@ function buildChatPrompt(question, lessons) {
|
|
|
145
184
|
return `(${i + 1}) [${mark}] ${l.title || ''}${tags}\n ${l.content}`;
|
|
146
185
|
}).join('\n');
|
|
147
186
|
|
|
187
|
+
const metricsBlock = metrics && Object.keys(metrics).length
|
|
188
|
+
? `\n=== Live numeric snapshot (your data, current) ===\n${JSON.stringify(metrics, null, 2)}\n`
|
|
189
|
+
: '';
|
|
190
|
+
|
|
148
191
|
const system = [
|
|
149
192
|
'You are ThumbGate\'s "chat with your data" assistant. Answer the user\'s question',
|
|
150
|
-
'using ONLY the captured lessons below (this team\'s real
|
|
151
|
-
'
|
|
152
|
-
'
|
|
193
|
+
'using ONLY the captured lessons and the live numeric snapshot below (this team\'s real data).',
|
|
194
|
+
'For count/quantity questions ("how many X", "what\'s our rate"), use the numeric snapshot.',
|
|
195
|
+
'For pattern/why questions, cite the lesson numbers like [1], [3].',
|
|
196
|
+
'If neither source contains the answer, say so plainly — do not invent facts.',
|
|
153
197
|
].join(' ');
|
|
154
198
|
|
|
155
|
-
return `${system}\n\n=== Captured lessons (your data) ===\n${context || '(no relevant lessons found)'}\n\n=== Question ===\n${q}`;
|
|
199
|
+
return `${system}\n\n=== Captured lessons (your data) ===\n${context || '(no relevant lessons found)'}\n${metricsBlock}\n=== Question ===\n${q}`;
|
|
156
200
|
}
|
|
157
201
|
|
|
158
202
|
// Parse the Gemini generateContent response into plain text. Pure (testable).
|
|
@@ -263,7 +307,8 @@ async function answerDataQuestion(question, opts = {}) {
|
|
|
263
307
|
}
|
|
264
308
|
|
|
265
309
|
const model = resolveModel(opts.model);
|
|
266
|
-
const
|
|
310
|
+
const metrics = retrieveMetricsContext();
|
|
311
|
+
const prompt = buildChatPrompt(q, lessons, metrics);
|
|
267
312
|
const fetchImpl = opts.fetch || globalThis.fetch;
|
|
268
313
|
const isPerplexity = apiKey && (apiKey.startsWith('pplx-') || apiKey.includes('perplexity'));
|
|
269
314
|
|
|
@@ -272,7 +317,8 @@ async function answerDataQuestion(question, opts = {}) {
|
|
|
272
317
|
if (isPerplexity) return await callPerplexityEndpoint({ apiKey, prompt, fetchImpl, sources });
|
|
273
318
|
return await callGeminiEndpoint({ apiKey, model, prompt, fetchImpl, sources });
|
|
274
319
|
} catch (err) {
|
|
275
|
-
|
|
320
|
+
const safeMessage = (err && err.message) ? String(err.message).split('\n')[0].slice(0, 100) : 'An unexpected error occurred.';
|
|
321
|
+
return { ok: false, error: 'network', message: safeMessage, sources };
|
|
276
322
|
}
|
|
277
323
|
}
|
|
278
324
|
|
package/scripts/dashboard.js
CHANGED
|
@@ -49,6 +49,10 @@ const {
|
|
|
49
49
|
readDecisionLog,
|
|
50
50
|
} = require('./decision-journal');
|
|
51
51
|
const { analyzeFeedback } = require('./feedback-loop');
|
|
52
|
+
const {
|
|
53
|
+
collectAggregateLogEntries,
|
|
54
|
+
shouldAggregateFeedback,
|
|
55
|
+
} = require('./feedback-aggregate');
|
|
52
56
|
|
|
53
57
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
54
58
|
const DEFAULT_GATES_PATH = path.join(PROJECT_ROOT, 'config', 'gates', 'default.json');
|
|
@@ -449,13 +453,9 @@ function computeGateAuditSeries(feedbackDir, options = {}) {
|
|
|
449
453
|
const dayKey = toLocalDayKey(entry.timestamp);
|
|
450
454
|
if (!dayKey) continue;
|
|
451
455
|
if (!countsByDay.has(dayKey)) {
|
|
452
|
-
countsByDay.set(dayKey, { allow: 0, deny: 0, warn: 0
|
|
453
|
-
}
|
|
454
|
-
const bucket = countsByDay.get(dayKey);
|
|
455
|
-
bucket[entry.decision] += 1;
|
|
456
|
-
if ((entry.decision === 'deny' || entry.decision === 'warn') && entry.gateId) {
|
|
457
|
-
bucket.byGate[entry.gateId] = (bucket.byGate[entry.gateId] || 0) + 1;
|
|
456
|
+
countsByDay.set(dayKey, { allow: 0, deny: 0, warn: 0 });
|
|
458
457
|
}
|
|
458
|
+
countsByDay.get(dayKey)[entry.decision] += 1;
|
|
459
459
|
}
|
|
460
460
|
|
|
461
461
|
const days = [];
|
|
@@ -465,7 +465,7 @@ function computeGateAuditSeries(feedbackDir, options = {}) {
|
|
|
465
465
|
const day = new Date(today);
|
|
466
466
|
day.setDate(today.getDate() - offset);
|
|
467
467
|
const dayKey = toLocalDayKey(day);
|
|
468
|
-
const record = countsByDay.get(dayKey) || { allow: 0, deny: 0, warn: 0
|
|
468
|
+
const record = countsByDay.get(dayKey) || { allow: 0, deny: 0, warn: 0 };
|
|
469
469
|
const intercepted = record.deny + record.warn;
|
|
470
470
|
const total = intercepted + record.allow;
|
|
471
471
|
const summary = {
|
|
@@ -475,7 +475,6 @@ function computeGateAuditSeries(feedbackDir, options = {}) {
|
|
|
475
475
|
warn: record.warn,
|
|
476
476
|
intercepted,
|
|
477
477
|
total,
|
|
478
|
-
byGate: record.byGate || {},
|
|
479
478
|
};
|
|
480
479
|
totals.allow += record.allow;
|
|
481
480
|
totals.deny += record.deny;
|
|
@@ -530,14 +529,7 @@ function computePreventionImpact(feedbackDir, gateStats) {
|
|
|
530
529
|
// Last auto-promotion
|
|
531
530
|
const autoGates = readJsonFile(autoGatesPath);
|
|
532
531
|
let lastPromotion = null;
|
|
533
|
-
let promotionsToday = 0;
|
|
534
|
-
let promotionIdsToday = [];
|
|
535
532
|
if (autoGates && Array.isArray(autoGates.promotionLog) && autoGates.promotionLog.length > 0) {
|
|
536
|
-
const todayKey = toLocalDayKey(new Date());
|
|
537
|
-
promotionIdsToday = autoGates.promotionLog
|
|
538
|
-
.filter((p) => p && p.timestamp && toLocalDayKey(p.timestamp) === todayKey)
|
|
539
|
-
.map((p) => p.gateId || p.id || 'unknown');
|
|
540
|
-
promotionsToday = promotionIdsToday.length;
|
|
541
533
|
const sorted = autoGates.promotionLog
|
|
542
534
|
.filter((p) => p.timestamp)
|
|
543
535
|
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
@@ -552,8 +544,6 @@ function computePreventionImpact(feedbackDir, gateStats) {
|
|
|
552
544
|
estimatedHoursSaved,
|
|
553
545
|
ruleCount,
|
|
554
546
|
lastPromotion,
|
|
555
|
-
promotionsToday,
|
|
556
|
-
promotionIdsToday: promotionIdsToday.slice(0, 5),
|
|
557
547
|
};
|
|
558
548
|
}
|
|
559
549
|
|
|
@@ -1559,6 +1549,10 @@ function resolveTeamWindowHours(analyticsWindow) {
|
|
|
1559
1549
|
// ---------------------------------------------------------------------------
|
|
1560
1550
|
|
|
1561
1551
|
function collectAllFeedbackEntries(feedbackDir) {
|
|
1552
|
+
if (shouldAggregateFeedback()) {
|
|
1553
|
+
return collectAggregateLogEntries('feedback-log.jsonl', { feedbackDir }).entries;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1562
1556
|
const entries = [];
|
|
1563
1557
|
const seen = new Set();
|
|
1564
1558
|
|
|
@@ -2096,6 +2090,7 @@ module.exports = {
|
|
|
2096
2090
|
computeObservabilityStats,
|
|
2097
2091
|
readJSONL,
|
|
2098
2092
|
readJsonFile,
|
|
2093
|
+
collectAllFeedbackEntries,
|
|
2099
2094
|
};
|
|
2100
2095
|
|
|
2101
2096
|
if (require.main === module) {
|
|
@@ -6,6 +6,7 @@ const path = require('path');
|
|
|
6
6
|
|
|
7
7
|
const { getFeedbackPaths } = require('./feedback-loop');
|
|
8
8
|
const { ensureDir } = require('./fs-utils');
|
|
9
|
+
const { redactSecretsDeep } = require('./secret-redaction');
|
|
9
10
|
|
|
10
11
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
11
12
|
const DEFAULT_PROOF_DIR = process.env.THUMBGATE_PROOF_DIR
|
|
@@ -47,7 +48,10 @@ function readJSON(filePath) {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
function writeJSONL(filePath, rows) {
|
|
50
|
-
|
|
51
|
+
// Redact secrets from every bundle row — this is the single choke point for all bundle tables
|
|
52
|
+
// (feedback_events, memory_records, sequences, attributions, proof_reports). A shared/published
|
|
53
|
+
// dataset must never ship a captured credential. See scripts/secret-redaction.js.
|
|
54
|
+
const content = rows.map((row) => JSON.stringify(redactSecretsDeep(row))).join('\n');
|
|
51
55
|
fs.writeFileSync(filePath, content ? `${content}\n` : '');
|
|
52
56
|
}
|
|
53
57
|
|
|
@@ -9,6 +9,7 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const { traceForDpoPair, aggregateTraces } = require('./code-reasoning');
|
|
11
11
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
12
|
+
const { redactSecretsDeep } = require('./secret-redaction');
|
|
12
13
|
|
|
13
14
|
const DEFAULT_LOCAL_MEMORY_LOG = path.join(resolveFeedbackDir(), 'memory-log.jsonl');
|
|
14
15
|
|
|
@@ -201,14 +202,18 @@ function exportDpoFromMemories(memories) {
|
|
|
201
202
|
},
|
|
202
203
|
}));
|
|
203
204
|
|
|
205
|
+
// Redact secrets before the pairs leave this module — they are derived from memory content and
|
|
206
|
+
// are shipped to disk here AND consumed by export-hf-dataset.js. See scripts/secret-redaction.js.
|
|
207
|
+
const redactedPairs = pairsWithTraces.map((pair) => redactSecretsDeep(pair));
|
|
208
|
+
|
|
204
209
|
return {
|
|
205
|
-
pairs:
|
|
210
|
+
pairs: redactedPairs,
|
|
206
211
|
unpairedErrors: result.unpairedErrors,
|
|
207
212
|
unpairedLearnings: result.unpairedLearnings,
|
|
208
213
|
errors,
|
|
209
214
|
learnings,
|
|
210
215
|
reasoning,
|
|
211
|
-
jsonl: toJSONL(
|
|
216
|
+
jsonl: toJSONL(redactedPairs),
|
|
212
217
|
};
|
|
213
218
|
}
|
|
214
219
|
|