thumbgate 1.22.0 → 1.23.1

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/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.0">
22
+ <meta name="thumbgate-version" content="1.23.1">
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">
@@ -817,6 +817,37 @@ __GA_BOOTSTRAP__
817
817
  </div>
818
818
  </section>
819
819
 
820
+ <section class="compatibility" id="deterministic-prevention">
821
+ <div class="container">
822
+ <div class="section-label">Deterministic Prevention</div>
823
+ <h2 class="section-title">Native thumbs are a black box. ThumbGate is the inspectable control layer.</h2>
824
+ <p style="text-align:center;font-size:16px;color:var(--text-muted);max-width:900px;margin:0 auto 28px;line-height:1.7;">Codex, Claude Code, ChatGPT, and other agent surfaces can collect preference signals, but you usually cannot see exactly what changed, which rule will fire, or why a future tool call is allowed. ThumbGate keeps the prevention layer outside the model: typed feedback becomes a local lesson, repeated mistakes become explicit rules, and every block names the matched rule, source lesson, tool call, and audit event.</p>
825
+ <div class="agent-grid">
826
+ <div class="agent-card">
827
+ <h3>Black-box memory</h3>
828
+ <p>Native thumbs and vendor memories may improve future behavior, but they do not give teams a deterministic allow/block contract at the moment an agent touches files, terminals, APIs, or CI.</p>
829
+ </div>
830
+ <div class="agent-card">
831
+ <h3>Inspectable ThumbGate memory</h3>
832
+ <p>Lessons live in your ThumbGate store, can be searched, exported as JSONL or DPO pairs, and traced back to the exact correction that created the rule.</p>
833
+ </div>
834
+ <div class="agent-card">
835
+ <h3>Rules before execution</h3>
836
+ <p>The final decision is not another model opinion. ThumbGate checks tool name, arguments, working directory, command shape, confidence, and required evidence before the action runs.</p>
837
+ </div>
838
+ </div>
839
+ <div style="margin:26px auto 0;max-width:960px;border:1px solid rgba(34,211,238,0.18);border-radius:8px;background:rgba(34,211,238,0.05);padding:18px 20px;">
840
+ <h3 style="margin:0 0 10px;color:var(--text);font-size:18px;">Why this matters now</h3>
841
+ <ul style="margin:0;padding-left:20px;color:var(--text-muted);line-height:1.7;">
842
+ <li><strong>Agent security is now mainstream risk.</strong> Coding agents run shell commands, write files, query databases, and chain actions with developer permissions, so unattended autonomy needs a local policy boundary.</li>
843
+ <li><strong>MCP adoption is accelerating.</strong> More tools are becoming agent-callable through shared protocols, which means one cross-agent governance layer beats one-off prompt rules per app.</li>
844
+ <li><strong>Repeated failures waste cash and trust.</strong> Every repeat burns tokens, review time, and release confidence. ThumbGate turns the first correction into a reusable prevention check.</li>
845
+ </ul>
846
+ <p style="margin:12px 0 0;font-size:13px;color:var(--text-dim);">Sources to verify the market timing: <a href="https://www.docker.com/blog/ai-coding-agent-horror-stories-security-risks/" target="_blank" rel="noopener" style="color:var(--cyan);">Docker on AI coding agent security risks</a>, <a href="https://www.techradar.com/pro/how-ai-agents-are-wrecking-havoc-in-legacy-security-setups-and-enterprises-are-catching-up" target="_blank" rel="noopener" style="color:var(--cyan);">TechRadar on enterprise agent security pressure</a>, and <a href="https://www.techradar.com/pro/zendesk-becomes-the-latest-to-adopt-mcp-to-futureproof-customers-in-the-ai-first-era" target="_blank" rel="noopener" style="color:var(--cyan);">current MCP adoption coverage</a>.</p>
847
+ </div>
848
+ </div>
849
+ </section>
850
+
820
851
  <section class="marketing-deep-dive" style="padding:28px 0 10px;">
821
852
  <div class="container" style="max-width:1240px;">
822
853
  <div class="section-label">Status bar proof</div>
