thumbgate 1.21.2 β 1.22.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/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +109 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +53 -0
- package/config/mcp-allowlists.json +12 -6
- package/package.json +19 -5
- package/public/agent-manager.html +1 -1
- package/public/codex-enterprise.html +123 -0
- package/public/dashboard.html +18 -5
- package/public/index.html +12 -5
- package/public/lessons.html +34 -0
- package/public/numbers.html +2 -2
- package/scripts/auto-wire-hooks.js +14 -0
- package/scripts/build-metadata.js +32 -13
- package/scripts/gate-stats.js +89 -0
- package/scripts/gates-engine.js +14 -0
- package/scripts/hook-runtime.js +9 -3
- package/scripts/tool-registry.js +18 -0
- package/scripts/workflow-sentinel.js +6 -1
- package/src/api/server.js +59 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thumbgate-marketplace",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"owner": {
|
|
5
5
|
"name": "Igor Ganapolsky",
|
|
6
6
|
"email": "ig5973700@gmail.com"
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"source": "npm",
|
|
15
15
|
"package": "thumbgate"
|
|
16
16
|
},
|
|
17
|
-
"version": "1.
|
|
17
|
+
"version": "1.22.0",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Igor Ganapolsky",
|
|
20
20
|
"email": "ig5973700@gmail.com",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thumbgate",
|
|
3
3
|
"description": "One π becomes a hard rule the agent cannot bypass. Captures thumbs-down feedback, distills it into PreToolUse Pre-Action Checks, enforced across every future Claude Code session.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.22.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Igor Ganapolsky",
|
|
7
7
|
"email": "ig5973700@gmail.com",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thumbgate",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "ThumbGate β ππ feedback that teaches your AI agent. Thumbs down a mistake, it never happens again.",
|
|
5
5
|
"homepage": "https://thumbgate-production.up.railway.app",
|
|
6
6
|
"transport": "stdio",
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"mcpServers": {
|
|
3
3
|
"thumbgate": {
|
|
4
4
|
"command": "npx",
|
|
5
|
-
"args": ["--yes", "--package", "thumbgate@1.
|
|
5
|
+
"args": ["--yes", "--package", "thumbgate@1.22.0", "thumbgate", "serve"]
|
|
6
6
|
}
|
|
7
7
|
},
|
|
8
8
|
"hooks": {
|
|
9
9
|
"preToolUse": {
|
|
10
10
|
"command": "npx",
|
|
11
|
-
"args": ["--yes", "--package", "thumbgate@1.
|
|
11
|
+
"args": ["--yes", "--package", "thumbgate@1.22.0", "thumbgate", "gate-check"]
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -216,7 +216,7 @@ const {
|
|
|
216
216
|
finalizeSession: finalizeFeedbackSession,
|
|
217
217
|
} = require('../../scripts/feedback-session');
|
|
218
218
|
|
|
219
|
-
const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.
|
|
219
|
+
const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.22.0' };
|
|
220
220
|
const COMMERCE_CATEGORIES = [
|
|
221
221
|
'product_recommendation',
|
|
222
222
|
'brand_compliance',
|
|
@@ -368,6 +368,111 @@ function buildRecallResponse(args = {}) {
|
|
|
368
368
|
return toTextResult(text);
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
+
function buildSuggestFixResponse(args = {}) {
|
|
372
|
+
const context = String(args.context || '').trim();
|
|
373
|
+
const rawLimit = Number(args.limit);
|
|
374
|
+
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, 5) : 3;
|
|
375
|
+
|
|
376
|
+
// If no context provided, return generic suggestion
|
|
377
|
+
if (!context) {
|
|
378
|
+
return toTextResult({
|
|
379
|
+
suggestions: [
|
|
380
|
+
{
|
|
381
|
+
action: 'Capture feedback about what went wrong so ThumbGate can learn and prevent recurrence.',
|
|
382
|
+
source: 'generic',
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
query: '',
|
|
386
|
+
totalFound: 0,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Search lessons via lesson-search module
|
|
391
|
+
const lessonModule = loadPrivateMcpModule('lessonSearch');
|
|
392
|
+
let lessonActions = [];
|
|
393
|
+
if (lessonModule) {
|
|
394
|
+
try {
|
|
395
|
+
const searchResult = lessonModule.searchLessons(context, { limit: 10 });
|
|
396
|
+
const results = Array.isArray(searchResult && searchResult.results) ? searchResult.results : [];
|
|
397
|
+
for (const result of results) {
|
|
398
|
+
const correctiveActions = (result.systemResponse && Array.isArray(result.systemResponse.correctiveActions))
|
|
399
|
+
? result.systemResponse.correctiveActions
|
|
400
|
+
: [];
|
|
401
|
+
for (const action of correctiveActions) {
|
|
402
|
+
const text = String(action.text || '').trim();
|
|
403
|
+
if (text) {
|
|
404
|
+
lessonActions.push({
|
|
405
|
+
action: text,
|
|
406
|
+
source: action.source || `lesson:${result.id || 'unknown'}`,
|
|
407
|
+
score: result.score || 0,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Also pick up lesson-level howToAvoid / actionNeeded when no explicit correctiveActions
|
|
412
|
+
if (correctiveActions.length === 0 && result.lesson) {
|
|
413
|
+
const text = result.lesson.howToAvoid || result.lesson.actionNeeded || '';
|
|
414
|
+
if (text) {
|
|
415
|
+
lessonActions.push({
|
|
416
|
+
action: String(text).trim(),
|
|
417
|
+
source: `lesson:${result.id || 'unknown'}`,
|
|
418
|
+
score: result.score || 0,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
} catch {
|
|
424
|
+
// lesson search failure is non-fatal
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Search prevention rules directly via lesson-search module's helper
|
|
429
|
+
let ruleActions = [];
|
|
430
|
+
try {
|
|
431
|
+
const { readPreventionRuleMatches } = require('../../scripts/lesson-search');
|
|
432
|
+
const ruleMatches = readPreventionRuleMatches(context, limit);
|
|
433
|
+
for (const rule of ruleMatches) {
|
|
434
|
+
const text = rule.summary || rule.title || '';
|
|
435
|
+
if (text) {
|
|
436
|
+
ruleActions.push({
|
|
437
|
+
action: String(text).trim(),
|
|
438
|
+
source: `rule:${String(rule.title || 'prevention_rules').trim()}`,
|
|
439
|
+
score: rule.score || 0,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
} catch {
|
|
444
|
+
// rule search failure is non-fatal
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Merge, deduplicate, sort by score, and take top `limit`
|
|
448
|
+
const seen = new Set();
|
|
449
|
+
const all = [...lessonActions, ...ruleActions]
|
|
450
|
+
.filter((item) => {
|
|
451
|
+
if (!item.action) return false;
|
|
452
|
+
const key = item.action.toLowerCase();
|
|
453
|
+
if (seen.has(key)) return false;
|
|
454
|
+
seen.add(key);
|
|
455
|
+
return true;
|
|
456
|
+
})
|
|
457
|
+
.sort((a, b) => (b.score || 0) - (a.score || 0))
|
|
458
|
+
.slice(0, limit)
|
|
459
|
+
.map(({ action, source }) => ({ action, source }));
|
|
460
|
+
|
|
461
|
+
// If nothing matched, add generic fallback
|
|
462
|
+
if (all.length === 0) {
|
|
463
|
+
all.push({
|
|
464
|
+
action: 'No matching lessons or rules found. Capture feedback via capture_feedback so ThumbGate can learn from this failure.',
|
|
465
|
+
source: 'generic',
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return toTextResult({
|
|
470
|
+
suggestions: all,
|
|
471
|
+
query: context,
|
|
472
|
+
totalFound: all.length,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
371
476
|
function buildDiagnoseFailureResponse(args = {}) {
|
|
372
477
|
let intentPlan = null;
|
|
373
478
|
const requestedProfile = args.mcpProfile || getActiveMcpProfile();
|
|
@@ -579,6 +684,8 @@ async function callToolInner(name, args) {
|
|
|
579
684
|
tags: Array.isArray(args.tags) ? args.tags : [],
|
|
580
685
|
}));
|
|
581
686
|
}
|
|
687
|
+
case 'suggest_fix':
|
|
688
|
+
return buildSuggestFixResponse(args);
|
|
582
689
|
case 'retrieve_lessons': {
|
|
583
690
|
// Cross-encoder reranking: retrieve more candidates, then rerank for precision
|
|
584
691
|
const { retrieveWithRerankingSync } = loadOptionalModule(path.join(__dirname, '../../scripts/cross-encoder-reranker'), () => ({
|
|
@@ -1330,6 +1437,7 @@ module.exports = {
|
|
|
1330
1437
|
acquireLock,
|
|
1331
1438
|
toCaptureFeedbackTextResult,
|
|
1332
1439
|
formatCorrectiveActionsReminder,
|
|
1440
|
+
buildSuggestFixResponse,
|
|
1333
1441
|
__test__: {
|
|
1334
1442
|
PRIVATE_MCP_MODULES,
|
|
1335
1443
|
loadPrivateMcpModule,
|
package/bin/cli.js
CHANGED
|
@@ -878,7 +878,14 @@ function stats() {
|
|
|
878
878
|
const { analyzeFeedback } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
|
|
879
879
|
const data = analyzeFeedback();
|
|
880
880
|
|
|
881
|
+
// Gate enforcement stats β runtime intercepts + configured gates
|
|
882
|
+
let gateData = { blocked: 0, warned: 0, passed: 0, byGate: {} };
|
|
883
|
+
try { gateData = require(path.join(PKG_ROOT, 'scripts', 'gates-engine')).loadStats(); } catch {}
|
|
884
|
+
let gateConfigData = { totalGates: 0, autoPromotedGates: 0, estimatedHoursSaved: '0.0', topBlocked: null, firstTimeFixRate: null };
|
|
885
|
+
try { gateConfigData = require(path.join(PKG_ROOT, 'scripts', 'gate-stats')).calculateStats(); } catch {}
|
|
886
|
+
|
|
881
887
|
const avgCostOfMistake = 2.50;
|
|
888
|
+
const totalInterceptions = gateData.blocked + gateData.warned;
|
|
882
889
|
const payload = {
|
|
883
890
|
total: data.total,
|
|
884
891
|
positives: data.totalPositive,
|
|
@@ -888,6 +895,13 @@ function stats() {
|
|
|
888
895
|
revenueAtRisk: Number((data.totalNegative * avgCostOfMistake).toFixed(2)),
|
|
889
896
|
topTags: data.topTags || [],
|
|
890
897
|
recentActivity: data.recentActivity || [],
|
|
898
|
+
gatesBlocked: gateData.blocked,
|
|
899
|
+
gatesWarned: gateData.warned,
|
|
900
|
+
totalGates: gateConfigData.totalGates,
|
|
901
|
+
autoPromotedGates: gateConfigData.autoPromotedGates,
|
|
902
|
+
estimatedHoursSaved: gateConfigData.estimatedHoursSaved,
|
|
903
|
+
topBlockedGate: gateConfigData.topBlocked ? gateConfigData.topBlocked.id : null,
|
|
904
|
+
firstTimeFixRate: gateConfigData.firstTimeFixRate,
|
|
891
905
|
};
|
|
892
906
|
|
|
893
907
|
if (args.json) {
|
|
@@ -901,6 +915,18 @@ function stats() {
|
|
|
901
915
|
console.log(` Approval Rate : ${payload.approvalRate}%`);
|
|
902
916
|
console.log(` Recent Trend : ${payload.recentTrend}%`);
|
|
903
917
|
|
|
918
|
+
// Gate enforcement β the high-ROI section
|
|
919
|
+
if (totalInterceptions > 0 || payload.totalGates > 0) {
|
|
920
|
+
console.log('\nπ‘οΈ PRE-ACTION GATE ENFORCEMENT');
|
|
921
|
+
console.log(` Actions blocked : ${payload.gatesBlocked}`);
|
|
922
|
+
console.log(` Actions warned : ${payload.gatesWarned}`);
|
|
923
|
+
console.log(` Active gates : ${payload.totalGates} (${payload.autoPromotedGates} auto-promoted)`);
|
|
924
|
+
if (payload.topBlockedGate) console.log(` Top blocker : ${payload.topBlockedGate}`);
|
|
925
|
+
console.log(` Est. time saved : ~${payload.estimatedHoursSaved} hours`);
|
|
926
|
+
const { formatFirstTimeFixRate } = require(path.join(PKG_ROOT, 'scripts', 'gate-stats'));
|
|
927
|
+
console.log(` First-time fix : ${formatFirstTimeFixRate(payload.firstTimeFixRate)}`);
|
|
928
|
+
}
|
|
929
|
+
|
|
904
930
|
if (payload.negatives > 0) {
|
|
905
931
|
console.log('\nβ οΈ REVENUE-AT-RISK ANALYSIS');
|
|
906
932
|
console.log(` Repeated Failures detected: ${payload.negatives}`);
|
|
@@ -1684,6 +1710,25 @@ function gateStats() {
|
|
|
1684
1710
|
console.log('\n' + formatStats(stats) + '\n');
|
|
1685
1711
|
}
|
|
1686
1712
|
|
|
1713
|
+
function contextPacks() {
|
|
1714
|
+
const args = parseArgs(process.argv.slice(3));
|
|
1715
|
+
const { generateAutoContextPacks } = require(path.join(PKG_ROOT, 'scripts', 'auto-context-packs'));
|
|
1716
|
+
const result = generateAutoContextPacks();
|
|
1717
|
+
if (args.json) {
|
|
1718
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
console.log(`\nGenerated ${result.packCount} context pack(s):`);
|
|
1722
|
+
for (const p of result.packs) {
|
|
1723
|
+
console.log(` [${p.type}] ${p.name}`);
|
|
1724
|
+
console.log(` -> ${p.filePath}`);
|
|
1725
|
+
}
|
|
1726
|
+
if (result.packCount === 0) {
|
|
1727
|
+
console.log(' (No failure patterns found yet β capture some feedback first.)');
|
|
1728
|
+
}
|
|
1729
|
+
console.log('');
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1687
1732
|
function harnessAudit() {
|
|
1688
1733
|
const args = parseArgs(process.argv.slice(3));
|
|
1689
1734
|
const {
|
|
@@ -2486,6 +2531,11 @@ switch (COMMAND) {
|
|
|
2486
2531
|
case 'search-lessons':
|
|
2487
2532
|
lessons();
|
|
2488
2533
|
break;
|
|
2534
|
+
case 'notes': {
|
|
2535
|
+
const { cli: notesCli } = require(path.join(PKG_ROOT, 'scripts', 'implementation-notes'));
|
|
2536
|
+
notesCli(process.argv.slice(3));
|
|
2537
|
+
break;
|
|
2538
|
+
}
|
|
2489
2539
|
case 'lesson-health':
|
|
2490
2540
|
case 'stale': {
|
|
2491
2541
|
const { initDB } = require(path.join(PKG_ROOT, 'scripts', 'lesson-db'));
|
|
@@ -2627,6 +2677,9 @@ switch (COMMAND) {
|
|
|
2627
2677
|
case 'rules':
|
|
2628
2678
|
rules();
|
|
2629
2679
|
break;
|
|
2680
|
+
case 'context-packs':
|
|
2681
|
+
contextPacks();
|
|
2682
|
+
break;
|
|
2630
2683
|
case 'harness-audit':
|
|
2631
2684
|
case 'harness':
|
|
2632
2685
|
harnessAudit();
|
|
@@ -68,7 +68,8 @@
|
|
|
68
68
|
"perplexity_search",
|
|
69
69
|
"perplexity_ask",
|
|
70
70
|
"perplexity_research",
|
|
71
|
-
"perplexity_reason"
|
|
71
|
+
"perplexity_reason",
|
|
72
|
+
"suggest_fix"
|
|
72
73
|
],
|
|
73
74
|
"essential": [
|
|
74
75
|
"capture_feedback",
|
|
@@ -104,7 +105,8 @@
|
|
|
104
105
|
"report_product_issue",
|
|
105
106
|
"require_evidence_for_claim",
|
|
106
107
|
"session_report",
|
|
107
|
-
"generate_operator_artifact"
|
|
108
|
+
"generate_operator_artifact",
|
|
109
|
+
"suggest_fix"
|
|
108
110
|
],
|
|
109
111
|
"commerce": [
|
|
110
112
|
"capture_feedback",
|
|
@@ -123,7 +125,8 @@
|
|
|
123
125
|
"workflow_sentinel",
|
|
124
126
|
"prevention_rules",
|
|
125
127
|
"feedback_stats",
|
|
126
|
-
"feedback_summary"
|
|
128
|
+
"feedback_summary",
|
|
129
|
+
"suggest_fix"
|
|
127
130
|
],
|
|
128
131
|
"readonly": [
|
|
129
132
|
"recall",
|
|
@@ -164,7 +167,8 @@
|
|
|
164
167
|
"session_report",
|
|
165
168
|
"generate_operator_artifact",
|
|
166
169
|
"perplexity_search",
|
|
167
|
-
"perplexity_ask"
|
|
170
|
+
"perplexity_ask",
|
|
171
|
+
"suggest_fix"
|
|
168
172
|
],
|
|
169
173
|
"dispatch": [
|
|
170
174
|
"recall",
|
|
@@ -204,7 +208,8 @@
|
|
|
204
208
|
"session_report",
|
|
205
209
|
"generate_operator_artifact",
|
|
206
210
|
"perplexity_search",
|
|
207
|
-
"perplexity_ask"
|
|
211
|
+
"perplexity_ask",
|
|
212
|
+
"suggest_fix"
|
|
208
213
|
],
|
|
209
214
|
"locked": [
|
|
210
215
|
"feedback_summary",
|
|
@@ -228,7 +233,8 @@
|
|
|
228
233
|
"workflow_sentinel",
|
|
229
234
|
"settings_status",
|
|
230
235
|
"native_messaging_audit",
|
|
231
|
-
"generate_operator_artifact"
|
|
236
|
+
"generate_operator_artifact",
|
|
237
|
+
"suggest_fix"
|
|
232
238
|
]
|
|
233
239
|
}
|
|
234
240
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thumbgate",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "ThumbGate self-improving agent governance: thumbs-up/down turns every mistake into a prevention rule and blocks repeat patterns. 33 pre-action checks, budget enforcement, and self-protection for Claude Code, Cursor, Codex, Gemini CLI, and Amp.",
|
|
5
5
|
"homepage": "https://thumbgate-production.up.railway.app",
|
|
6
6
|
"repository": {
|
|
@@ -223,6 +223,7 @@
|
|
|
223
223
|
"openapi/",
|
|
224
224
|
"public/agent-manager.html",
|
|
225
225
|
"public/blog.html",
|
|
226
|
+
"public/codex-enterprise.html",
|
|
226
227
|
"public/codex-plugin.html",
|
|
227
228
|
"public/compare.html",
|
|
228
229
|
"public/dashboard.html",
|
|
@@ -333,7 +334,7 @@
|
|
|
333
334
|
"social:prospect:bluesky:dry": "node scripts/social-bluesky-prospecting.js --dry-run",
|
|
334
335
|
"social:reply-publish:bluesky:dry": "node scripts/social-reply-monitor-bluesky.js --publish-approved --dry-run",
|
|
335
336
|
"test:python": "python3 -m pytest tests/*.py",
|
|
336
|
-
"test": "npm run test:python && npm run test:schema && npm run test:loop && npm run test:dpo && npm run test:kto && npm run test:api && npm run test:proof && npm run test:e2e && npm run test:rlaif && npm run test:attribution && npm run test:quality && npm run test:intelligence && npm run test:training-export && npm run test:deployment && npm run test:operational-integrity && npm run test:workflow && npm run test:billing && npm run test:cli && npm run test:watcher && npm run test:autoresearch && npm run test:ops && npm run test:session-analyzer && npm run test:tessl && npm run test:gates && npm run test:evoskill && npm run test:gates-hardening && npm run test:workers && npm run test:social-analytics && npm run test:memalign && npm run test:xmemory-lite && npm run test:filesystem-search && npm run test:zernio && npm run test:platform-limits && npm run test:post-video && npm run test:post-everywhere-instagram && npm run test:post-everywhere-channels && npm run test:post-everywhere-zernio-default && npm run test:zernio-canonical-pollers && npm run test:zernio-status && npm run test:obsidian-export && npm run test:lesson-db && npm run test:lesson-rotation && npm run test:memory-dedup && npm run test:feedback-quality && npm run test:sync-version && npm run test:check-congruence && npm run test:tool-registry && npm run test:feedback-to-rules && npm run test:memory-firewall && npm run test:memory-scope-readiness && npm run test:belief-update && npm run test:hosted-config && npm run test:operational-summary && npm run test:operational-dashboard && npm run test:operator-artifacts && npm run test:operator-key-auth && npm run test:cloudflare-sandbox && npm run test:mcp-config && npm run test:plan-gate && npm run test:pulse && npm run test:semantic-layer && npm run test:data-pipeline && npm run test:optimize-context && npm run test:principle-extractor && npm run test:analytics-window && npm run test:funnel-analytics && npm run test:experiment-tracker && npm run test:build-metadata && npm run test:context-engine && npm run test:hf-papers && npm run test:marketing-experiment && npm run test:seo-gsd && npm run test:verify-run && npm run test:export-dpo-pairs && npm run test:export-hf-dataset && npm run test:license && npm run test:bot-detector && npm run test:audit-pr-bot-contamination && npm run test:stripe-bootstrap-saas-catalog && npm run test:postinstall && npm run test:funnel-invariants && npm run test:cli-telemetry && npm run test:pro-parity && npm run test:model-tier-router && npm run test:computer-use-firewall && npm run test:skill-exporter && npm run test:statusline && npm run test:evolution && npm run test:org-dashboard && npm run test:multi-hop-recall && npm run test:synthetic-dpo && npm run test:thumbgate-skill && npm run test:learn-hub && npm run test:feedback-fallback && npm run test:metaclaw && npm run test:server-lock && npm run test:control-tower && npm run test:pii-scanner && npm run test:data-governance && npm run test:lesson-inference && npm run test:semantic-dedup && npm run test:fs-utils && npm run test:cli-schema && npm run test:explore && npm run test:lesson-reranker && npm run test:lesson-retrieval && npm run test:cross-encoder && npm run test:reflector-agent && npm run test:feedback-session && npm run test:feedback-history-distiller && npm run test:hallucination-detector && npm run test:history-distiller && npm run test:predictive-insights && npm run test:prove-predictive-insights && npm run test:statusbar-cli && npm run test:generate-instagram-card && npm run test:instagram-thumbgate-post && npm run test:publish-instagram-thumbgate && npm run test:lesson-synthesis && npm run test:lesson-canonical && npm run test:background-governance && npm run test:memory-migration && npm run test:prompt-dlp && npm run test:ephemeral-store && npm run test:agent-security && npm run test:skill-progressive && npm run test:per-step-scoring && npm run test:weekly-auto-post && npm run test:social-post-hourly && npm run test:social-quality-gate && npm run test:a2ui-engine && npm run test:gate-satisfy && npm run test:money-watcher && npm run test:budget && npm run test:quick-start && npm run test:utm && npm run test:product-feedback && npm run test:feedback-root-consolidator && npm run test:engagement-audit && npm run test:install-growth-automation && npm run test:publish-thumbgate-launch && npm run test:community-course-platform-launch-kit && npm run test:reconcile-thumbgate-campaign && npm run test:reddit-publisher && npm run test:schedule-thumbgate-campaign && npm run test:social-reply-monitor && npm run test:social-dedupe-cleanup && npm run test:sync-launch-assets && npm run test:ai-search-visibility && npm run test:perplexity && npm run test:security-scanner && npm run test:llm-client && npm run test:managed-lesson-agent && npm run test:self-distill && npm run test:meta-agent && npm run test:harness-selector && npm run test:thumbgate-bench && npm run test:seo-guides && npm run test:enforcement-loop && npm run test:cli-agent-experience && npm run test:bot-detection && npm run test:checkout-archived-product-guard && npm run test:postgres-guard && npm run test:checkout-bot-guard && npm run test:checkout-pro-confirmation-gate && npm run test:session-health && npm run test:session-episodes && npm run test:spec-gate && npm run test:decision-trace && npm run test:dashboard-insights && npm run test:telemetry-tracked-link-slug && npm run test:prompt-eval && npm run test:demo-voiceover && npm run test:gate-coherence && npm run test:gate-eval && npm run test:high-roi && npm run test:public-static-assets && npm run test:token-savings && npm run test:numbers-page && npm run test:workflow-gate-checkpoint && npm run test:lesson-export-import && npm run test:landing-page-claims && npm run test:competitive-positioning-marketing && npm run test:medium-weekly && npm run test:dashboard-deeplink-e2e && npm run test:public-package-parity && npm run test:token-savings-dashboard && npm run test:cursor-wiring && npm run test:pretooluse-injection && npm run test:recent-corrective-context && npm run test:durability-step && npm run test:mailer && npm run test:brand-assets && npm run test:enforcement-teeth && npm run test:bayes-optimal-gate && npm run test:swarm-coordinator && npm run test:session-report && npm run test:agent-reasoning-traces && npm run test:judge-reward && npm run test:llm-behavior-monitor && npm run test:prompting-os && npm run test:single-use-credential-gate && npm run test:structured-prompt-driven && npm run test:require-evidence-gate && npm run test:rule-validator && npm run test:bluesky-atproto && npm run test:social-reply-monitor-bluesky && npm run test:bluesky-delete-replies && npm run test:architect-kit-memory-bridge && npm run test:sonar-review-hotspots && npm run test:actionable-remediations && npm run test:gemini-embedding-policy && npm run test:agent-design-governance && npm run test:public-core-boundary && npm run test:hook-stop-verify-deploy && npm run test:hook-stop-anti-claim && npm run test:plausible-server-events && npm run test:activation-tracker && npm run test:unified-revenue-rollup && npm run test:conversion-rate-stats && npm run test:external-customer-audit && npm run test:telemetry-export && npm run test:stripe-checkout-diagnostic && npm run test:stripe-business-identity-probe && npm run test:revenue-observability-doctor && npm run test:public-bundle-ratchet && npm run test:stripe-payment-link-update && npm run test:ci-cd-hygiene-audit && npm run test:verify-marketing-pages-deployed && npm run test:install-email-capture",
|
|
337
|
+
"test": "npm run test:python && npm run test:schema && npm run test:loop && npm run test:dpo && npm run test:kto && npm run test:api && npm run test:proof && npm run test:e2e && npm run test:rlaif && npm run test:attribution && npm run test:quality && npm run test:intelligence && npm run test:training-export && npm run test:deployment && npm run test:operational-integrity && npm run test:workflow && npm run test:billing && npm run test:cli && npm run test:watcher && npm run test:autoresearch && npm run test:ops && npm run test:session-analyzer && npm run test:tessl && npm run test:gates && npm run test:evoskill && npm run test:gates-hardening && npm run test:workers && npm run test:social-analytics && npm run test:memalign && npm run test:xmemory-lite && npm run test:filesystem-search && npm run test:zernio && npm run test:platform-limits && npm run test:post-video && npm run test:post-everywhere-instagram && npm run test:post-everywhere-channels && npm run test:post-everywhere-zernio-default && npm run test:zernio-canonical-pollers && npm run test:zernio-status && npm run test:obsidian-export && npm run test:lesson-db && npm run test:lesson-rotation && npm run test:memory-dedup && npm run test:feedback-quality && npm run test:sync-version && npm run test:check-congruence && npm run test:tool-registry && npm run test:feedback-to-rules && npm run test:memory-firewall && npm run test:memory-scope-readiness && npm run test:belief-update && npm run test:hosted-config && npm run test:operational-summary && npm run test:operational-dashboard && npm run test:operator-artifacts && npm run test:operator-key-auth && npm run test:cloudflare-sandbox && npm run test:mcp-config && npm run test:plan-gate && npm run test:pulse && npm run test:semantic-layer && npm run test:data-pipeline && npm run test:optimize-context && npm run test:principle-extractor && npm run test:analytics-window && npm run test:funnel-analytics && npm run test:experiment-tracker && npm run test:build-metadata && npm run test:context-engine && npm run test:hf-papers && npm run test:marketing-experiment && npm run test:seo-gsd && npm run test:verify-run && npm run test:export-dpo-pairs && npm run test:export-hf-dataset && npm run test:license && npm run test:bot-detector && npm run test:audit-pr-bot-contamination && npm run test:stripe-bootstrap-saas-catalog && npm run test:postinstall && npm run test:funnel-invariants && npm run test:cli-telemetry && npm run test:pro-parity && npm run test:model-tier-router && npm run test:computer-use-firewall && npm run test:skill-exporter && npm run test:statusline && npm run test:evolution && npm run test:org-dashboard && npm run test:multi-hop-recall && npm run test:synthetic-dpo && npm run test:thumbgate-skill && npm run test:learn-hub && npm run test:feedback-fallback && npm run test:metaclaw && npm run test:server-lock && npm run test:control-tower && npm run test:pii-scanner && npm run test:data-governance && npm run test:lesson-inference && npm run test:semantic-dedup && npm run test:fs-utils && npm run test:cli-schema && npm run test:explore && npm run test:lesson-reranker && npm run test:lesson-retrieval && npm run test:cross-encoder && npm run test:reflector-agent && npm run test:feedback-session && npm run test:feedback-history-distiller && npm run test:hallucination-detector && npm run test:history-distiller && npm run test:predictive-insights && npm run test:prove-predictive-insights && npm run test:statusbar-cli && npm run test:generate-instagram-card && npm run test:instagram-thumbgate-post && npm run test:publish-instagram-thumbgate && npm run test:lesson-synthesis && npm run test:lesson-canonical && npm run test:background-governance && npm run test:memory-migration && npm run test:prompt-dlp && npm run test:ephemeral-store && npm run test:agent-security && npm run test:skill-progressive && npm run test:per-step-scoring && npm run test:weekly-auto-post && npm run test:social-post-hourly && npm run test:social-quality-gate && npm run test:a2ui-engine && npm run test:gate-satisfy && npm run test:money-watcher && npm run test:budget && npm run test:quick-start && npm run test:utm && npm run test:product-feedback && npm run test:feedback-root-consolidator && npm run test:engagement-audit && npm run test:install-growth-automation && npm run test:publish-thumbgate-launch && npm run test:community-course-platform-launch-kit && npm run test:reconcile-thumbgate-campaign && npm run test:reddit-publisher && npm run test:schedule-thumbgate-campaign && npm run test:social-reply-monitor && npm run test:social-dedupe-cleanup && npm run test:sync-launch-assets && npm run test:ai-search-visibility && npm run test:perplexity && npm run test:security-scanner && npm run test:llm-client && npm run test:managed-lesson-agent && npm run test:self-distill && npm run test:meta-agent && npm run test:harness-selector && npm run test:thumbgate-bench && npm run test:seo-guides && npm run test:enforcement-loop && npm run test:cli-agent-experience && npm run test:bot-detection && npm run test:checkout-archived-product-guard && npm run test:postgres-guard && npm run test:checkout-bot-guard && npm run test:checkout-pro-confirmation-gate && npm run test:session-health && npm run test:session-episodes && npm run test:spec-gate && npm run test:decision-trace && npm run test:dashboard-insights && npm run test:telemetry-tracked-link-slug && npm run test:prompt-eval && npm run test:demo-voiceover && npm run test:gate-coherence && npm run test:gate-eval && npm run test:high-roi && npm run test:public-static-assets && npm run test:token-savings && npm run test:numbers-page && npm run test:workflow-gate-checkpoint && npm run test:lesson-export-import && npm run test:landing-page-claims && npm run test:competitive-positioning-marketing && npm run test:medium-weekly && npm run test:dashboard-deeplink-e2e && npm run test:public-package-parity && npm run test:token-savings-dashboard && npm run test:cursor-wiring && npm run test:pretooluse-injection && npm run test:recent-corrective-context && npm run test:durability-step && npm run test:mailer && npm run test:brand-assets && npm run test:enforcement-teeth && npm run test:bayes-optimal-gate && npm run test:swarm-coordinator && npm run test:session-report && npm run test:agent-reasoning-traces && npm run test:judge-reward && npm run test:llm-behavior-monitor && npm run test:prompting-os && npm run test:single-use-credential-gate && npm run test:structured-prompt-driven && npm run test:require-evidence-gate && npm run test:rule-validator && npm run test:bluesky-atproto && npm run test:social-reply-monitor-bluesky && npm run test:bluesky-delete-replies && npm run test:architect-kit-memory-bridge && npm run test:sonar-review-hotspots && npm run test:actionable-remediations && npm run test:gemini-embedding-policy && npm run test:agent-design-governance && npm run test:public-core-boundary && npm run test:hook-stop-verify-deploy && npm run test:hook-stop-anti-claim && npm run test:plausible-server-events && npm run test:activation-tracker && npm run test:unified-revenue-rollup && npm run test:conversion-rate-stats && npm run test:external-customer-audit && npm run test:telemetry-export && npm run test:stripe-checkout-diagnostic && npm run test:stripe-business-identity-probe && npm run test:revenue-observability-doctor && npm run test:public-bundle-ratchet && npm run test:stripe-payment-link-update && npm run test:ci-cd-hygiene-audit && npm run test:verify-marketing-pages-deployed && npm run test:install-email-capture && npm run test:install-shim && npm run test:hook-runtime-subcommands && npm run test:implementation-notes",
|
|
337
338
|
"test:hook-stop-verify-deploy": "node --test tests/hook-stop-verify-deploy.test.js",
|
|
338
339
|
"test:hook-stop-anti-claim": "node --test tests/hook-stop-anti-claim.test.js",
|
|
339
340
|
"test:plausible-server-events": "node --test tests/plausible-server-events.test.js",
|
|
@@ -443,10 +444,10 @@
|
|
|
443
444
|
"test:evolution": "node --test tests/workspace-evolver.test.js",
|
|
444
445
|
"test:watcher": "node --test tests/jsonl-watcher.test.js",
|
|
445
446
|
"test:autoresearch": "node --test tests/autoresearch.test.js",
|
|
446
|
-
"test:ops": "node --test tests/adk-consolidator.test.js tests/anthropic-partner-strategy.test.js tests/auto-promote-gates.test.js tests/auto-wire-hooks.test.js tests/claude-skill.test.js tests/codegraph-context.test.js tests/commercial-signals.test.js tests/decision-journal.test.js tests/delegation-runtime.test.js tests/disagreement-mining.test.js tests/failure-diagnostics.test.js tests/gate-stats.test.js tests/git-hook-installer.test.js tests/github-billing.test.js tests/intervention-policy.test.js tests/markdown-escape.test.js tests/mcp-tools-gates.test.js tests/native-messaging-audit.test.js tests/project-bayes-e2e.test.js tests/project-bayes.test.js tests/rate-limiter.test.js tests/schedule-manager.test.js tests/session-handoff.test.js tests/skill-generator.test.js tests/smart-learning.test.js tests/spike-and-sink.test.js tests/stripe-revenue.test.js tests/stripe-webhook-route.test.js tests/stripe-webhook-rotation.test.js tests/train-from-feedback.test.js tests/workflow-hardening-sprint.test.js tests/workflow-sentinel.test.js tests/test-suite-parity.test.js tests/a2ui-engine.test.js tests/webhook-delivery.test.js",
|
|
447
|
+
"test:ops": "node --test tests/adk-consolidator.test.js tests/anthropic-partner-strategy.test.js tests/auto-promote-gates.test.js tests/auto-wire-hooks.test.js tests/claude-skill.test.js tests/codegraph-context.test.js tests/commercial-signals.test.js tests/decision-journal.test.js tests/delegation-runtime.test.js tests/disagreement-mining.test.js tests/failure-diagnostics.test.js tests/gate-stats.test.js tests/git-hook-installer.test.js tests/github-billing.test.js tests/intervention-policy.test.js tests/markdown-escape.test.js tests/mcp-tools-gates.test.js tests/native-messaging-audit.test.js tests/project-bayes-e2e.test.js tests/project-bayes.test.js tests/rate-limiter.test.js tests/schedule-manager.test.js tests/session-handoff.test.js tests/skill-generator.test.js tests/smart-learning.test.js tests/spike-and-sink.test.js tests/stripe-revenue.test.js tests/stripe-webhook-route.test.js tests/stripe-webhook-rotation.test.js tests/train-from-feedback.test.js tests/workflow-hardening-sprint.test.js tests/workflow-sentinel.test.js tests/test-suite-parity.test.js tests/a2ui-engine.test.js tests/webhook-delivery.test.js tests/auto-context-packs.test.js",
|
|
447
448
|
"test:session-analyzer": "node --test tests/session-analyzer.test.js",
|
|
448
449
|
"test:tessl": "node --test tests/tessl-export.test.js",
|
|
449
|
-
"test:gates": "node --test tests/gate-templates.test.js tests/gates-engine.test.js tests/claim-verification.test.js tests/secret-scanner.test.js tests/secret-fixture-safety.test.js tests/prompt-guard.test.js tests/audit-trail.test.js tests/profile-router.test.js tests/workflow-sentinel.test.js tests/docker-sandbox-planner.test.js",
|
|
450
|
+
"test:gates": "node --test tests/gate-templates.test.js tests/gates-engine.test.js tests/claim-verification.test.js tests/secret-scanner.test.js tests/secret-fixture-safety.test.js tests/prompt-guard.test.js tests/audit-trail.test.js tests/profile-router.test.js tests/workflow-sentinel.test.js tests/docker-sandbox-planner.test.js tests/mcp-tools-suggest-fix.test.js",
|
|
450
451
|
"test:budget": "node --test tests/budget-enforcer.test.js",
|
|
451
452
|
"test:workers": "npm --prefix workers ci && npm --prefix workers test",
|
|
452
453
|
"test:evoskill": "node --test tests/evoskill.test.js",
|
|
@@ -640,6 +641,10 @@
|
|
|
640
641
|
"test:competitive-positioning-marketing": "node --test tests/competitive-positioning-marketing.test.js tests/knowledge-graph-guardrails.test.js tests/supply-chain-guardrails.test.js",
|
|
641
642
|
"test:medium-weekly": "node --test tests/medium-weekly.test.js",
|
|
642
643
|
"test:dashboard-deeplink-e2e": "node --test tests/dashboard-deeplink-e2e.test.js",
|
|
644
|
+
"test:e2e:playwright": "playwright test",
|
|
645
|
+
"test:e2e:playwright:headed": "playwright test --headed",
|
|
646
|
+
"test:e2e:playwright:ui": "playwright test --ui",
|
|
647
|
+
"test:e2e:playwright:report": "playwright show-report",
|
|
643
648
|
"test:public-package-parity": "node --test tests/public-package-parity.test.js",
|
|
644
649
|
"prepare": "bash bin/install-hooks.sh >/dev/null 2>&1 || true",
|
|
645
650
|
"install:hooks": "bash bin/install-hooks.sh",
|
|
@@ -656,7 +661,15 @@
|
|
|
656
661
|
"test:stripe-payment-link-update": "node --test tests/stripe-payment-link-update.test.js",
|
|
657
662
|
"test:verify-marketing-pages-deployed": "node --test tests/verify-marketing-pages-deployed.test.js",
|
|
658
663
|
"verify:marketing-pages": "node scripts/verify-marketing-pages-deployed.js",
|
|
659
|
-
"test:install-email-capture": "node --test tests/install-email-capture.test.js"
|
|
664
|
+
"test:install-email-capture": "node --test tests/install-email-capture.test.js",
|
|
665
|
+
"test:install-shim": "node --test tests/install-shim.test.js",
|
|
666
|
+
"test:hook-runtime-subcommands": "node --test tests/hook-runtime-subcommands.test.js",
|
|
667
|
+
"test:implementation-notes": "node --test tests/implementation-notes.test.js",
|
|
668
|
+
"test:lessons-page-clickability": "playwright test tests/e2e/lessons-page-clickability.spec.js",
|
|
669
|
+
"test:index-page-clickability": "playwright test tests/e2e/index-page-clickability.spec.js",
|
|
670
|
+
"test:dashboard-page-clickability": "playwright test tests/e2e/dashboard-page-clickability.spec.js",
|
|
671
|
+
"test:agent-manager-page-clickability": "playwright test tests/e2e/agent-manager-page-clickability.spec.js",
|
|
672
|
+
"test:pricing-page-clickability": "playwright test tests/e2e/pricing-page-clickability.spec.js"
|
|
660
673
|
},
|
|
661
674
|
"keywords": [
|
|
662
675
|
"mcp",
|
|
@@ -731,6 +744,7 @@
|
|
|
731
744
|
"devDependencies": {
|
|
732
745
|
"@changesets/changelog-github": "^0.7.0",
|
|
733
746
|
"@changesets/cli": "^2.31.0",
|
|
747
|
+
"@playwright/test": "^1.60.0",
|
|
734
748
|
"c8": "^11.0.0",
|
|
735
749
|
"undici": "^8.2.0"
|
|
736
750
|
}
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
</tr>
|
|
77
77
|
<tr>
|
|
78
78
|
<td><strong>Plugin marketplace</strong><br>Deciding which Claude Code / Cursor / Codex plugins are blessed and which are not.</td>
|
|
79
|
-
<td>ThumbGate ships as a Claude Code plugin, a Cursor extension, a Codex plugin, and a Gemini CLI hook. One install, every supported agent. Adapter compatibility matrix kept current as runtimes change.</td>
|
|
79
|
+
<td>ThumbGate ships as a Claude Code plugin, a Cursor extension (Marketplace listing pending Cursor's review since 2026-05-19; runtime install works today via <code>npx thumbgate init --agent cursor</code>), a Codex plugin, and a Gemini CLI hook. One install, every supported agent. Adapter compatibility matrix kept current as runtimes change.</td>
|
|
80
80
|
</tr>
|
|
81
81
|
<tr>
|
|
82
82
|
<td><strong>Permissions policy</strong><br>What an agent is allowed to execute, against which surfaces, with which evidence required.</td>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>ThumbGate for Codex in the Enterprise β Governance for OpenAI Codex (Dell-Distributed or Self-Hosted)</title>
|
|
7
|
+
<script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
|
|
8
|
+
<meta name="description" content="OpenAI and Dell are distributing Codex into the enterprise. Codex in production needs a governance layer β capture every agent decision, promote repeat failures to PreToolUse gates, ship the audit trail procurement requires.">
|
|
9
|
+
<meta property="og:title" content="ThumbGate for Codex in the Enterprise">
|
|
10
|
+
<meta property="og:description" content="Dell-distributed or self-hosted, Codex agents repeat the same mistakes. ThumbGate is the governance layer underneath β capture, promote, audit.">
|
|
11
|
+
<meta property="og:type" content="article">
|
|
12
|
+
<meta property="og:image" content="https://thumbgate-production.up.railway.app/og.png">
|
|
13
|
+
<link rel="canonical" href="https://thumbgate-production.up.railway.app/codex-enterprise">
|
|
14
|
+
<script type="application/ld+json">
|
|
15
|
+
{
|
|
16
|
+
"@context": "https://schema.org",
|
|
17
|
+
"@type": "TechArticle",
|
|
18
|
+
"headline": "ThumbGate for Codex in the Enterprise",
|
|
19
|
+
"description": "Dell-distributed or self-hosted, Codex agents in production need a governance layer. ThumbGate captures every agent decision, promotes repeat failures to PreToolUse gates, and ships the audit trail enterprise procurement requires.",
|
|
20
|
+
"datePublished": "2026-05-20",
|
|
21
|
+
"dateModified": "2026-05-20",
|
|
22
|
+
"author": { "@type": "Person", "name": "Igor Ganapolsky", "url": "https://github.com/IgorGanapolsky" },
|
|
23
|
+
"publisher": { "@type": "Organization", "name": "ThumbGate", "url": "https://thumbgate-production.up.railway.app" },
|
|
24
|
+
"about": [
|
|
25
|
+
{ "@type": "Thing", "name": "OpenAI Codex" },
|
|
26
|
+
{ "@type": "Thing", "name": "Dell Codex Enterprise" },
|
|
27
|
+
{ "@type": "Thing", "name": "Agent Governance" },
|
|
28
|
+
{ "@type": "Thing", "name": "PreToolUse Gates" }
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
<style>
|
|
33
|
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
34
|
+
:root { --bg:#0a0a0b; --card:#161618; --border:#222225; --text:#e8e8ec; --muted:#8b8b94; --cyan:#22d3ee; }
|
|
35
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--text); line-height: 1.7; }
|
|
36
|
+
.container { max-width: 860px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
|
|
37
|
+
nav { padding: 1rem 2rem; border-bottom: 1px solid var(--border); display:flex; gap:1.5rem; flex-wrap:wrap; }
|
|
38
|
+
nav a { color: var(--muted); text-decoration:none; font-size:0.9rem; }
|
|
39
|
+
nav .brand { color: var(--text); font-weight:700; }
|
|
40
|
+
.pill { display:inline-block; font-size:0.75rem; letter-spacing:0.08em; text-transform:uppercase; color:var(--cyan); background:rgba(34,211,238,0.08); border:1px solid rgba(34,211,238,0.2); padding:4px 12px; border-radius:100px; margin-top:1.5rem; font-weight:600; }
|
|
41
|
+
h1 { font-size:2.2rem; line-height:1.15; margin:1rem 0 1rem; }
|
|
42
|
+
h2 { font-size:1.45rem; margin:2.2rem 0 1rem; color:var(--cyan); }
|
|
43
|
+
h3 { margin:0.6rem 0; font-size:1rem; }
|
|
44
|
+
p, li { margin-bottom:0.75rem; }
|
|
45
|
+
ul, ol { padding-left:1.25rem; }
|
|
46
|
+
.card { background: var(--card); border:1px solid var(--border); border-radius:12px; padding:1.25rem; margin:1rem 0; }
|
|
47
|
+
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:1rem; margin:1rem 0; }
|
|
48
|
+
.grid .card h3 { color:var(--cyan); }
|
|
49
|
+
.cta { display:inline-block; background:var(--cyan); color:#000; padding:0.8rem 1.2rem; border-radius:8px; text-decoration:none; font-weight:700; }
|
|
50
|
+
.secondary { color:var(--cyan); text-decoration:underline; margin-left:1rem; }
|
|
51
|
+
.quote { border-left:3px solid var(--cyan); padding:0.75rem 1rem; margin:1rem 0; color:var(--muted); font-style:italic; }
|
|
52
|
+
code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; background:#0f0f11; border:1px solid var(--border); border-radius:6px; padding:0.15rem 0.4rem; font-size:0.9rem; }
|
|
53
|
+
pre { padding:0.85rem 1rem; overflow-x:auto; }
|
|
54
|
+
.footer-links { margin-top:2.5rem; padding-top:1.25rem; border-top:1px solid var(--border); color:var(--muted); font-size:0.9rem; }
|
|
55
|
+
.footer-links a { color:var(--cyan); text-decoration:none; }
|
|
56
|
+
</style>
|
|
57
|
+
</head>
|
|
58
|
+
<body>
|
|
59
|
+
<nav>
|
|
60
|
+
<a href="/" class="brand">ThumbGate</a>
|
|
61
|
+
<a href="/guide">Guide</a>
|
|
62
|
+
<a href="/agent-manager">Agent Manager</a>
|
|
63
|
+
<a href="/codex-plugin">Codex plugin</a>
|
|
64
|
+
<a href="/dashboard">Dashboard demo</a>
|
|
65
|
+
<a href="https://github.com/IgorGanapolsky/ThumbGate" target="_blank" rel="noopener">GitHub</a>
|
|
66
|
+
</nav>
|
|
67
|
+
<div class="container">
|
|
68
|
+
<span class="pill">Codex in the Enterprise</span>
|
|
69
|
+
<h1>Codex in production needs a governance layer. Dell-distributed or self-hosted, agents repeat the same mistakes.</h1>
|
|
70
|
+
<p>OpenAI and Dell <a href="https://openai.com/index/dell-codex-enterprise-partnership/" target="_blank" rel="noopener" style="color:var(--cyan)">just announced</a> a partnership to distribute Codex into the enterprise β Dell PCs, Dell servers, and Dell's enterprise sales motion become a delivery channel for OpenAI's coding agent. Codex's addressable market jumps from individual developer install to org-wide procurement. The governance gap jumps with it: every enterprise that turns Codex on now needs a runtime layer that captures what the agent did, blocks the repeat failures, and produces the audit trail their security review will ask for.</p>
|
|
71
|
+
<p>ThumbGate already ships a <a href="/codex-plugin">Codex plugin</a>. The free CLI is real, MIT-licensed, and the gates work locally without a hosted account. This page is what that plugin maps to once Codex is no longer one developer's experiment but a procurement line item.</p>
|
|
72
|
+
|
|
73
|
+
<h2>What the governance layer ships</h2>
|
|
74
|
+
<div class="grid">
|
|
75
|
+
<div class="card">
|
|
76
|
+
<h3>Capture every agent decision as it happens</h3>
|
|
77
|
+
<p>The Thariq pattern β running implementation notes that record decisions, assumptions (marked VERIFIED or UNVERIFIED), tradeoffs, and corrections β productionized as a Codex hook. Every multi-step task gets a structured journal you can review async without re-reading the entire transcript.</p>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="card">
|
|
80
|
+
<h3>Promote repeat failures to PreToolUse gates</h3>
|
|
81
|
+
<p>When the same agent mistake shows up twice, ThumbGate distills it into a prevention rule and blocks the next attempt at the tool-call boundary β with the rule that fired in the agent's reasoning trace, so Codex chooses a safer plan instead of being told to "be more careful."</p>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="card">
|
|
84
|
+
<h3>Audit trail enterprise procurement requires</h3>
|
|
85
|
+
<p>Per-tool-call evidence, per-rule provenance, exportable for SOC 2 / ISO 27001 / EU AI Act review. The hosted dashboard rolls this up across repos so the Agent Manager role has one surface instead of N developer machines.</p>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<h2>Why this matters now</h2>
|
|
90
|
+
<p>The Dell distribution channel changes who buys Codex. The individual-developer install is opt-in; the enterprise procurement install is policy-driven. The teams approving the Codex line item will ask three questions ThumbGate is built to answer:</p>
|
|
91
|
+
<ol>
|
|
92
|
+
<li><strong>What did the agent do?</strong> β capture, with evidence, on every tool call.</li>
|
|
93
|
+
<li><strong>What did we stop it from doing?</strong> β PreToolUse gates with the rule that fired and why.</li>
|
|
94
|
+
<li><strong>How do you keep this current as Codex updates?</strong> β adapter matrix that's CI-checked against upstream.</li>
|
|
95
|
+
</ol>
|
|
96
|
+
<div class="quote">"Dell-distributed Codex into the enterprise is the moment governance moves from optional to procurement-required. The runtime that captures, blocks, and audits is the line item underneath the line item."</div>
|
|
97
|
+
|
|
98
|
+
<h2>Install</h2>
|
|
99
|
+
<p>One repo, one command:</p>
|
|
100
|
+
<pre><code>npx thumbgate init --agent codex</code></pre>
|
|
101
|
+
<p>This wires the Codex hook, sets up the local lesson DB, and gives you the capture/promote/block loop without a hosted account. If you want the standalone Codex plugin as a self-contained zip β for offline distribution to Dell-managed machines or for security review β grab it from <a href="https://github.com/IgorGanapolsky/ThumbGate/releases" target="_blank" rel="noopener" style="color:var(--cyan)">GitHub releases</a> (look for <code>codex-plugin-*.zip</code>).</p>
|
|
102
|
+
|
|
103
|
+
<div class="card">
|
|
104
|
+
<p><strong>The free CLI is real. The paid tier is the hosted dashboard, the org-wide rule library, and the operator the Agent Manager doesn't have to be themselves.</strong></p>
|
|
105
|
+
<p>
|
|
106
|
+
<a href="/#workflow-sprint-intake?utm_source=website&utm_medium=codex_enterprise_page&utm_campaign=codex_enterprise_sprint&cta_id=codex_enterprise_sprint_intake&cta_placement=codex_enterprise_page" class="cta">Start the Workflow Hardening Sprint</a>
|
|
107
|
+
<a href="/checkout/pro?utm_source=website&utm_medium=codex_enterprise_page&utm_campaign=pro_upgrade&cta_id=codex_enterprise_pro_checkout&cta_placement=codex_enterprise_page&plan_id=pro" class="secondary">Or start Pro at $19/mo β</a>
|
|
108
|
+
</p>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<h2>Related reading</h2>
|
|
112
|
+
<ul>
|
|
113
|
+
<li><a href="/agent-manager">ThumbGate for the Agent Manager</a> β the role inside the enterprise that owns Codex rollout policy.</li>
|
|
114
|
+
<li><a href="/codex-plugin">Codex plugin overview</a> β the standalone plugin surface this page rides on top of.</li>
|
|
115
|
+
<li><a href="/compare">Compare</a> β how governance compares to orchestration suites under the same Codex install.</li>
|
|
116
|
+
</ul>
|
|
117
|
+
|
|
118
|
+
<div class="footer-links">
|
|
119
|
+
Built for teams who turned on Codex and discovered "tell the model to be more careful" doesn't scale. See also <a href="/agent-manager">/agent-manager</a> for the role-level framing.
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</body>
|
|
123
|
+
</html>
|
package/public/dashboard.html
CHANGED
|
@@ -247,9 +247,9 @@
|
|
|
247
247
|
|
|
248
248
|
<!-- STATS -->
|
|
249
249
|
<div class="stats-grid" id="statsGrid">
|
|
250
|
-
<a class="stat-card" data-card-action="all" onclick="selectCard(this,'all')" href="/lessons" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view all feedback β Lessons page"><div class="stat-label">Total Feedback</div><div class="stat-value cyan" id="statTotal">β</div></a>
|
|
251
|
-
<a class="stat-card" data-card-action="up" onclick="selectCard(this,'up')" href="/lessons?signal=
|
|
252
|
-
<a class="stat-card" data-card-action="down" onclick="selectCard(this,'down')" href="/lessons?signal=
|
|
250
|
+
<a class="stat-card" data-card-action="all" onclick="selectCard(this,'all')" href="/lessons?signal=all" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view all feedback β Lessons page"><div class="stat-label">Total Feedback</div><div class="stat-value cyan" id="statTotal">β</div></a>
|
|
251
|
+
<a class="stat-card" data-card-action="up" onclick="selectCard(this,'up')" href="/lessons?signal=up" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view positive feedback β Lessons page"><div class="stat-label">π Positive</div><div class="stat-value green" id="statPositive">β</div></a>
|
|
252
|
+
<a class="stat-card" data-card-action="down" onclick="selectCard(this,'down')" href="/lessons?signal=down" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view negative feedback β Lessons page"><div class="stat-label">π Negative</div><div class="stat-value red" id="statNegative">β</div></a>
|
|
253
253
|
<a class="stat-card" data-card-action="gates" onclick="selectCard(this,'gates');return false;" href="#" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view active checks"><div class="stat-label">Active Gates</div><div class="stat-value cyan" id="statGates">β</div></a>
|
|
254
254
|
</div>
|
|
255
255
|
|
|
@@ -750,10 +750,23 @@ function setSource(el, source) {
|
|
|
750
750
|
function switchTab(name) {
|
|
751
751
|
document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
|
|
752
752
|
document.querySelectorAll('.tab-content').forEach(function(c) { c.classList.remove('active'); });
|
|
753
|
-
|
|
753
|
+
// Scope the header lookup to .tab β the prior selector
|
|
754
|
+
// [onclick*="<name>"] also matched the stat-cards (which carry onclick
|
|
755
|
+
// attributes like selectCard(this,'gates')), and a stat-card appears
|
|
756
|
+
// before the tab header in DOM order, so for 'gates' the wrong element
|
|
757
|
+
// (the card) got the .active class and the tab header stayed dormant.
|
|
758
|
+
var tabEl = document.querySelector('.tab[onclick*="' + name + '"]');
|
|
754
759
|
var contentEl = document.getElementById('tab-' + name);
|
|
755
760
|
if (tabEl) tabEl.classList.add('active');
|
|
756
|
-
if (contentEl)
|
|
761
|
+
if (contentEl) {
|
|
762
|
+
contentEl.classList.add('active');
|
|
763
|
+
// Stat-card clicks fire switchTab from above the fold; without this scroll
|
|
764
|
+
// the user sees "nothing happen" because the just-activated content sits
|
|
765
|
+
// below the viewport. Same class of bug as the /lessons tile fix in #2268.
|
|
766
|
+
try {
|
|
767
|
+
contentEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
768
|
+
} catch (_e) { /* older browsers without smooth-scroll: no-op */ }
|
|
769
|
+
}
|
|
757
770
|
// Sync URL hash so deep-links stay shareable without scroll jump
|
|
758
771
|
try {
|
|
759
772
|
if (('#' + name) !== window.location.hash) {
|
package/public/index.html
CHANGED
|
@@ -19,7 +19,7 @@ __GOOGLE_SITE_VERIFICATION_META__
|
|
|
19
19
|
<meta property="og:image" content="https://thumbgate-production.up.railway.app/og.png">
|
|
20
20
|
<meta name="twitter:card" content="summary_large_image">
|
|
21
21
|
<meta name="twitter:image" content="https://thumbgate-production.up.railway.app/og.png">
|
|
22
|
-
<meta name="thumbgate-version" content="1.
|
|
22
|
+
<meta name="thumbgate-version" content="1.22.0">
|
|
23
23
|
<meta name="keywords" content="ThumbGate, thumbgate, AI agent orchestration, AI experience orchestration, agent enforcement layer, save LLM tokens, reduce Claude API cost, reduce OpenAI cost, AI agent token savings, prevent LLM retries, prevent hallucination retries, stop AI token waste, pre-action checks, agent governance, Claude Code, Cursor, Codex, Gemini, Amp, Cline, OpenCode, workflow hardening, context engineering, AI authenticity, brand authenticity AI">
|
|
24
24
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
25
25
|
|
|
@@ -928,8 +928,8 @@ __GA_BOOTSTRAP__
|
|
|
928
928
|
<div class="card-arrow">Open the Codex install page β</div>
|
|
929
929
|
</a>
|
|
930
930
|
<a class="compat-card" href="/guides/cursor-prevent-repeated-mistakes" rel="noopener">
|
|
931
|
-
<h3>π― Cursor plugin</h3>
|
|
932
|
-
<p>Drop the ThumbGate MCP config into <code>.cursor/mcp.json</code> and Cursor gets the same pre-action checks as Claude Code and Codex. Ships with bundled rules, commands, hooks, and agents.</p>
|
|
931
|
+
<h3>π― Cursor plugin <span style="font-size:12px;font-weight:500;color:var(--text-muted);">(Marketplace review pending)</span></h3>
|
|
932
|
+
<p>Drop the ThumbGate MCP config into <code>.cursor/mcp.json</code> and Cursor gets the same pre-action checks as Claude Code and Codex. Ships with bundled rules, commands, hooks, and agents. The runtime install works today via <code>npx thumbgate init --agent cursor</code>; the official Cursor Marketplace listing was submitted 2026-05-19 and is awaiting Cursor's manual review.</p>
|
|
933
933
|
<div class="card-arrow">Read the Cursor guide β</div>
|
|
934
934
|
</a>
|
|
935
935
|
<a class="compat-card" href="/guide" rel="noopener">
|
|
@@ -1408,7 +1408,7 @@ __GA_BOOTSTRAP__
|
|
|
1408
1408
|
</div>
|
|
1409
1409
|
<div class="faq-item">
|
|
1410
1410
|
<div class="faq-q" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="handleFaqKeydown(event)">What AI agents and editors does this work with?</div>
|
|
1411
|
-
<div class="faq-a">ThumbGate works with Claude Code, Cursor, Codex, Gemini CLI, Amp, Cline, OpenCode, and any other MCP-compatible agent. Cursor
|
|
1411
|
+
<div class="faq-a">ThumbGate works with Claude Code, Cursor, Codex, Gemini CLI, Amp, Cline, OpenCode, and any other MCP-compatible agent. The Cursor plugin bundle ships in this repo and installs today via <code>npx thumbgate init --agent cursor</code>; the Cursor Marketplace listing was submitted 2026-05-19 and is still pending Cursor's manual review, so it is not yet discoverable from the in-app Marketplace. Codex now ships both a standalone plugin bundle and a repo-local app plugin profile, and the published download is linked directly from this page. VS Code works when you run an MCP-compatible agent inside it, but this repo does not ship a standalone VS Code extension today.</div>
|
|
1412
1412
|
</div>
|
|
1413
1413
|
<div class="faq-item">
|
|
1414
1414
|
<div class="faq-q" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="handleFaqKeydown(event)">Do I have to chat inside the ThumbGate GPT for enforcement?</div>
|
|
@@ -1492,7 +1492,7 @@ __GA_BOOTSTRAP__
|
|
|
1492
1492
|
<a href="https://www.linkedin.com/in/igorganapolsky" target="_blank" rel="noopener">LinkedIn</a>
|
|
1493
1493
|
<a href="/blog">Blog</a>
|
|
1494
1494
|
</div>
|
|
1495
|
-
<span class="footer-copy">Β© 2026 ThumbGate Β· MIT License Β· npm v1.
|
|
1495
|
+
<span class="footer-copy">Β© 2026 ThumbGate Β· MIT License Β· npm v1.22.0</span>
|
|
1496
1496
|
</div>
|
|
1497
1497
|
</footer>
|
|
1498
1498
|
|
|
@@ -1672,6 +1672,13 @@ function copyInstall(el) {
|
|
|
1672
1672
|
toggleFaq(event.currentTarget);
|
|
1673
1673
|
}
|
|
1674
1674
|
|
|
1675
|
+
// Hoist FAQ handlers to window scope so the inline `onclick="toggleFaq(this)"`
|
|
1676
|
+
// attributes on every FAQ question can resolve them. Without this, every FAQ
|
|
1677
|
+
// click silently throws ReferenceError β all 13 FAQ items on the landing
|
|
1678
|
+
// page are dead. Discovered by comprehensive E2E coverage in this PR.
|
|
1679
|
+
window.toggleFaq = toggleFaq;
|
|
1680
|
+
window.handleFaqKeydown = handleFaqKeydown;
|
|
1681
|
+
|
|
1675
1682
|
/* CTA clicks */
|
|
1676
1683
|
trackClick('.btn-pro', 'checkout_start', { tier: 'pro', price: 19, billing: 'monthly' });
|
|
1677
1684
|
trackClick('.btn-gpt-page:not(.btn-install-hero)', 'chatgpt_gpt_click', { tier: 'free', source: 'homepage_gpt' });
|
package/public/lessons.html
CHANGED
|
@@ -449,6 +449,18 @@ function switchTab(name) {
|
|
|
449
449
|
// Highlight the corresponding stat card
|
|
450
450
|
var cardMap = { rules: 0, timeline: 2, insights: 3 };
|
|
451
451
|
highlightCard(cardMap[name] !== undefined ? cardMap[name] : -1);
|
|
452
|
+
// Scroll the active tab content into view so the click has a visible effect.
|
|
453
|
+
// Without this, clicking a stat card or tab header when its content is below
|
|
454
|
+
// the fold appears to do nothing β the tab changes silently and the user
|
|
455
|
+
// never sees the new content. The tab-strip itself stays visible so the
|
|
456
|
+
// selected-state is still observable.
|
|
457
|
+
if (content && typeof content.scrollIntoView === 'function') {
|
|
458
|
+
try {
|
|
459
|
+
content.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
460
|
+
} catch (_err) {
|
|
461
|
+
content.scrollIntoView();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
452
464
|
if (typeof plausible === 'function') plausible('lessons_tab', { props: { tab: name } });
|
|
453
465
|
}
|
|
454
466
|
|
|
@@ -922,6 +934,28 @@ async function loadLive() {
|
|
|
922
934
|
}
|
|
923
935
|
|
|
924
936
|
loadLive().then(function() {
|
|
937
|
+
// Handle ?signal= query param from dashboard stat-card navigation.
|
|
938
|
+
// Vocabulary: 'up' | 'down' | 'all' (canonical). Also accepts the legacy
|
|
939
|
+
// 'positive' | 'negative' aliases the dashboard once emitted.
|
|
940
|
+
var qsSignal = new URLSearchParams(window.location.search).get('signal');
|
|
941
|
+
if (qsSignal) {
|
|
942
|
+
var signalMap = { positive: 'up', negative: 'down', up: 'up', down: 'down', all: 'all' };
|
|
943
|
+
var mapped = signalMap[qsSignal];
|
|
944
|
+
if (mapped) {
|
|
945
|
+
switchTab('timeline');
|
|
946
|
+
filterTimeline(mapped, null);
|
|
947
|
+
var filterBtns = document.querySelectorAll('#tab-timeline .filter-btn');
|
|
948
|
+
filterBtns.forEach(function(b) {
|
|
949
|
+
var label = b.textContent.trim().toLowerCase();
|
|
950
|
+
var match = (mapped === 'all' && label === 'all') ||
|
|
951
|
+
(mapped === 'up' && (label.indexOf('π') !== -1 || label.indexOf('positive') !== -1 || label === 'up')) ||
|
|
952
|
+
(mapped === 'down' && (label.indexOf('π') !== -1 || label.indexOf('negative') !== -1 || label === 'down'));
|
|
953
|
+
b.classList.toggle('active', match);
|
|
954
|
+
});
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
925
959
|
// Default: highlight Active Rules card on page load
|
|
926
960
|
highlightCard(0);
|
|
927
961
|
|
package/public/numbers.html
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"alternateName": "thumbgate",
|
|
26
26
|
"applicationCategory": "DeveloperApplication",
|
|
27
27
|
"operatingSystem": "Cross-platform, Node.js >=18.18.0",
|
|
28
|
-
"softwareVersion": "1.
|
|
28
|
+
"softwareVersion": "1.22.0",
|
|
29
29
|
"url": "https://thumbgate-production.up.railway.app/numbers",
|
|
30
30
|
"dateModified": "2026-05-07",
|
|
31
31
|
"creator": {
|
|
@@ -202,7 +202,7 @@
|
|
|
202
202
|
<main class="container">
|
|
203
203
|
<h1>The Numbers</h1>
|
|
204
204
|
<p class="subtitle">Generated first-party operational snapshot from the ThumbGate runtime. This is not customer traction, install volume, revenue, or proof that a configured gate has fired.</p>
|
|
205
|
-
<div class="freshness">Updated: 2026-05-07 Β· Version 1.
|
|
205
|
+
<div class="freshness">Updated: 2026-05-07 Β· Version 1.22.0</div>
|
|
206
206
|
<div class="truth-note"><strong>Read this first:</strong> configured checks are inventory. Recorded blocks and warnings are usage evidence. This snapshot currently reports 0 recorded hard-block event(s) and 0 recorded warning event(s).</div>
|
|
207
207
|
|
|
208
208
|
<h2>Gate enforcement</h2>
|
|
@@ -27,6 +27,7 @@ const {
|
|
|
27
27
|
statuslineCommand,
|
|
28
28
|
userPromptHookCommand,
|
|
29
29
|
} = require('./hook-runtime');
|
|
30
|
+
const { installShim } = require('./install-shim');
|
|
30
31
|
|
|
31
32
|
function getHome() {
|
|
32
33
|
return process.env.HOME || process.env.USERPROFILE || '';
|
|
@@ -338,6 +339,19 @@ function wireClaudeHooks(options) {
|
|
|
338
339
|
options.projectSettingsPath || claudeProjectSettingsPath(options.projectDir);
|
|
339
340
|
const dryRun = options.dryRun || false;
|
|
340
341
|
const projectDir = options.projectDir || process.cwd();
|
|
342
|
+
|
|
343
|
+
// --- Install stable shim before resolving hook commands ---
|
|
344
|
+
// The shim at ~/.thumbgate/bin/thumbgate-hook always resolves @latest,
|
|
345
|
+
// so hooks never go stale across version bumps (Volta-style pattern).
|
|
346
|
+
// Skip in source-checkout mode β developers use direct node commands.
|
|
347
|
+
if (!dryRun && !require('./mcp-config').isSourceCheckout(path.join(__dirname, '..'))) {
|
|
348
|
+
try {
|
|
349
|
+
installShim();
|
|
350
|
+
} catch {
|
|
351
|
+
// Non-fatal: fall back to version-pinned commands
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
341
355
|
const desiredStatusLine = statuslineCommand();
|
|
342
356
|
|
|
343
357
|
// --- Step 0: clean up stale hooks from BOTH settings locations ---
|
|
@@ -16,6 +16,13 @@ function normalizeNullableText(value) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function resolveBuildMetadata({ env = process.env, filePath } = {}) {
|
|
19
|
+
// Precedence: immutable JSON file (baked into Docker image at build time, so it
|
|
20
|
+
// ALWAYS matches the deployed code) wins over runtime env vars. Env vars are
|
|
21
|
+
// mutable Railway/host config that can drift β they shadowed the freshly-stamped
|
|
22
|
+
// SHA in prod on 2026-05-20 and made /health lie about the deployed commit.
|
|
23
|
+
// Fall back to env vars only when the file is missing or its values are null,
|
|
24
|
+
// and require an explicit SHA env var (not just a stray GENERATED_AT) before
|
|
25
|
+
// trusting the env branch.
|
|
19
26
|
const resolvedPath =
|
|
20
27
|
normalizeNullableText(filePath) ||
|
|
21
28
|
normalizeNullableText(env.THUMBGATE_BUILD_METADATA_PATH) ||
|
|
@@ -23,28 +30,40 @@ function resolveBuildMetadata({ env = process.env, filePath } = {}) {
|
|
|
23
30
|
const envBuildSha = normalizeNullableText(env[BUILD_SHA_ENV_KEY]);
|
|
24
31
|
const envGeneratedAt = normalizeNullableText(env[BUILD_GENERATED_AT_ENV_KEY]);
|
|
25
32
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
path: resolvedPath,
|
|
29
|
-
buildSha: envBuildSha,
|
|
30
|
-
generatedAt: envGeneratedAt,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
33
|
+
let fileBuildSha = null;
|
|
34
|
+
let fileGeneratedAt = null;
|
|
34
35
|
try {
|
|
35
36
|
const parsed = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
|
|
37
|
+
fileBuildSha = normalizeNullableText(parsed.buildSha);
|
|
38
|
+
fileGeneratedAt = normalizeNullableText(parsed.generatedAt);
|
|
39
|
+
} catch {
|
|
40
|
+
// file missing or unreadable β fall through to env branch
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (fileBuildSha) {
|
|
36
44
|
return {
|
|
37
45
|
path: resolvedPath,
|
|
38
|
-
buildSha:
|
|
39
|
-
generatedAt:
|
|
46
|
+
buildSha: fileBuildSha,
|
|
47
|
+
generatedAt: fileGeneratedAt || envGeneratedAt,
|
|
40
48
|
};
|
|
41
|
-
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// No SHA in the file β fall back to env only if an explicit SHA is set.
|
|
52
|
+
// (Previously a bare GENERATED_AT with no SHA could short-circuit and return
|
|
53
|
+
// { buildSha: null }, losing both signals; now we require the SHA.)
|
|
54
|
+
if (envBuildSha) {
|
|
42
55
|
return {
|
|
43
56
|
path: resolvedPath,
|
|
44
|
-
buildSha:
|
|
45
|
-
generatedAt:
|
|
57
|
+
buildSha: envBuildSha,
|
|
58
|
+
generatedAt: envGeneratedAt,
|
|
46
59
|
};
|
|
47
60
|
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
path: resolvedPath,
|
|
64
|
+
buildSha: null,
|
|
65
|
+
generatedAt: fileGeneratedAt || envGeneratedAt,
|
|
66
|
+
};
|
|
48
67
|
}
|
|
49
68
|
|
|
50
69
|
function writeBuildMetadataFile({ sha, outputPath, generatedAt = new Date().toISOString() }) {
|
package/scripts/gate-stats.js
CHANGED
|
@@ -9,6 +9,7 @@ const { sequencePathFor } = require('./risk-scorer');
|
|
|
9
9
|
|
|
10
10
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
11
11
|
const MANUAL_GATES_PATH = path.join(PROJECT_ROOT, 'config', 'gates', 'default.json');
|
|
12
|
+
const STATS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'gate-stats.json');
|
|
12
13
|
|
|
13
14
|
function loadGatesFile(filePath) {
|
|
14
15
|
if (!fs.existsSync(filePath)) return [];
|
|
@@ -65,6 +66,15 @@ function calculateStats() {
|
|
|
65
66
|
// sample can produce a misleading 0.0% floor.
|
|
66
67
|
const bayesErrorRate = tryComputeBayesErrorRate();
|
|
67
68
|
|
|
69
|
+
// Calibration: per-gate assessment of whether block actions are well-supported
|
|
70
|
+
// by negative feedback, or potentially over/under-blocking without confirmation.
|
|
71
|
+
const calibration = computeCalibration(allGates);
|
|
72
|
+
|
|
73
|
+
// First-time fix rate: 1 - (recurringBlocks / totalBlocksAndWarns)
|
|
74
|
+
// Measures how often a single gate fire resolves the issue vs the agent retrying.
|
|
75
|
+
// Returns null when there is no recorded block/warn data yet.
|
|
76
|
+
const firstTimeFixRate = computeFirstTimeFixRate();
|
|
77
|
+
|
|
68
78
|
return {
|
|
69
79
|
totalGates: allGates.length,
|
|
70
80
|
manualGates: manualGates.length,
|
|
@@ -77,10 +87,70 @@ function calculateStats() {
|
|
|
77
87
|
lastPromotion,
|
|
78
88
|
estimatedHoursSaved,
|
|
79
89
|
bayesErrorRate,
|
|
90
|
+
calibration,
|
|
91
|
+
firstTimeFixRate,
|
|
80
92
|
gates: allGates,
|
|
81
93
|
};
|
|
82
94
|
}
|
|
83
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Assess each gate's calibration by comparing block occurrences to confirmed
|
|
98
|
+
* negative feedback counts. A gate with many blocks but no confirming negative
|
|
99
|
+
* feedback may be over-blocking; one with matching feedback is well-calibrated.
|
|
100
|
+
*
|
|
101
|
+
* @param {Array} gates - Combined array of manual + auto-promoted gate objects
|
|
102
|
+
* @returns {Array<{gateId: string, occurrences: number, action: string, calibrationNote: string}>}
|
|
103
|
+
*/
|
|
104
|
+
function computeCalibration(gates) {
|
|
105
|
+
const calibration = [];
|
|
106
|
+
for (const gate of gates || []) {
|
|
107
|
+
if (!gate || !gate.id) continue;
|
|
108
|
+
const occurrences = Number(gate.occurrences || 0);
|
|
109
|
+
const action = gate.action || 'unknown';
|
|
110
|
+
// Only annotate gates with recorded occurrence data
|
|
111
|
+
if (occurrences === 0) continue;
|
|
112
|
+
|
|
113
|
+
if (action === 'block') {
|
|
114
|
+
const confirmedNegative = Number(gate.confirmedNegative || gate.negativeCount || 0);
|
|
115
|
+
let calibrationNote;
|
|
116
|
+
if (occurrences > 10 && confirmedNegative === 0) {
|
|
117
|
+
calibrationNote = `over-blocking (${occurrences} blocks, 0 confirmed)`;
|
|
118
|
+
} else if (confirmedNegative > 0) {
|
|
119
|
+
calibrationNote = `well-calibrated (${occurrences} blocks, ${confirmedNegative} confirmed)`;
|
|
120
|
+
} else {
|
|
121
|
+
// Low occurrence count with no feedback β not enough data yet
|
|
122
|
+
calibrationNote = `insufficient data (${occurrences} blocks, 0 confirmed)`;
|
|
123
|
+
}
|
|
124
|
+
calibration.push({ gateId: gate.id, occurrences, action, calibrationNote });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return calibration;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Compute the first-time fix rate from the persisted gate-stats.json file.
|
|
132
|
+
*
|
|
133
|
+
* firstTimeFixRate = 1 - (recurringBlocks / totalBlocksAndWarns)
|
|
134
|
+
*
|
|
135
|
+
* Returns null when there are no recorded block/warn events yet.
|
|
136
|
+
* Returns a number in [0, 1] otherwise, where 1.0 means every gate fire
|
|
137
|
+
* was a first-time occurrence and 0.0 means every gate fired at least twice.
|
|
138
|
+
*/
|
|
139
|
+
function computeFirstTimeFixRate() {
|
|
140
|
+
try {
|
|
141
|
+
if (!fs.existsSync(STATS_PATH)) return null;
|
|
142
|
+
const raw = fs.readFileSync(STATS_PATH, 'utf8');
|
|
143
|
+
const data = JSON.parse(raw);
|
|
144
|
+
const totalBlocksAndWarns = (data.blocked || 0) + (data.warned || 0);
|
|
145
|
+
if (totalBlocksAndWarns === 0) return null;
|
|
146
|
+
const recurring = data.recurringBlocks || 0;
|
|
147
|
+
const rate = 1 - (recurring / totalBlocksAndWarns);
|
|
148
|
+
return Math.max(0, Math.min(1, rate));
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
84
154
|
function tryComputeBayesErrorRate() {
|
|
85
155
|
try {
|
|
86
156
|
const seqPath = sequencePathFor();
|
|
@@ -142,6 +212,13 @@ function formatStats(stats) {
|
|
|
142
212
|
lines.push(` Last promotion: ${formatLastPromotion(stats.lastPromotion)}`);
|
|
143
213
|
lines.push(` Estimated time saved: ~${stats.estimatedHoursSaved} hours`);
|
|
144
214
|
lines.push(` Bayes error rate: ${formatBayesErrorRate(stats.bayesErrorRate)}`);
|
|
215
|
+
lines.push(` First-time fix rate: ${formatFirstTimeFixRate(stats.firstTimeFixRate)}`);
|
|
216
|
+
if (Array.isArray(stats.calibration) && stats.calibration.length > 0) {
|
|
217
|
+
lines.push('Calibration:');
|
|
218
|
+
for (const entry of stats.calibration) {
|
|
219
|
+
lines.push(` - ${entry.gateId}: ${entry.calibrationNote}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
145
222
|
return lines.join('\n');
|
|
146
223
|
}
|
|
147
224
|
|
|
@@ -153,6 +230,14 @@ function formatBayesErrorRate(rate) {
|
|
|
153
230
|
return `${pct}% β high irreducible error; the feature set can't discriminate`;
|
|
154
231
|
}
|
|
155
232
|
|
|
233
|
+
function formatFirstTimeFixRate(rate) {
|
|
234
|
+
if (rate === null || rate === undefined) return 'n/a (no blocks or warns recorded yet)';
|
|
235
|
+
const pct = (rate * 100).toFixed(1);
|
|
236
|
+
if (rate >= 0.95) return `${pct}% β agents fix issues on first block (excellent)`;
|
|
237
|
+
if (rate >= 0.80) return `${pct}% β most blocks resolved first time (good)`;
|
|
238
|
+
return `${pct}% β recurring blocks detected; agents may be ignoring gate feedback`;
|
|
239
|
+
}
|
|
240
|
+
|
|
156
241
|
if (require.main === module) {
|
|
157
242
|
try {
|
|
158
243
|
const stats = calculateStats();
|
|
@@ -168,7 +253,11 @@ module.exports = {
|
|
|
168
253
|
formatStats,
|
|
169
254
|
formatLastPromotion,
|
|
170
255
|
formatBayesErrorRate,
|
|
256
|
+
formatFirstTimeFixRate,
|
|
257
|
+
computeFirstTimeFixRate,
|
|
171
258
|
loadGatesFile,
|
|
172
259
|
tryComputeBayesErrorRate,
|
|
260
|
+
computeCalibration,
|
|
173
261
|
MANUAL_GATES_PATH,
|
|
262
|
+
STATS_PATH,
|
|
174
263
|
};
|
package/scripts/gates-engine.js
CHANGED
|
@@ -439,6 +439,20 @@ function recordStat(gateId, action, gate) {
|
|
|
439
439
|
else if (action === 'warn') stats.byGate[gateId].warned += 1;
|
|
440
440
|
else if (action === 'approve') stats.byGate[gateId].pendingApproval = (stats.byGate[gateId].pendingApproval || 0) + 1;
|
|
441
441
|
else if (action === 'log') stats.byGate[gateId].logged = (stats.byGate[gateId].logged || 0) + 1;
|
|
442
|
+
|
|
443
|
+
// Track per-gate recurrence within a session for first-time fix rate
|
|
444
|
+
if (action === 'block' || action === 'warn') {
|
|
445
|
+
if (!stats.sessionFiredGates) stats.sessionFiredGates = {};
|
|
446
|
+
const sessionKey = `session_${Math.floor(Date.now() / SESSION_ACTION_TTL_MS)}`;
|
|
447
|
+
if (!stats.sessionFiredGates[sessionKey]) stats.sessionFiredGates[sessionKey] = {};
|
|
448
|
+
if (stats.sessionFiredGates[sessionKey][gateId]) {
|
|
449
|
+
// Same gate fired again in this session β it's a recurring block
|
|
450
|
+
stats.recurringBlocks = (stats.recurringBlocks || 0) + 1;
|
|
451
|
+
} else {
|
|
452
|
+
stats.sessionFiredGates[sessionKey][gateId] = true;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
442
456
|
saveStats(stats);
|
|
443
457
|
// Track lesson freshness when an auto-promoted gate fires
|
|
444
458
|
if (gate && gate.sourceLessonId) {
|
package/scripts/hook-runtime.js
CHANGED
|
@@ -7,6 +7,7 @@ const {
|
|
|
7
7
|
publishedCliAvailable,
|
|
8
8
|
} = require('./mcp-config');
|
|
9
9
|
const { publishedCliShellCommand } = require('./published-cli');
|
|
10
|
+
const { shimInstalled, shimPath } = require('./install-shim');
|
|
10
11
|
|
|
11
12
|
const PKG_ROOT = path.join(__dirname, '..');
|
|
12
13
|
const featureSupportCache = new Map();
|
|
@@ -34,13 +35,18 @@ function publishedHookCommandsAvailable(version) {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
function resolveCliCommand(subcommand) {
|
|
38
|
+
// Source checkout: always use direct node command for development
|
|
39
|
+
if (isSourceCheckout(PKG_ROOT)) {
|
|
40
|
+
return `node ${shellQuote(path.join(PKG_ROOT, 'bin', 'cli.js'))} ${subcommand}`;
|
|
41
|
+
}
|
|
42
|
+
// Prefer stable shim β always resolves @latest, survives version bumps
|
|
43
|
+
if (shimInstalled()) {
|
|
44
|
+
return `${shellQuote(shimPath())} ${subcommand}`;
|
|
45
|
+
}
|
|
37
46
|
const version = packageVersion();
|
|
38
47
|
if (publishedHookCommandsAvailable(version)) {
|
|
39
48
|
return publishedCliShellCommand(version, [subcommand]);
|
|
40
49
|
}
|
|
41
|
-
if (isSourceCheckout(PKG_ROOT)) {
|
|
42
|
-
return `node ${shellQuote(path.join(PKG_ROOT, 'bin', 'cli.js'))} ${subcommand}`;
|
|
43
|
-
}
|
|
44
50
|
return publishedCliShellCommand(version, [subcommand]);
|
|
45
51
|
}
|
|
46
52
|
|
package/scripts/tool-registry.js
CHANGED
|
@@ -397,6 +397,24 @@ const TOOLS = [
|
|
|
397
397
|
},
|
|
398
398
|
},
|
|
399
399
|
}),
|
|
400
|
+
readOnlyTool({
|
|
401
|
+
name: 'suggest_fix',
|
|
402
|
+
description: 'Suggest corrective actions for a described failure by searching the lesson DB and prevention rules. Returns up to 3 ranked suggestions with their source. Call this when something goes wrong and you need guidance on what to do next.',
|
|
403
|
+
inputSchema: {
|
|
404
|
+
type: 'object',
|
|
405
|
+
required: ['context'],
|
|
406
|
+
properties: {
|
|
407
|
+
context: {
|
|
408
|
+
type: 'string',
|
|
409
|
+
description: 'Description of what went wrong or what the agent is trying to fix.',
|
|
410
|
+
},
|
|
411
|
+
limit: {
|
|
412
|
+
type: 'number',
|
|
413
|
+
description: 'Maximum number of suggestions to return (default 3, max 5).',
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
}),
|
|
400
418
|
readOnlyTool({
|
|
401
419
|
name: 'infer_lesson_from_history',
|
|
402
420
|
description: 'Perform autonomous inference on chat history to identify why a failure occurred and what rule should be recorded.',
|
|
@@ -1178,10 +1178,15 @@ function chooseDecision({ riskScore, integrity, memoryGuard, learnedPolicy, blas
|
|
|
1178
1178
|
if (lowRiskHandoff) {
|
|
1179
1179
|
return 'allow';
|
|
1180
1180
|
}
|
|
1181
|
+
// Background customer-system actions checkpoint (warn), never hard-deny.
|
|
1182
|
+
// The checkpoint IS the mitigation β blocking outright prevents legitimate work.
|
|
1183
|
+
if (backgroundAgent && customerSystemAction) {
|
|
1184
|
+
return 'warn';
|
|
1185
|
+
}
|
|
1181
1186
|
if (destructiveBypass || learnedHardStop || repeatedHighBlast || (hasOperationalBlockers && riskScore >= 0.72) || riskScore >= 0.86) {
|
|
1182
1187
|
return 'deny';
|
|
1183
1188
|
}
|
|
1184
|
-
if (economicAction || (backgroundAgent &&
|
|
1189
|
+
if (economicAction || (backgroundAgent && riskScore >= 0.3)) {
|
|
1185
1190
|
return 'warn';
|
|
1186
1191
|
}
|
|
1187
1192
|
if ((workflowControl && workflowControl.mode === 'warn') || (costControl && costControl.mode === 'warn') || riskScore >= 0.45 || (learnedWarning && riskScore >= 0.3) || (learnedRecall && riskScore >= 0.34)) {
|
package/src/api/server.js
CHANGED
|
@@ -2504,6 +2504,7 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
2504
2504
|
{ path: '/agent-manager', changefreq: 'weekly', priority: '0.9' },
|
|
2505
2505
|
{ path: '/llm-context.md', changefreq: 'weekly', priority: '0.8' },
|
|
2506
2506
|
{ path: '/codex-plugin', changefreq: 'weekly', priority: '0.75' },
|
|
2507
|
+
{ path: '/codex-enterprise', changefreq: 'weekly', priority: '0.85' },
|
|
2507
2508
|
...THUMBGATE_SEO_SITEMAP_ENTRIES,
|
|
2508
2509
|
];
|
|
2509
2510
|
return [
|
|
@@ -4520,6 +4521,30 @@ async function addContext(){
|
|
|
4520
4521
|
return;
|
|
4521
4522
|
}
|
|
4522
4523
|
|
|
4524
|
+
if (isGetLikeRequest && (pathname === '/codex-enterprise' || pathname === '/codex-enterprise.html')) {
|
|
4525
|
+
// Landing page riding the 2026-05-20 OpenAIΓDell Codex Enterprise
|
|
4526
|
+
// partnership announcement. Dell-distributed Codex expands the TAM
|
|
4527
|
+
// for ThumbGate's governance layer β capture every agent decision,
|
|
4528
|
+
// promote repeat failures to PreToolUse gates, ship the audit trail
|
|
4529
|
+
// procurement requires. Routed through servePublicMarketingPage so
|
|
4530
|
+
// arrivals via the partnership news cycle capture UTM attribution
|
|
4531
|
+
// and landing_page_view telemetry with pageType: 'codex_enterprise'.
|
|
4532
|
+
try {
|
|
4533
|
+
servePublicMarketingPage({
|
|
4534
|
+
req,
|
|
4535
|
+
res,
|
|
4536
|
+
parsed,
|
|
4537
|
+
hostedConfig,
|
|
4538
|
+
isHeadRequest,
|
|
4539
|
+
renderHtml: () => fs.readFileSync(path.join(PUBLIC_DIR, 'codex-enterprise.html'), 'utf-8'),
|
|
4540
|
+
extraTelemetry: { pageType: 'codex_enterprise' },
|
|
4541
|
+
});
|
|
4542
|
+
} catch {
|
|
4543
|
+
sendJson(res, 404, { error: 'Codex Enterprise page not found' });
|
|
4544
|
+
}
|
|
4545
|
+
return;
|
|
4546
|
+
}
|
|
4547
|
+
|
|
4523
4548
|
if (isGetLikeRequest && pathname === '/learn/learn.css') {
|
|
4524
4549
|
try {
|
|
4525
4550
|
const cssPath = path.join(LEARN_DIR, 'learn.css');
|
|
@@ -7729,6 +7754,7 @@ function startServer({ port, host } = {}) {
|
|
|
7729
7754
|
const listenPort = Number(port ?? process.env.PORT ?? 8787);
|
|
7730
7755
|
const listenHost = String(host ?? process.env.HOST ?? '0.0.0.0').trim() || '0.0.0.0';
|
|
7731
7756
|
const server = createApiServer();
|
|
7757
|
+
registerGracefulShutdown(server);
|
|
7732
7758
|
return new Promise((resolve) => {
|
|
7733
7759
|
server.listen(listenPort, listenHost, () => {
|
|
7734
7760
|
const address = server.address();
|
|
@@ -7744,6 +7770,39 @@ function startServer({ port, host } = {}) {
|
|
|
7744
7770
|
});
|
|
7745
7771
|
}
|
|
7746
7772
|
|
|
7773
|
+
// Railway / Cloud Run / Kubernetes deploy rotations send SIGTERM to swap
|
|
7774
|
+
// containers. Without a handler, Node exits immediately β in-flight requests
|
|
7775
|
+
// are killed and the orchestrator may mark the container as "crashed" (instead
|
|
7776
|
+
// of "gracefully stopped"), wasting its restart-policy budget on a healthy
|
|
7777
|
+
// shutdown. Drain HTTP, give a deadline, then force-exit if anything hangs.
|
|
7778
|
+
function registerGracefulShutdown(server, { gracePeriodMs = 25_000 } = {}) {
|
|
7779
|
+
if (server[GRACEFUL_SHUTDOWN_KEY]) return;
|
|
7780
|
+
server[GRACEFUL_SHUTDOWN_KEY] = true;
|
|
7781
|
+
let shuttingDown = false;
|
|
7782
|
+
const stop = (signal) => {
|
|
7783
|
+
if (shuttingDown) return;
|
|
7784
|
+
shuttingDown = true;
|
|
7785
|
+
console.log(`[shutdown] ${signal} received β draining connections (deadline ${gracePeriodMs}ms)`);
|
|
7786
|
+
const forceTimer = setTimeout(() => {
|
|
7787
|
+
console.error('[shutdown] grace period elapsed β forcing exit');
|
|
7788
|
+
process.exit(1);
|
|
7789
|
+
}, gracePeriodMs);
|
|
7790
|
+
if (typeof forceTimer.unref === 'function') forceTimer.unref();
|
|
7791
|
+
server.close((err) => {
|
|
7792
|
+
if (err) {
|
|
7793
|
+
console.error('[shutdown] server.close error:', err.message);
|
|
7794
|
+
process.exit(1);
|
|
7795
|
+
}
|
|
7796
|
+
console.log('[shutdown] drained cleanly');
|
|
7797
|
+
process.exit(0);
|
|
7798
|
+
});
|
|
7799
|
+
};
|
|
7800
|
+
process.on('SIGTERM', () => stop('SIGTERM'));
|
|
7801
|
+
process.on('SIGINT', () => stop('SIGINT'));
|
|
7802
|
+
}
|
|
7803
|
+
|
|
7804
|
+
const GRACEFUL_SHUTDOWN_KEY = Symbol.for('thumbgate.gracefulShutdownRegistered');
|
|
7805
|
+
|
|
7747
7806
|
module.exports = {
|
|
7748
7807
|
createApiServer,
|
|
7749
7808
|
startServer,
|