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.
Files changed (37) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.well-known/mcp/server-card.json +1 -1
  4. package/README.md +1 -0
  5. package/adapters/chatgpt/openapi.yaml +10 -0
  6. package/adapters/claude/.mcp.json +2 -2
  7. package/adapters/mcp/server-stdio.js +109 -1
  8. package/adapters/opencode/opencode.json +1 -1
  9. package/bin/cli.js +247 -30
  10. package/config/mcp-allowlists.json +12 -6
  11. package/openapi/openapi.yaml +10 -0
  12. package/package.json +29 -5
  13. package/public/agent-manager.html +1 -1
  14. package/public/agents-cost-savings.html +151 -0
  15. package/public/ai-malpractice-prevention.html +183 -0
  16. package/public/codex-enterprise.html +123 -0
  17. package/public/codex-plugin.html +1 -1
  18. package/public/dashboard.html +18 -5
  19. package/public/index.html +13 -6
  20. package/public/lessons.html +34 -0
  21. package/public/numbers.html +2 -2
  22. package/public/pricing.html +1 -1
  23. package/scripts/auto-wire-hooks.js +14 -0
  24. package/scripts/build-metadata.js +32 -13
  25. package/scripts/cli-telemetry.js +6 -1
  26. package/scripts/gate-stats.js +89 -0
  27. package/scripts/gates-engine.js +133 -6
  28. package/scripts/hook-runtime.js +9 -3
  29. package/scripts/meta-agent-loop.js +32 -0
  30. package/scripts/pro-local-dashboard.js +4 -4
  31. package/scripts/rate-limiter.js +7 -1
  32. package/scripts/self-healing-check.js +193 -0
  33. package/scripts/silent-failure-cluster.js +512 -0
  34. package/scripts/telemetry-analytics.js +38 -0
  35. package/scripts/tool-registry.js +18 -0
  36. package/scripts/workflow-sentinel.js +6 -1
  37. 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&amp;utm_medium=codex_enterprise_page&amp;utm_campaign=codex_enterprise_sprint&amp;cta_id=codex_enterprise_sprint_intake&amp;cta_placement=codex_enterprise_page" class="cta">Start the Workflow Hardening Sprint</a>
107
+ <a href="/checkout/pro?utm_source=website&amp;utm_medium=codex_enterprise_page&amp;utm_campaign=pro_upgrade&amp;cta_id=codex_enterprise_pro_checkout&amp;cta_placement=codex_enterprise_page&amp;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>
@@ -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="/public/llm-context.md" type="text/markdown">
15
+ <link rel="llm-context" href="/llm-context.md" type="text/markdown">
16
16
 
17
17
  <script type="application/ld+json">
18
18
  {
@@ -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=positive" 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=negative" 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>
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
- var tabEl = document.querySelector('[onclick*="' + name + '"]');
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) contentEl.classList.add('active');
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.21.2">
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:'watch_demo'})}catch(_){};sendFirstPartyTelemetry('hero_demo_clicked',{ctaId:'hero_watch_demo',ctaPlacement:'hero'});" class="btn-free" style="font-size:15px;padding:14px 22px;">▶ Watch the 90-second demo</a>
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 ships with a plugin bundle in this repo. 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>
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.21.2</span>
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' });
@@ -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
 
@@ -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.21.2",
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.21.2</div>
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>
@@ -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="/#features">Features</a>
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
- if (envBuildSha || envGeneratedAt) {
27
- return {
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: normalizeNullableText(parsed.buildSha),
39
- generatedAt: normalizeNullableText(parsed.generatedAt),
46
+ buildSha: fileBuildSha,
47
+ generatedAt: fileGeneratedAt || envGeneratedAt,
40
48
  };
41
- } catch {
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: null,
45
- generatedAt: null,
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() }) {
@@ -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 };
@@ -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
  };