@@ -1492,7 +1523,7 @@ __GA_BOOTSTRAP__
1492
1523
  <a href="https://www.linkedin.com/in/igorganapolsky" target="_blank" rel="noopener">LinkedIn</a>
1493
1524
  <a href="/blog">Blog</a>
1494
1525
  </div>
1495
- <span class="footer-copy">© 2026 ThumbGate · MIT License · npm v1.22.0</span>
1526
+ <span class="footer-copy">© 2026 ThumbGate · MIT License · npm v1.23.1</span>
1496
1527
  </div>
1497
1528
  </footer>
1498
1529
 
@@ -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.22.0",
28
+ "softwareVersion": "1.23.1",
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.22.0</div>
205
+ <div class="freshness">Updated: 2026-05-07 · Version 1.23.1</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>
package/public/pro.html CHANGED
@@ -815,6 +815,28 @@ __GA_BOOTSTRAP__
815
815
  </div>
816
816
  </section>
817
817
 
818
+ <section class="section" id="deterministic-loop">
819
+ <div class="container">
820
+ <div class="section-label">Why Pro now</div>
821
+ <h2 class="section-title">Black-box thumbs do not prove prevention. Pro gives the operator an audit loop.</h2>
822
+ <p class="section-intro">Native rating buttons can tell a vendor that an answer felt wrong. ThumbGate Pro gives you the operational record: the correction, the lesson, the rule, the blocked tool call, and the export path.</p>
823
+ <div class="grid-3">
824
+ <div class="feature-card">
825
+ <h3>Inspectable memory</h3>
826
+ <p>Search the exact lesson that came from a thumbs-down and see whether it is still active, warning-only, or blocking.</p>
827
+ </div>
828
+ <div class="feature-card">
829
+ <h3>Deterministic checks</h3>
830
+ <p>The enforcement layer evaluates tool name, arguments, working directory, command shape, confidence, and required evidence before the action runs.</p>
831
+ </div>
832
+ <div class="feature-card">
833
+ <h3>Exportable proof</h3>
834
+ <p>Take the same correction history into JSONL, DPO export, review packets, and team rollout conversations instead of trusting hidden memory.</p>
835
+ </div>
836
+ </div>
837
+ </div>
838
+ </section>
839
+
818
840
  <section class="section" id="why-pay">
819
841
  <div class="container">
820
842
  <div class="section-label">Why operators pay</div>
@@ -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 };
@@ -35,6 +35,75 @@ function normalizeSeatCount(value, fallback = TEAM_MIN_SEATS) {
35
35
  return Math.max(TEAM_MIN_SEATS, Math.round(parsed));
36
36
  }
37
37
 
