thumbgate 1.21.2 → 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 +109 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +247 -30
- package/config/mcp-allowlists.json +12 -6
- package/openapi/openapi.yaml +10 -0
- package/package.json +29 -5
- package/public/agent-manager.html +1 -1
- package/public/agents-cost-savings.html +151 -0
- package/public/ai-malpractice-prevention.html +183 -0
- package/public/codex-enterprise.html +123 -0
- package/public/codex-plugin.html +1 -1
- package/public/dashboard.html +18 -5
- package/public/index.html +13 -6
- package/public/lessons.html +34 -0
- package/public/numbers.html +2 -2
- package/public/pricing.html +1 -1
- package/scripts/auto-wire-hooks.js +14 -0
- package/scripts/build-metadata.js +32 -13
- package/scripts/cli-telemetry.js +6 -1
- package/scripts/gate-stats.js +89 -0
- package/scripts/gates-engine.js +133 -6
- package/scripts/hook-runtime.js +9 -3
- 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/scripts/tool-registry.js +18 -0
- package/scripts/workflow-sentinel.js +6 -1
- package/src/api/server.js +311 -36
|
@@ -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/codex-plugin.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<meta property="og:type" content="website">
|
|
13
13
|
<meta property="og:url" content="https://thumbgate-production.up.railway.app/codex-plugin">
|
|
14
14
|
<link rel="canonical" href="https://thumbgate-production.up.railway.app/codex-plugin">
|
|
15
|
-
<link rel="llm-context" href="/
|
|
15
|
+
<link rel="llm-context" href="/llm-context.md" type="text/markdown">
|
|
16
16
|
|
|
17
17
|
<script type="application/ld+json">
|
|
18
18
|
{
|
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.23.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
|
|
|
@@ -721,7 +721,7 @@ __GA_BOOTSTRAP__
|
|
|
721
721
|
</div>
|
|
722
722
|
<a href="/go/install?utm_source=website&utm_medium=hero_cta&utm_campaign=install_free&cta_id=hero_install_cli&cta_placement=hero" onclick="event.preventDefault(); navigator.clipboard.writeText('npx thumbgate init'); this.textContent='Copied ✓ — paste in your repo'; setTimeout(()=>{this.textContent='Install Free CLI'},2000); try{posthog.capture('hero_install_click',{cta:'install_cli'})}catch(_){}" class="btn-gpt-page btn-install-hero" title="Click to copy: npx thumbgate init">Install Free CLI</a>
|
|
723
723
|
<a href="#workflow-sprint-intake" onclick="try{posthog.capture('hero_sprint_click',{cta:'sprint_intake'})}catch(_){};sendFirstPartyTelemetry('hero_sprint_intake_started',{ctaId:'hero_workflow_sprint',ctaPlacement:'hero',offer:'workflow_sprint'});" class="btn-pro-page hero-pro">Talk to me — Workflow Hardening Sprint →</a>
|
|
724
|
-
<a href="#demo" onclick="try{posthog.capture('hero_demo_click',{cta:'
|
|
724
|
+
<a href="#demo" onclick="try{posthog.capture('hero_demo_click',{cta:'see_enforcement'})}catch(_){};sendFirstPartyTelemetry('hero_demo_clicked',{ctaId:'hero_see_enforcement',ctaPlacement:'hero'});" class="btn-free" style="font-size:15px;padding:14px 22px;">See the enforcement in action</a>
|
|
725
725
|
</div>
|
|
726
726
|
|
|
727
727
|
<div class="hero-trust-bar">
|
|
@@ -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.23.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.23.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.23.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>
|
package/public/pricing.html
CHANGED
|
@@ -213,7 +213,7 @@ __GA_BOOTSTRAP__
|
|
|
213
213
|
<div class="container">
|
|
214
214
|
<a href="/" class="nav-logo">ThumbGate</a>
|
|
215
215
|
<div class="nav-links">
|
|
216
|
-
<a href="/#
|
|
216
|
+
<a href="/#how-it-works">Features</a>
|
|
217
217
|
<a href="/pricing" style="color:var(--text);">Pricing</a>
|
|
218
218
|
<a href="/guide">Guide</a>
|
|
219
219
|
<a href="/dashboard">Dashboard</a>
|
|
@@ -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/cli-telemetry.js
CHANGED
|
@@ -10,6 +10,9 @@ const _DEFAULT_TELEMETRY_HOST = 'https://thumbgate-production.up.railway.app';
|
|
|
10
10
|
const TELEMETRY_ENDPOINT = `${process.env.THUMBGATE_API_URL || _DEFAULT_TELEMETRY_HOST}/v1/telemetry/ping`;
|
|
11
11
|
const INSTALL_ID_PATH = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.thumbgate', 'install-id');
|
|
12
12
|
|
|
13
|
+
// Session ID: random per process invocation. Groups all events from one CLI run.
|
|
14
|
+
const SESSION_ID = crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex');
|
|
15
|
+
|
|
13
16
|
/**
|
|
14
17
|
* Get or create a stable anonymous install ID.
|
|
15
18
|
* This is NOT tied to any personal info — it's a random UUID stored locally.
|
|
@@ -61,7 +64,9 @@ function trackEvent(eventType, metadata = {}) {
|
|
|
61
64
|
const payload = JSON.stringify({
|
|
62
65
|
eventType,
|
|
63
66
|
installId: getInstallId(),
|
|
67
|
+
sessionId: SESSION_ID,
|
|
64
68
|
visitorType: classifyInstall(),
|
|
69
|
+
clientType: 'cli',
|
|
65
70
|
platform: os.platform(),
|
|
66
71
|
arch: os.arch(),
|
|
67
72
|
nodeVersion: process.version,
|
|
@@ -87,4 +92,4 @@ function trackEvent(eventType, metadata = {}) {
|
|
|
87
92
|
} catch (_) {} // never crash the CLI
|
|
88
93
|
}
|
|
89
94
|
|
|
90
|
-
module.exports = { trackEvent, getInstallId, classifyInstall, INSTALL_ID_PATH };
|
|
95
|
+
module.exports = { trackEvent, getInstallId, classifyInstall, INSTALL_ID_PATH, SESSION_ID };
|
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
|
};
|