thumbgate 1.22.0 → 1.23.0
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-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +1 -0
- package/adapters/chatgpt/openapi.yaml +10 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +194 -30
- package/openapi/openapi.yaml +10 -0
- package/package.json +13 -3
- package/public/agents-cost-savings.html +151 -0
- package/public/ai-malpractice-prevention.html +183 -0
- package/public/codex-plugin.html +1 -1
- package/public/index.html +3 -3
- package/public/numbers.html +2 -2
- package/public/pricing.html +1 -1
- package/scripts/cli-telemetry.js +6 -1
- package/scripts/gates-engine.js +119 -6
- package/scripts/meta-agent-loop.js +32 -0
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/rate-limiter.js +7 -1
- package/scripts/self-healing-check.js +193 -0
- package/scripts/silent-failure-cluster.js +512 -0
- package/scripts/telemetry-analytics.js +38 -0
- package/src/api/server.js +252 -36
|
@@ -282,6 +282,8 @@ function buildPromotedGate(candidate, metrics, runId) {
|
|
|
282
282
|
occurrences: metrics.hits,
|
|
283
283
|
promotedAt: new Date().toISOString(),
|
|
284
284
|
source: 'meta-agent',
|
|
285
|
+
// origin distinguishes silent-failure-clustered candidates from feedback-derived ones
|
|
286
|
+
origin: candidate.origin || 'user-feedback',
|
|
285
287
|
runId,
|
|
286
288
|
score: parseFloat(metrics.score.toFixed(3)),
|
|
287
289
|
hitRate: parseFloat(metrics.hitRate.toFixed(3)),
|
|
@@ -371,6 +373,34 @@ async function runMetaAgentLoop({ dryRun = false, verbose = false } = {}) {
|
|
|
371
373
|
candidates = generateCandidatesHeuristic(failures, blockPatterns);
|
|
372
374
|
}
|
|
373
375
|
|
|
376
|
+
// Tag existing-pipeline candidates with their origin so downstream precision
|
|
377
|
+
// measurement (silentFailureDerivedGates vs user-feedback-derived) is possible.
|
|
378
|
+
candidates = candidates.map((c) => (c.origin ? c : { ...c, origin: 'user-feedback' }));
|
|
379
|
+
|
|
380
|
+
// Step 3b: Silent-failure clustering — behind THUMBGATE_SILENT_FAILURE_CLUSTERING=1.
|
|
381
|
+
// Candidates flow through the SAME scoring / fp-rate eval below; we do not
|
|
382
|
+
// bypass any guardrail. Off by default to preserve existing behavior.
|
|
383
|
+
let silentFailureStats = null;
|
|
384
|
+
if (process.env.THUMBGATE_SILENT_FAILURE_CLUSTERING === '1') {
|
|
385
|
+
try {
|
|
386
|
+
const { generateSilentFailureCandidates } = require('./silent-failure-cluster');
|
|
387
|
+
const sfResult = generateSilentFailureCandidates({ feedbackLogPath });
|
|
388
|
+
silentFailureStats = sfResult.stats;
|
|
389
|
+
if (sfResult.candidates && sfResult.candidates.length > 0) {
|
|
390
|
+
candidates = candidates.concat(sfResult.candidates);
|
|
391
|
+
}
|
|
392
|
+
if (verbose) {
|
|
393
|
+
process.stdout.write(
|
|
394
|
+
`[meta-agent] silent-failure-cluster: candidates=${sfResult.candidates.length} `
|
|
395
|
+
+ `failed=${sfResult.stats.failedCalls} clusters=${sfResult.stats.clusters} `
|
|
396
|
+
+ `skipped=${sfResult.stats.skippedReason || 'none'}\n`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
} catch (err) {
|
|
400
|
+
if (verbose) process.stdout.write(`[meta-agent] silent-failure-cluster failed (non-fatal): ${err.message}\n`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
374
404
|
if (verbose) {
|
|
375
405
|
process.stdout.write(`[meta-agent] candidates generated: ${candidates.length} (mode=${analysisMode})\n`);
|
|
376
406
|
}
|
|
@@ -507,6 +537,8 @@ async function runMetaAgentLoop({ dryRun = false, verbose = false } = {}) {
|
|
|
507
537
|
skipped: evolutionResult.skipped || false,
|
|
508
538
|
}
|
|
509
539
|
: null,
|
|
540
|
+
silentFailureCluster: silentFailureStats,
|
|
541
|
+
silentFailureDerivedGates: promotedGates.filter((g) => g.origin === 'silent-failure-cluster').length,
|
|
510
542
|
};
|
|
511
543
|
|
|
512
544
|
if (!dryRun) {
|
|
@@ -16,7 +16,7 @@ const CREATOR_SYNTHETIC_KEY = process.env.THUMBGATE_DEV_KEY || '';
|
|
|
16
16
|
* 2. Env var: THUMBGATE_DEV_BYPASS=[set via THUMBGATE_DEV_SECRET env var]
|
|
17
17
|
* Requires a specific non-obvious value (not boolean) to prevent accidental activation.
|
|
18
18
|
*/
|
|
19
|
-
function isCreatorDev({ env = process.env, homeDir = os.homedir() } = {}) {
|
|
19
|
+
function isCreatorDev({ env = process.env, homeDir = env.HOME || env.USERPROFILE || os.homedir() } = {}) {
|
|
20
20
|
// Layer 1: env var with specific value
|
|
21
21
|
if (CREATOR_BYPASS_VALUE && String(env[CREATOR_BYPASS_ENV] || '') === CREATOR_BYPASS_VALUE) {
|
|
22
22
|
return true;
|
|
@@ -37,7 +37,7 @@ function isCreatorDev({ env = process.env, homeDir = os.homedir() } = {}) {
|
|
|
37
37
|
* with any non-empty bypass value. No env var needed — just the config file.
|
|
38
38
|
* Used by the server to skip auth on localhost during local development.
|
|
39
39
|
*/
|
|
40
|
-
function hasDevOverride(homeDir = os.homedir()) {
|
|
40
|
+
function hasDevOverride(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
|
|
41
41
|
// Disabled during test runs to avoid interfering with auth assertions
|
|
42
42
|
if (process.env.NODE_TEST_CONTEXT || process.env.THUMBGATE_TESTING) return false;
|
|
43
43
|
try {
|
|
@@ -47,11 +47,11 @@ function hasDevOverride(homeDir = os.homedir()) {
|
|
|
47
47
|
} catch { return false; }
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function getLicenseDir(homeDir = os.homedir()) {
|
|
50
|
+
function getLicenseDir(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
|
|
51
51
|
return path.join(homeDir, '.thumbgate');
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
function getLicensePath(homeDir = os.homedir()) {
|
|
54
|
+
function getLicensePath(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
|
|
55
55
|
return path.join(getLicenseDir(homeDir), 'license.json');
|
|
56
56
|
}
|
|
57
57
|
|
package/scripts/rate-limiter.js
CHANGED
|
@@ -29,6 +29,7 @@ const FREE_TIER_LIMITS = {
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
const FREE_TIER_MAX_GATES = 5; // 5 active prevention rules on free; Pro is unlimited
|
|
32
|
+
const FREE_TIER_DAILY_BLOCKS = 10; // 10 gate blocks/day on free; after limit, deny → warn + upgrade CTA
|
|
32
33
|
|
|
33
34
|
const UPGRADE_MESSAGE = `Pro: ${PRO_PRICE_LABEL} — unlimited rules, recall, lesson search, dashboard, and exports: ${PRO_MONTHLY_PAYMENT_LINK}\n Team: ${TEAM_PRICE_LABEL} after workflow qualification.`;
|
|
34
35
|
|
|
@@ -45,7 +46,10 @@ function getInstallAgeDays() {
|
|
|
45
46
|
try {
|
|
46
47
|
const { INSTALL_ID_PATH } = require('./cli-telemetry');
|
|
47
48
|
if (!fs.existsSync(INSTALL_ID_PATH)) return null;
|
|
48
|
-
|
|
49
|
+
// Use mtimeMs — birthtimeMs is unreliable on Linux (ext4 doesn't backdate creation time).
|
|
50
|
+
// The install-id file is written once at install, so mtime == creation time in practice.
|
|
51
|
+
const stat = fs.statSync(INSTALL_ID_PATH);
|
|
52
|
+
const created = stat.mtimeMs || stat.birthtimeMs;
|
|
49
53
|
if (!Number.isFinite(created) || created <= 0) return null;
|
|
50
54
|
return (Date.now() - created) / (1000 * 60 * 60 * 24);
|
|
51
55
|
} catch (_) {
|
|
@@ -211,6 +215,7 @@ function getUsage(action, authContext) {
|
|
|
211
215
|
module.exports = {
|
|
212
216
|
checkLimit,
|
|
213
217
|
getUsage,
|
|
218
|
+
getInstallAgeDays,
|
|
214
219
|
isProTier,
|
|
215
220
|
isInTrialPeriod,
|
|
216
221
|
trialDaysRemaining,
|
|
@@ -219,6 +224,7 @@ module.exports = {
|
|
|
219
224
|
todayKey,
|
|
220
225
|
FREE_TIER_LIMITS,
|
|
221
226
|
FREE_TIER_MAX_GATES,
|
|
227
|
+
FREE_TIER_DAILY_BLOCKS,
|
|
222
228
|
TRIAL_DAYS,
|
|
223
229
|
UPGRADE_MESSAGE,
|
|
224
230
|
PAYWALL_MESSAGES,
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { spawnSync } = require('node:child_process');
|
|
6
|
+
const { diagnoseFailure } = require('./failure-diagnostics');
|
|
7
|
+
const { appendDiagnosticRecord } = require('./feedback-loop');
|
|
8
|
+
|
|
9
|
+
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
10
|
+
const DEFAULT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
|
11
|
+
const DEFAULT_TESTS_TIMEOUT_MS = Number.parseInt(
|
|
12
|
+
process.env.THUMBGATE_SELF_HEAL_TEST_TIMEOUT_MS || '',
|
|
13
|
+
10,
|
|
14
|
+
) || 60 * 60_000;
|
|
15
|
+
|
|
16
|
+
const DEFAULT_CHECKS = [
|
|
17
|
+
{ name: 'budget_status', command: ['npm', 'run', 'budget:status'], timeoutMs: 60_000 },
|
|
18
|
+
{ name: 'tests', command: ['npm', 'test'], timeoutMs: DEFAULT_TESTS_TIMEOUT_MS },
|
|
19
|
+
{ name: 'prove_adapters', command: ['npm', 'run', 'prove:adapters'], timeoutMs: 10 * 60_000, useTempProofDir: true },
|
|
20
|
+
{ name: 'prove_automation', command: ['npm', 'run', 'prove:automation'], timeoutMs: 10 * 60_000, useTempProofDir: true },
|
|
21
|
+
{ name: 'prove_data_pipeline', command: ['npm', 'run', 'prove:data-pipeline'], timeoutMs: 10 * 60_000, useTempProofDir: true },
|
|
22
|
+
{ name: 'prove_tessl', command: ['npm', 'run', 'prove:tessl'], timeoutMs: 10 * 60_000, useTempProofDir: true },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function runCommand(command, {
|
|
26
|
+
cwd = PROJECT_ROOT,
|
|
27
|
+
timeoutMs = 5 * 60_000,
|
|
28
|
+
env = process.env,
|
|
29
|
+
maxBufferBytes = DEFAULT_MAX_BUFFER_BYTES,
|
|
30
|
+
} = {}) {
|
|
31
|
+
const [cmd, ...args] = command;
|
|
32
|
+
const started = Date.now();
|
|
33
|
+
const result = spawnSync(cmd, args, {
|
|
34
|
+
cwd,
|
|
35
|
+
env,
|
|
36
|
+
encoding: 'utf-8',
|
|
37
|
+
timeout: timeoutMs,
|
|
38
|
+
maxBuffer: maxBufferBytes,
|
|
39
|
+
shell: false,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const durationMs = Date.now() - started;
|
|
43
|
+
const status = Number.isInteger(result.status) ? result.status : 1;
|
|
44
|
+
return {
|
|
45
|
+
exitCode: status,
|
|
46
|
+
durationMs,
|
|
47
|
+
stdout: result.stdout || '',
|
|
48
|
+
stderr: result.stderr || '',
|
|
49
|
+
error: result.error ? result.error.message : null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createCheckEnvironment(check) {
|
|
54
|
+
const environment = { ...process.env };
|
|
55
|
+
let cleanup = null;
|
|
56
|
+
|
|
57
|
+
if (check.useTempProofDir) {
|
|
58
|
+
const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), `thumbgate-${check.name}-`));
|
|
59
|
+
environment.THUMBGATE_PROOF_DIR = proofDir;
|
|
60
|
+
if (check.name === 'prove_automation') {
|
|
61
|
+
environment.THUMBGATE_AUTOMATION_PROOF_DIR = proofDir;
|
|
62
|
+
}
|
|
63
|
+
cleanup = () => {
|
|
64
|
+
fs.rmSync(proofDir, { recursive: true, force: true });
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { env: environment, cleanup };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function collectHealthReport({
|
|
72
|
+
checks = DEFAULT_CHECKS,
|
|
73
|
+
runner = runCommand,
|
|
74
|
+
cwd = PROJECT_ROOT,
|
|
75
|
+
persistDiagnostics = false,
|
|
76
|
+
} = {}) {
|
|
77
|
+
const startedAt = new Date();
|
|
78
|
+
const results = checks.map((check) => {
|
|
79
|
+
const { env, cleanup } = createCheckEnvironment(check);
|
|
80
|
+
let run;
|
|
81
|
+
try {
|
|
82
|
+
run = runner(check.command, { cwd, timeoutMs: check.timeoutMs, env });
|
|
83
|
+
} finally {
|
|
84
|
+
if (cleanup) {
|
|
85
|
+
cleanup();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const diagnosis = run.exitCode === 0
|
|
89
|
+
? null
|
|
90
|
+
: diagnoseFailure({
|
|
91
|
+
step: check.name,
|
|
92
|
+
context: check.command.join(' '),
|
|
93
|
+
healthCheck: {
|
|
94
|
+
name: check.name,
|
|
95
|
+
exitCode: run.exitCode,
|
|
96
|
+
status: 'unhealthy',
|
|
97
|
+
outputTail: `${run.stdout}\n${run.stderr}`.trim().slice(-2000),
|
|
98
|
+
},
|
|
99
|
+
exitCode: run.exitCode,
|
|
100
|
+
error: run.error,
|
|
101
|
+
output: `${run.stdout}\n${run.stderr}`.trim(),
|
|
102
|
+
});
|
|
103
|
+
const persistedDiagnosis = persistDiagnostics && diagnosis
|
|
104
|
+
? appendDiagnosticRecord({
|
|
105
|
+
source: 'self_heal_check',
|
|
106
|
+
step: check.name,
|
|
107
|
+
context: check.command.join(' '),
|
|
108
|
+
diagnosis,
|
|
109
|
+
metadata: {
|
|
110
|
+
command: check.command.join(' '),
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
: null;
|
|
114
|
+
return {
|
|
115
|
+
name: check.name,
|
|
116
|
+
command: check.command.join(' '),
|
|
117
|
+
status: run.exitCode === 0 ? 'healthy' : 'unhealthy',
|
|
118
|
+
exitCode: run.exitCode,
|
|
119
|
+
durationMs: run.durationMs,
|
|
120
|
+
error: run.error,
|
|
121
|
+
outputTail: `${run.stdout}\n${run.stderr}`.trim().slice(-2000),
|
|
122
|
+
diagnosis,
|
|
123
|
+
persistedDiagnosis,
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const healthyCount = results.filter((x) => x.status === 'healthy').length;
|
|
128
|
+
const unhealthyCount = results.length - healthyCount;
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
generatedAt: startedAt.toISOString(),
|
|
132
|
+
durationMs: Date.now() - startedAt.getTime(),
|
|
133
|
+
overall_status: unhealthyCount === 0 ? 'healthy' : 'unhealthy',
|
|
134
|
+
summary: {
|
|
135
|
+
total: results.length,
|
|
136
|
+
healthy: healthyCount,
|
|
137
|
+
unhealthy: unhealthyCount,
|
|
138
|
+
},
|
|
139
|
+
checks: results,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function reportToText(report) {
|
|
144
|
+
const lines = [];
|
|
145
|
+
lines.push(`Self-Healing Health Check @ ${report.generatedAt}`);
|
|
146
|
+
lines.push(`Overall: ${report.overall_status.toUpperCase()}`);
|
|
147
|
+
lines.push(`Checks: ${report.summary.healthy}/${report.summary.total} healthy`);
|
|
148
|
+
lines.push('');
|
|
149
|
+
|
|
150
|
+
report.checks.forEach((check) => {
|
|
151
|
+
const icon = check.status === 'healthy' ? '✅' : '❌';
|
|
152
|
+
lines.push(`${icon} ${check.name} (${check.durationMs}ms)`);
|
|
153
|
+
if (check.status !== 'healthy') {
|
|
154
|
+
lines.push(` command: ${check.command}`);
|
|
155
|
+
if (check.error) lines.push(` error: ${check.error}`);
|
|
156
|
+
if (check.diagnosis && check.diagnosis.rootCauseCategory) {
|
|
157
|
+
lines.push(` diagnosis: ${check.diagnosis.rootCauseCategory}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return `${lines.join('\n')}\n`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function runCli() {
|
|
166
|
+
const args = new Set(process.argv.slice(2));
|
|
167
|
+
const emitJson = args.has('--json');
|
|
168
|
+
const noFail = args.has('--no-fail');
|
|
169
|
+
const report = collectHealthReport({ persistDiagnostics: true });
|
|
170
|
+
|
|
171
|
+
if (emitJson) {
|
|
172
|
+
console.log(JSON.stringify(report, null, 2));
|
|
173
|
+
} else {
|
|
174
|
+
process.stdout.write(reportToText(report));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!noFail && report.overall_status !== 'healthy') {
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = {
|
|
183
|
+
DEFAULT_CHECKS,
|
|
184
|
+
DEFAULT_TESTS_TIMEOUT_MS,
|
|
185
|
+
DEFAULT_MAX_BUFFER_BYTES,
|
|
186
|
+
runCommand,
|
|
187
|
+
collectHealthReport,
|
|
188
|
+
reportToText,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
if (require.main === module) {
|
|
192
|
+
runCli();
|
|
193
|
+
}
|