38
+ function trackedProUrl(source = 'cli_receipt', content = 'value_receipt') {
39
+ try {
40
+ const url = new URL(PRO_MONTHLY_PAYMENT_LINK);
41
+ url.searchParams.set('utm_source', source);
42
+ url.searchParams.set('utm_medium', 'cli');
43
+ url.searchParams.set('utm_campaign', 'pro_conversion');
44
+ url.searchParams.set('utm_content', content);
45
+ return url.toString();
46
+ } catch (_) {
47
+ return PRO_MONTHLY_PAYMENT_LINK;
48
+ }
49
+ }
50
+
51
+ function pluralize(count, singular, plural = `${singular}s`) {
52
+ return Number(count) === 1 ? singular : plural;
53
+ }
54
+
55
+ function buildCaptureReceipt({ signal, feedbackId, memoryId, actionType } = {}) {
56
+ const normalizedSignal = String(signal || '').toUpperCase() || 'UNKNOWN';
57
+ const lines = [
58
+ '',
59
+ 'Value receipt',
60
+ '─'.repeat(50),
61
+ ` Stored proof : ${normalizedSignal} feedback${feedbackId ? ` (${feedbackId})` : ''}`,
62
+ memoryId ? ` Local memory : ${memoryId}` : ' Local memory : saved locally',
63
+ actionType ? ` Rule pressure : ${actionType}` : ' Rule pressure : available for promotion',
64
+ ' Next proof : npx thumbgate stats',
65
+ ' Cost proof : npx thumbgate cost',
66
+ '',
67
+ ` Solo Pro : ${PRO_PRICE_LABEL} for dashboard, search, exports, sync`,
68
+ ` Upgrade : ${trackedProUrl('cli_capture_receipt', actionType || normalizedSignal.toLowerCase())}`,
69
+ ` Team path : ${TEAM_PRICE_LABEL}; start with one repeated workflow failure`,
70
+ ' https://thumbgate.ai/#workflow-sprint-intake',
71
+ '',
72
+ ];
73
+ return lines.join('\n');
74
+ }
75
+
76
+ function buildStatsReceipt(stats = {}) {
77
+ const negatives = Number(stats.negatives || stats.totalNegative || 0);
78
+ const blocked = Number(stats.gatesBlocked || stats.blocked || 0);
79
+ const warned = Number(stats.gatesWarned || stats.warned || 0);
80
+ const gates = Number(stats.totalGates || 0);
81
+ const autoPromoted = Number(stats.autoPromotedGates || 0);
82
+ const hasFriction = negatives > 0 || blocked > 0 || warned > 0 || gates > 0;
83
+ if (!hasFriction) return '';
84
+
85
+ const interventions = blocked + warned;
86
+ const lines = [
87
+ '',
88
+ 'Paid-intent next step',
89
+ '─'.repeat(50),
90
+ ];
91
+ if (interventions > 0) {
92
+ lines.push(` Proof already seen : ${interventions} gate ${pluralize(interventions, 'intervention')}`);
93
+ }
94
+ if (gates > 0) {
95
+ lines.push(` Active prevention : ${gates} ${pluralize(gates, 'gate')} (${autoPromoted} auto-promoted)`);
96
+ }
97
+ if (negatives > 0) {
98
+ lines.push(` Failure pressure : ${negatives} negative ${pluralize(negatives, 'signal')}`);
99
+ }
100
+ lines.push(' Show the buyer : npx thumbgate cost');
101
+ lines.push(` Solo Pro : ${trackedProUrl('cli_stats_receipt', 'proof_seen')}`);
102
+ lines.push(' Team workflow : https://thumbgate.ai/#workflow-sprint-intake');
103
+ lines.push('');
104
+ return lines.join('\n');
105
+ }
106
+
38
107
  module.exports = {
39
108
  PRO_MONTHLY_PAYMENT_LINK,
40
109
  PRO_ANNUAL_PAYMENT_LINK,
@@ -51,4 +120,7 @@ module.exports = {
51
120
  normalizePlanId,
52
121
  normalizeBillingCycle,
53
122
  normalizeSeatCount,
123
+ buildCaptureReceipt,
124
+ buildStatsReceipt,
125
+ trackedProUrl,
54
126
  };
@@ -7,7 +7,7 @@ const crypto = require('crypto');
7
7
  const { execSync, execFileSync } = require('child_process');
8
8
  const { loadOptionalModule } = require('./private-core-boundary');
9
9
 
10
- const { isProTier, FREE_TIER_MAX_GATES } = require('./rate-limiter');
10
+ const { isProTier, isInTrialPeriod, FREE_TIER_MAX_GATES, FREE_TIER_DAILY_BLOCKS, todayKey } = require('./rate-limiter');
11
11
  const {
12
12
  DEFAULT_BASE_BRANCH,
13
13
  evaluateOperationalIntegrity,
@@ -466,6 +466,69 @@ function recordStat(gateId, action, gate) {
466
466
  }
467
467
  }
468
468
 
469
+ // ---------------------------------------------------------------------------
470
+ // Free-tier daily block cap
471
+ // ---------------------------------------------------------------------------
472
+
473
+ /**
474
+ * Count today's gate blocks from stats. Free tier gets FREE_TIER_DAILY_BLOCKS
475
+ * blocks/day. After the limit, deny → warn + upgrade CTA so the action proceeds
476
+ * but the user sees they lost protection.
477
+ */
478
+ function getTodayBlockCount() {
479
+ const stats = loadStats();
480
+ const today = todayKey();
481
+ if (!stats.dailyBlocks || !stats.dailyBlocks[today]) return 0;
482
+ return stats.dailyBlocks[today];
483
+ }
484
+
485
+ function incrementTodayBlockCount() {
486
+ const stats = loadStats();
487
+ const today = todayKey();
488
+ if (!stats.dailyBlocks) stats.dailyBlocks = {};
489
+ // Clean old dates (keep only last 7 days to prevent unbounded growth)
490
+ const keys = Object.keys(stats.dailyBlocks);
491
+ if (keys.length > 7) {
492
+ keys.sort();
493
+ for (const k of keys.slice(0, keys.length - 7)) {
494
+ delete stats.dailyBlocks[k];
495
+ }
496
+ }
497
+ stats.dailyBlocks[today] = (stats.dailyBlocks[today] || 0) + 1;
498
+ saveStats(stats);
499
+ return stats.dailyBlocks[today];
500
+ }
501
+
502
+ /**
503
+ * If the user is free-tier and has exceeded daily block limit, downgrade
504
+ * a deny result to a warn with an upgrade CTA. Returns null if no cap applies.
505
+ */
506
+ function applyDailyBlockCap(denyResult) {
507
+ // Pro, trial, CI, and THUMBGATE_NO_RATE_LIMIT users are uncapped
508
+ if (isProTier()) return null;
509
+ if (process.env.CI || process.env.GITHUB_ACTIONS) return null;
510
+
511
+ const todayCount = getTodayBlockCount();
512
+ if (todayCount < FREE_TIER_DAILY_BLOCKS) {
513
+ // Under limit: allow the block, increment counter
514
+ incrementTodayBlockCount();
515
+ return null;
516
+ }
517
+
518
+ // Over limit: downgrade deny → warn with upgrade CTA
519
+ const remaining = 0;
520
+ return {
521
+ decision: 'warn',
522
+ gate: denyResult.gate,
523
+ message: `⚠️ ${denyResult.message}\n\n🔓 Daily protection limit reached (${FREE_TIER_DAILY_BLOCKS}/${FREE_TIER_DAILY_BLOCKS} blocks used). This action was allowed through. Upgrade for unlimited protection: https://thumbgate.ai/go/pro`,
524
+ severity: denyResult.severity,
525
+ reasoning: (denyResult.reasoning || []).concat([
526
+ `Free-tier daily block limit (${FREE_TIER_DAILY_BLOCKS}) exceeded — deny downgraded to warn`,
527
+ ]),
528
+ dailyBlockCapApplied: true,
529
+ };
530
+ }
531
+
469
532
  // ---------------------------------------------------------------------------
470
533
  // Reasoning chain builder
471
534
  // ---------------------------------------------------------------------------
@@ -1491,11 +1554,19 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
1491
1554
  });
1492
1555
 
1493
1556
  if (gate.action === 'block') {
1557
+ const denyResult = { decision: 'deny', gate: gate.id, message, severity: gate.severity, reasoning };
1558
+ // Free-tier daily block cap: after N blocks/day, deny → warn + upgrade CTA
1559
+ const cappedResult = applyDailyBlockCap(denyResult);
1560
+ if (cappedResult) {
1561
+ recordStat(gate.id, 'warn', gate);
1562
+ const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message: cappedResult.message, severity: gate.severity, source: 'gates-engine', dailyBlockCapApplied: true });
1563
+ auditToFeedback(auditRecord);
1564
+ return cappedResult;
1565
+ }
1494
1566
  recordStat(gate.id, 'block', gate);
1495
- const result = { decision: 'deny', gate: gate.id, message, severity: gate.severity, reasoning };
1496
1567
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
1497
1568
  auditToFeedback(auditRecord);
1498
- return result;
1569
+ return denyResult;
1499
1570
  }
1500
1571
 
1501
1572
  if (gate.action === 'approve') {
@@ -1653,11 +1724,19 @@ function evaluateGates(toolName, toolInput, configPath) {
1653
1724
  const reasoning = buildReasoning(gate, toolName, toolInput, matchDetails);
1654
1725
 
1655
1726
  if (gate.action === 'block') {
1727
+ const denyResult = { decision: 'deny', gate: gate.id, message, severity: gate.severity, reasoning };
1728
+ // Free-tier daily block cap: after N blocks/day, deny → warn + upgrade CTA
1729
+ const cappedResult = applyDailyBlockCap(denyResult);
1730
+ if (cappedResult) {
1731
+ recordStat(gate.id, 'warn', gate);
1732
+ const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'warn', gateId: gate.id, message: cappedResult.message, severity: gate.severity, source: 'gates-engine', dailyBlockCapApplied: true });
1733
+ auditToFeedback(auditRecord);
1734
+ return cappedResult;
1735
+ }
1656
1736
  recordStat(gate.id, 'block', gate);
1657
- const result = { decision: 'deny', gate: gate.id, message, severity: gate.severity, reasoning };
1658
1737
  const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'deny', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
1659
1738
  auditToFeedback(auditRecord);
1660
- return result;
1739
+ return denyResult;
1661
1740
  }
1662
1741
 
1663
1742
  if (gate.action === 'approve') {
@@ -1856,6 +1935,35 @@ function buildReminderOutput(context) {
1856
1935
  };
1857
1936
  }
1858
1937
 
1938
+ // ---------------------------------------------------------------------------
1939
+ // Upgrade nudge: surfaces Pro value at usage milestones and trial expiry.
1940
+ // Block-action Pro CTA: brief upgrade mention after a deny/warn decision.
1941
+ // Highest-intent moment — user just saw ThumbGate save them from a mistake.
1942
+ // ---------------------------------------------------------------------------
1943
+
1944
+ function buildBlockActionProCta() {
1945
+ try {
1946
+ if (process.env.THUMBGATE_NO_NUDGE === '1') return null;
1947
+ if (process.env.CI || process.env.GITHUB_ACTIONS) return null;
1948
+ if (isProTier()) return null;
1949
+ if (isInTrialPeriod()) return null; // Already have full access
1950
+
1951
+ const stats = loadStats();
1952
+ const totalBlocks = stats.blocked || 0;
1953
+ if (totalBlocks < 5) return null; // Too early — let them experience the product
1954
+
1955
+ if (totalBlocks < 25) {
1956
+ return '\n\n💡 Pro: sync rules across machines + dashboard analytics → thumbgate.ai/go/pro';
1957
+ }
1958
+ if (totalBlocks < 100) {
1959
+ return `\n\n💡 ${totalBlocks} actions blocked. Pro keeps rules in sync everywhere → thumbgate.ai/go/pro ($19/mo)`;
1960
+ }
1961
+ return `\n\n💡 ${totalBlocks} mistakes caught. Your team could use this → thumbgate.ai/go/pro`;
1962
+ } catch (_) {
1963
+ return null;
1964
+ }
1965
+ }
1966
+
1859
1967
  function formatOutput(result, behavioralContext) {
1860
1968
  if (!result) {
1861
1969
  // No gate matched — inject behavioral context if available
@@ -1874,11 +1982,12 @@ function formatOutput(result, behavioralContext) {
1874
1982
  if (result.decision === 'deny') {
1875
1983
  const reminder = behavioralContext ? buildReminderOutput(behavioralContext) : {};
1876
1984
  const reminderSuffix = behavioralContext ? `\n\nSystem reminder:\n${behavioralContext}` : '';
1985
+ const proCta = buildBlockActionProCta() || '';
1877
1986
  return JSON.stringify({
1878
1987
  hookSpecificOutput: {
1879
1988
  ...reminder,
1880
1989
  permissionDecision: 'deny',
1881
- permissionDecisionReason: `[GATE:${result.gate}] ${result.message}${reasoningSuffix}${reminderSuffix}`,
1990
+ permissionDecisionReason: `[GATE:${result.gate}] ${result.message}${reasoningSuffix}${reminderSuffix}${proCta}`,
1882
1991
  },
1883
1992
  });
1884
1993
  }
@@ -2468,6 +2577,10 @@ module.exports = {
2468
2577
  isRemoteSideEffectCommand,
2469
2578
  evaluateLocalOnlyRemoteSideEffectGate,
2470
2579
  PR_THREAD_RESOLUTION_ACTION,
2580
+ buildBlockActionProCta,
2581
+ applyDailyBlockCap,
2582
+ getTodayBlockCount,
2583
+ incrementTodayBlockCount,
2471
2584
  };
2472
2585
 
2473
2586
  // ---------------------------------------------------------------------------
@@ -282,6 +282,8 @@ function buildPromotedGate(candidate, metrics, runId) {
282
282
  occurrences: metrics.hits,
283
283
  promotedAt: new Date().toISOString(),
284
284
  source: 'meta-agent',
285
+ // origin distinguishes silent-failure-clustered candidates from feedback-derived ones
286
+ origin: candidate.origin || 'user-feedback',
285
287
  runId,
286
288
  score: parseFloat(metrics.score.toFixed(3)),
287
289
  hitRate: parseFloat(metrics.hitRate.toFixed(3)),
@@ -371,6 +373,34 @@ async function runMetaAgentLoop({ dryRun = false, verbose = false } = {}) {
371
373
  candidates = generateCandidatesHeuristic(failures, blockPatterns);
372
374
  }
373
375
 
376
+ // Tag existing-pipeline candidates with their origin so downstream precision
377
+ // measurement (silentFailureDerivedGates vs user-feedback-derived) is possible.
378
+ candidates = candidates.map((c) => (c.origin ? c : { ...c, origin: 'user-feedback' }));
379
+
380
+ // Step 3b: Silent-failure clustering — behind THUMBGATE_SILENT_FAILURE_CLUSTERING=1.
381
+ // Candidates flow through the SAME scoring / fp-rate eval below; we do not
382
+ // bypass any guardrail. Off by default to preserve existing behavior.
383
+ let silentFailureStats = null;
384
+ if (process.env.THUMBGATE_SILENT_FAILURE_CLUSTERING === '1') {
385
+ try {
386
+ const { generateSilentFailureCandidates } = require('./silent-failure-cluster');
387
+ const sfResult = generateSilentFailureCandidates({ feedbackLogPath });
388
+ silentFailureStats = sfResult.stats;
389
+ if (sfResult.candidates && sfResult.candidates.length > 0) {
390
+ candidates = candidates.concat(sfResult.candidates);
391
+ }
392
+ if (verbose) {
393
+ process.stdout.write(
394
+ `[meta-agent] silent-failure-cluster: candidates=${sfResult.candidates.length} `
395
+ + `failed=${sfResult.stats.failedCalls} clusters=${sfResult.stats.clusters} `
396
+ + `skipped=${sfResult.stats.skippedReason || 'none'}\n`
397
+ );
398
+ }
399
+ } catch (err) {
400
+ if (verbose) process.stdout.write(`[meta-agent] silent-failure-cluster failed (non-fatal): ${err.message}\n`);
401
+ }
402
+ }
403
+
374
404
  if (verbose) {
375
405
  process.stdout.write(`[meta-agent] candidates generated: ${candidates.length} (mode=${analysisMode})\n`);
376
406
  }
@@ -507,6 +537,8 @@ async function runMetaAgentLoop({ dryRun = false, verbose = false } = {}) {
507
537
  skipped: evolutionResult.skipped || false,
508
538
  }
509
539
  : null,
540
+ silentFailureCluster: silentFailureStats,
541
+ silentFailureDerivedGates: promotedGates.filter((g) => g.origin === 'silent-failure-cluster').length,
510
542
  };
511
543
 
512
544
  if (!dryRun) {
@@ -16,7 +16,7 @@ const CREATOR_SYNTHETIC_KEY = process.env.THUMBGATE_DEV_KEY || '';
16
16
  * 2. Env var: THUMBGATE_DEV_BYPASS=[set via THUMBGATE_DEV_SECRET env var]
17
17
  * Requires a specific non-obvious value (not boolean) to prevent accidental activation.
18
18
  */
19
- function isCreatorDev({ env = process.env, homeDir = os.homedir() } = {}) {
19
+ function isCreatorDev({ env = process.env, homeDir = env.HOME || env.USERPROFILE || os.homedir() } = {}) {
20
20
  // Layer 1: env var with specific value
21
21
  if (CREATOR_BYPASS_VALUE && String(env[CREATOR_BYPASS_ENV] || '') === CREATOR_BYPASS_VALUE) {
22
22
  return true;
@@ -37,7 +37,7 @@ function isCreatorDev({ env = process.env, homeDir = os.homedir() } = {}) {
37
37
  * with any non-empty bypass value. No env var needed — just the config file.
38
38
  * Used by the server to skip auth on localhost during local development.
39
39
  */
40
- function hasDevOverride(homeDir = os.homedir()) {
40
+ function hasDevOverride(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
41
41
  // Disabled during test runs to avoid interfering with auth assertions
42
42
  if (process.env.NODE_TEST_CONTEXT || process.env.THUMBGATE_TESTING) return false;
43
43
  try {
@@ -47,11 +47,11 @@ function hasDevOverride(homeDir = os.homedir()) {
47
47
  } catch { return false; }
48
48
  }
49
49
 
50
- function getLicenseDir(homeDir = os.homedir()) {
50
+ function getLicenseDir(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
51
51
  return path.join(homeDir, '.thumbgate');
52
52
  }
53
53
 
54
- function getLicensePath(homeDir = os.homedir()) {
54
+ function getLicensePath(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
55
55
  return path.join(getLicenseDir(homeDir), 'license.json');
56
56
  }
57
57
 
@@ -29,6 +29,7 @@ const FREE_TIER_LIMITS = {
29
29
  };
30
30
 
31
31
  const FREE_TIER_MAX_GATES = 5; // 5 active prevention rules on free; Pro is unlimited
32
+ const FREE_TIER_DAILY_BLOCKS = 10; // 10 gate blocks/day on free; after limit, deny → warn + upgrade CTA
32
33
 
33
34
  const UPGRADE_MESSAGE = `Pro: ${PRO_PRICE_LABEL} — unlimited rules, recall, lesson search, dashboard, and exports: ${PRO_MONTHLY_PAYMENT_LINK}\n Team: ${TEAM_PRICE_LABEL} after workflow qualification.`;
34
35
 
@@ -45,7 +46,10 @@ function getInstallAgeDays() {
45
46
  try {
46
47
  const { INSTALL_ID_PATH } = require('./cli-telemetry');
47
48
  if (!fs.existsSync(INSTALL_ID_PATH)) return null;
48
- const created = fs.statSync(INSTALL_ID_PATH).birthtimeMs || fs.statSync(INSTALL_ID_PATH).mtimeMs;
49
+ // Use mtimeMs birthtimeMs is unreliable on Linux (ext4 doesn't backdate creation time).
50
+ // The install-id file is written once at install, so mtime == creation time in practice.
51
+ const stat = fs.statSync(INSTALL_ID_PATH);
52
+ const created = stat.mtimeMs || stat.birthtimeMs;
49
53
  if (!Number.isFinite(created) || created <= 0) return null;
50
54
  return (Date.now() - created) / (1000 * 60 * 60 * 24);
51
55
  } catch (_) {
@@ -211,6 +215,7 @@ function getUsage(action, authContext) {
211
215
  module.exports = {
212
216
  checkLimit,
213
217
  getUsage,
218
+ getInstallAgeDays,
214
219
  isProTier,
215
220
  isInTrialPeriod,
216
221
  trialDaysRemaining,
@@ -219,6 +224,7 @@ module.exports = {
219
224
  todayKey,
220
225
  FREE_TIER_LIMITS,
221
226
  FREE_TIER_MAX_GATES,
227
+ FREE_TIER_DAILY_BLOCKS,
222
228
  TRIAL_DAYS,
223
229
  UPGRADE_MESSAGE,
224
230
  PAYWALL_MESSAGES,