thumbgate 1.16.20 → 1.16.22
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 +3 -2
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bench/programbench-smoke.json +71 -0
- package/bench/thumbgate-bench.json +131 -0
- package/bin/cli.js +79 -2
- package/config/pro/constraints-pro.json +57 -0
- package/config/pro/prevention-rules-pro.md +27 -0
- package/config/pro/reminders-pro.json +38 -0
- package/config/pro/thompson-presets.json +38 -0
- package/package.json +16 -8
- package/public/dashboard.html +1 -1
- package/public/guide.html +5 -3
- package/public/index.html +43 -31
- package/public/lessons.html +1 -1
- package/public/numbers.html +45 -32
- package/public/pro.html +31 -88
- package/scripts/billing.js +3 -3
- package/scripts/gate-stats.js +29 -8
- package/scripts/harness-selector.js +188 -0
- package/scripts/rag-precision-guardrails.js +63 -1
- package/scripts/rate-limiter.js +1 -1
- package/scripts/reasoning-efficiency-guardrails.js +73 -1
- package/scripts/thumbgate-bench.js +707 -0
- package/src/api/server.js +66 -13
package/public/pro.html
CHANGED
|
@@ -14,7 +14,6 @@ __GOOGLE_SITE_VERIFICATION_META__
|
|
|
14
14
|
<link rel="icon" type="image/png" href="/thumbgate-icon.png">
|
|
15
15
|
<link rel="apple-touch-icon" href="/assets/brand/thumbgate-mark.svg">
|
|
16
16
|
<meta property="og:image" content="/og.png">
|
|
17
|
-
<meta name="keywords" content="ThumbGate Pro, AI agent reliability, pre-action checks, DPO export, local dashboard, review-ready evidence, Claude Code reliability, Codex reliability, Cursor reliability">
|
|
18
17
|
|
|
19
18
|
<script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
|
|
20
19
|
__GA_BOOTSTRAP__
|
|
@@ -30,91 +29,14 @@ __GA_BOOTSTRAP__
|
|
|
30
29
|
<script type="application/ld+json">
|
|
31
30
|
{
|
|
32
31
|
"@context": "https://schema.org",
|
|
33
|
-
"@type": "SoftwareApplication",
|
|
34
|
-
"name": "ThumbGate Pro",
|
|
35
|
-
"alternateName": "thumbgate pro",
|
|
36
|
-
"applicationCategory": "DeveloperApplication",
|
|
37
|
-
"operatingSystem": "Cross-platform, Node.js >=18.18.0",
|
|
38
|
-
"description": "Paid ThumbGate lane for individual operators who want a personal local dashboard, DPO export, review-ready evidence, and founder support for risky AI coding workflows.",
|
|
39
|
-
"url": "__APP_ORIGIN__/pro",
|
|
40
|
-
"downloadUrl": "https://www.npmjs.com/package/thumbgate",
|
|
41
|
-
"dateModified": "2026-04-20",
|
|
42
|
-
"creator": {
|
|
43
|
-
"@type": "Person",
|
|
44
|
-
"name": "Igor Ganapolsky",
|
|
45
|
-
"url": "https://github.com/IgorGanapolsky",
|
|
46
|
-
"sameAs": [
|
|
47
|
-
"https://github.com/IgorGanapolsky",
|
|
48
|
-
"https://www.linkedin.com/in/igorganapolsky"
|
|
49
|
-
]
|
|
50
|
-
},
|
|
51
|
-
"featureList": [
|
|
52
|
-
"Personal local dashboard",
|
|
53
|
-
"Visual check debugger",
|
|
54
|
-
"DPO export from real thumbs-down corrections",
|
|
55
|
-
"Auto-connect running agents after activation",
|
|
56
|
-
"Founder support on risky workflows",
|
|
57
|
-
"Model Hardening Advisor"
|
|
58
|
-
],
|
|
59
|
-
"offers": [
|
|
60
|
-
{
|
|
61
|
-
"@type": "Offer",
|
|
62
|
-
"name": "ThumbGate Pro Monthly",
|
|
63
|
-
"price": "__PRO_PRICE_DOLLARS__",
|
|
64
|
-
"priceCurrency": "USD",
|
|
65
|
-
"url": "__APP_ORIGIN__/checkout/pro?plan_id=pro&billing_cycle=monthly&landing_path=%2Fpro",
|
|
66
|
-
"description": "Monthly Pro for individual operators who want a personal local dashboard and proof-ready exports."
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
"@type": "Offer",
|
|
70
|
-
"name": "ThumbGate Pro Annual",
|
|
71
|
-
"price": "149",
|
|
72
|
-
"priceCurrency": "USD",
|
|
73
|
-
"url": "__APP_ORIGIN__/checkout/pro?plan_id=pro&billing_cycle=annual&landing_path=%2Fpro",
|
|
74
|
-
"description": "Annual Pro for operators who want the dashboard, DPO export, and founder support at a lower effective monthly price."
|
|
75
|
-
}
|
|
76
|
-
]
|
|
32
|
+
"@type": "SoftwareApplication", "name": "ThumbGate Pro", "applicationCategory": "DeveloperApplication", "operatingSystem": "Cross-platform, Node.js >=18.18.0", "description": "Paid ThumbGate lane for individual operators who want a personal local dashboard, DPO export, review-ready evidence, and founder support for risky AI coding workflows.", "url": "__APP_ORIGIN__/pro", "downloadUrl": "https://www.npmjs.com/package/thumbgate", "dateModified": "2026-04-20", "creator": { "@type": "Person", "name": "Igor Ganapolsky", "url": "https://github.com/IgorGanapolsky" }, "offers": [{ "@type": "Offer", "name": "ThumbGate Pro Monthly", "price": "__PRO_PRICE_DOLLARS__", "priceCurrency": "USD", "url": "__APP_ORIGIN__/checkout/pro?plan_id=pro&billing_cycle=monthly&landing_path=%2Fpro" }, { "@type": "Offer", "name": "ThumbGate Pro Annual", "price": "149", "priceCurrency": "USD", "url": "__APP_ORIGIN__/checkout/pro?plan_id=pro&billing_cycle=annual&landing_path=%2Fpro" }]
|
|
77
33
|
}
|
|
78
34
|
</script>
|
|
79
35
|
|
|
80
36
|
<script type="application/ld+json">
|
|
81
37
|
{
|
|
82
38
|
"@context": "https://schema.org",
|
|
83
|
-
"@type": "FAQPage",
|
|
84
|
-
"mainEntity": [
|
|
85
|
-
{
|
|
86
|
-
"@type": "Question",
|
|
87
|
-
"name": "How is Pro different from the free install?",
|
|
88
|
-
"acceptedAnswer": {
|
|
89
|
-
"@type": "Answer",
|
|
90
|
-
"text": "Free keeps the local recall, checks, and MCP workflow. Pro adds a personal local dashboard, DPO export, auto-connect for running agents, and founder support for risky workflows."
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
"@type": "Question",
|
|
95
|
-
"name": "Does Pro require a cloud account?",
|
|
96
|
-
"acceptedAnswer": {
|
|
97
|
-
"@type": "Answer",
|
|
98
|
-
"text": "No. ThumbGate Pro is still local-first for the individual operator lane. Team is the hosted rollout lane when you need shared lessons, org visibility, and rollout review views."
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
"@type": "Question",
|
|
103
|
-
"name": "What happens after checkout?",
|
|
104
|
-
"acceptedAnswer": {
|
|
105
|
-
"@type": "Answer",
|
|
106
|
-
"text": "You activate Pro, connect your personal local dashboard, and your running agents can appear automatically so you can inspect blocked actions, lessons, and exports without adding a cloud dashboard dependency."
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
"@type": "Question",
|
|
111
|
-
"name": "When should I choose Team instead of Pro?",
|
|
112
|
-
"acceptedAnswer": {
|
|
113
|
-
"@type": "Answer",
|
|
114
|
-
"text": "Choose Team when one correction needs to protect multiple developers or agents, when you need a shared hosted lesson database, org dashboard visibility, or a workflow hardening pilot across shared repositories."
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
]
|
|
39
|
+
"@type": "FAQPage", "mainEntity": [{ "@type": "Question", "name": "How is Pro different from the free install?", "acceptedAnswer": { "@type": "Answer", "text": "Free keeps local recall, checks, and MCP. Pro adds the personal dashboard, DPO export, auto-connect, and founder support." } }, { "@type": "Question", "name": "Does Pro require a cloud account?", "acceptedAnswer": { "@type": "Answer", "text": "No. Pro stays local-first; Team is the hosted rollout lane for shared lessons, org visibility, and reviews." } }, { "@type": "Question", "name": "What happens after checkout?", "acceptedAnswer": { "@type": "Answer", "text": "You activate Pro, connect the local dashboard, and inspect blocked actions, lessons, and exports." } }, { "@type": "Question", "name": "When should I choose Team instead of Pro?", "acceptedAnswer": { "@type": "Answer", "text": "Choose Team when one correction needs to protect multiple developers or agents across shared repositories." } }]
|
|
118
40
|
}
|
|
119
41
|
</script>
|
|
120
42
|
|
|
@@ -796,7 +718,7 @@ __GA_BOOTSTRAP__
|
|
|
796
718
|
<a href="#pricing">Pricing</a>
|
|
797
719
|
<a href="#faq">FAQ</a>
|
|
798
720
|
<a href="/dashboard">Demo</a>
|
|
799
|
-
<a class="nav-cta
|
|
721
|
+
<a class="nav-cta" data-quick-read-link href="https://buy.stripe.com/aFa8wPgH29Lo4lH35V3sI0w" onclick="sendFirstPartyTelemetry('quick_read_checkout_started',{ctaId:'pro_page_nav_quick_read_checkout',price:19});sendGa4Event('begin_checkout',{currency:'USD',value:19,items:[{item_id:'quick_read',item_name:'AI Agent Failure Quick Read'}]});">Pay $19 quick read</a>
|
|
800
722
|
</div>
|
|
801
723
|
</div>
|
|
802
724
|
</nav>
|
|
@@ -807,8 +729,8 @@ __GA_BOOTSTRAP__
|
|
|
807
729
|
<div class="eyebrow">Paid lane for individual operators</div>
|
|
808
730
|
<h1>Buy the operator loop that proves your AI agent stopped repeating the mistake.</h1>
|
|
809
731
|
<p style="font-size:13px;opacity:0.8;margin-bottom:0.5rem;">Updated: <time datetime="2026-04-20">2026-04-20</time> · by <a href="https://github.com/IgorGanapolsky" style="color:inherit;">Igor Ganapolsky</a></p>
|
|
810
|
-
<p>ThumbGate Pro is
|
|
811
|
-
<p>
|
|
732
|
+
<p>ThumbGate Pro is for one operator who already hit a repeated AI-agent failure and now needs proof: what was blocked, why it was blocked, and what changed before the next risky run.</p>
|
|
733
|
+
<p>If you need help today, start with the $19 quick read: send one failed tool call or workflow snippet and get the likely rule shape plus proof check. Use Pro when you want the local dashboard and DPO export.</p>
|
|
812
734
|
<div class="hero-proof">
|
|
813
735
|
<div class="proof-pill">Personal local dashboard</div>
|
|
814
736
|
<div class="proof-pill">DPO export from real corrections</div>
|
|
@@ -816,7 +738,8 @@ __GA_BOOTSTRAP__
|
|
|
816
738
|
<div class="proof-pill">Founder support on risky flows</div>
|
|
817
739
|
</div>
|
|
818
740
|
<div class="hero-actions">
|
|
819
|
-
<a class="btn-primary
|
|
741
|
+
<a class="btn-primary" data-quick-read-link href="https://buy.stripe.com/aFa8wPgH29Lo4lH35V3sI0w" onclick="sendFirstPartyTelemetry('quick_read_checkout_started',{ctaId:'pro_page_hero_quick_read_checkout',price:19});sendGa4Event('begin_checkout',{currency:'USD',value:19,items:[{item_id:'quick_read',item_name:'AI Agent Failure Quick Read'}]});">Pay $19 quick read</a>
|
|
742
|
+
<a class="btn-secondary btn-pro-checkout" href="/checkout/pro?utm_source=website&utm_medium=pro_page_hero&utm_campaign=pro_pack&cta_id=pro_page_primary&cta_placement=hero&plan_id=pro&landing_path=%2Fpro">Start Pro dashboard</a>
|
|
820
743
|
<a class="btn-secondary btn-demo" href="/dashboard?utm_source=website&utm_medium=pro_page&utm_campaign=pro_pack">Open dashboard demo</a>
|
|
821
744
|
<a class="btn-ghost btn-free-path" href="/guide?utm_source=website&utm_medium=pro_page&utm_campaign=free_install">Stay on Free and install locally</a>
|
|
822
745
|
</div>
|
|
@@ -852,6 +775,18 @@ __GA_BOOTSTRAP__
|
|
|
852
775
|
<p>Visual check debugger, DPO export, auto-connect after activation, Model Hardening Advisor, and founder support for the risky flow you need to harden first.</p>
|
|
853
776
|
</div>
|
|
854
777
|
|
|
778
|
+
<div class="aside-card" data-pro-paid-recovery>
|
|
779
|
+
<div class="aside-kicker">Team workflow blocked?</div>
|
|
780
|
+
<h3>Buy the paid diagnostic</h3>
|
|
781
|
+
<p>Skip self-serve Pro and pay for the smallest useful review now.</p>
|
|
782
|
+
<div class="price-stack">
|
|
783
|
+
<a class="btn-secondary" data-first-rule-link href="https://buy.stripe.com/4gM6oHgH2bTw4lH6i73sI0z" onclick="sendFirstPartyTelemetry('first_failure_rule_checkout_started',{ctaId:'pro_page_first_failure_rule_checkout',price:1});sendGa4Event('begin_checkout',{currency:'USD',value:1,items:[{item_id:'first_failure_rule',item_name:'First AI Agent Failure Rule'}]});">Pay $1 first rule</a>
|
|
784
|
+
<a class="btn-secondary" data-quick-read-link href="https://buy.stripe.com/aFa8wPgH29Lo4lH35V3sI0w" onclick="sendFirstPartyTelemetry('quick_read_checkout_started',{ctaId:'pro_page_quick_read_checkout',price:19});">Pay $19 quick read</a>
|
|
785
|
+
<a class="btn-primary" data-sprint-diagnostic-link href="__SPRINT_DIAGNOSTIC_CHECKOUT_URL__" onclick="sendFirstPartyTelemetry('workflow_sprint_diagnostic_checkout_started',{ctaId:'pro_page_sprint_diagnostic_checkout'});sendGa4Event('begin_checkout',{currency:'USD',value:__SPRINT_DIAGNOSTIC_PRICE_DOLLARS__});">Pay $__SPRINT_DIAGNOSTIC_PRICE_DOLLARS__ diagnostic</a>
|
|
786
|
+
<a class="btn-secondary" data-workflow-sprint-link href="__WORKFLOW_SPRINT_CHECKOUT_URL__" onclick="sendFirstPartyTelemetry('workflow_sprint_checkout_started',{ctaId:'pro_page_workflow_sprint_checkout'});sendGa4Event('begin_checkout',{currency:'USD',value:__WORKFLOW_SPRINT_PRICE_DOLLARS__});">Pay $__WORKFLOW_SPRINT_PRICE_DOLLARS__ sprint</a>
|
|
787
|
+
</div>
|
|
788
|
+
</div>
|
|
789
|
+
|
|
855
790
|
<div class="aside-card">
|
|
856
791
|
<div class="aside-kicker">Keep the buyer path warm</div>
|
|
857
792
|
<h3>Save your work email before you decide</h3>
|
|
@@ -901,7 +836,7 @@ __GA_BOOTSTRAP__
|
|
|
901
836
|
<div class="grid-3">
|
|
902
837
|
<div class="feature-card">
|
|
903
838
|
<h3>Debug the exact block in minutes</h3>
|
|
904
|
-
<p>The visual
|
|
839
|
+
<p>The visual debugger shows each blocked action and check, so you stop guessing whether the Reliability Gateway is working.</p>
|
|
905
840
|
<ul>
|
|
906
841
|
<li>See the check, evidence, and lesson behind each deny.</li>
|
|
907
842
|
<li>Trust the system faster on deploys, migrations, and CI.</li>
|
|
@@ -909,7 +844,7 @@ __GA_BOOTSTRAP__
|
|
|
909
844
|
</div>
|
|
910
845
|
<div class="feature-card">
|
|
911
846
|
<h3>Turn corrections into DPO export</h3>
|
|
912
|
-
<p>Pro turns real thumbs-down corrections into
|
|
847
|
+
<p>Pro turns real thumbs-down corrections into preference pairs for model hardening, instead of leaving learning trapped in one operator's head.</p>
|
|
913
848
|
<ul>
|
|
914
849
|
<li>DPO export built from actual accepted vs rejected behavior.</li>
|
|
915
850
|
<li>Model Hardening Advisor helps decide when fine-tuning is worth it.</li>
|
|
@@ -917,7 +852,7 @@ __GA_BOOTSTRAP__
|
|
|
917
852
|
</div>
|
|
918
853
|
<div class="feature-card">
|
|
919
854
|
<h3>Ship proof, not just confidence</h3>
|
|
920
|
-
<p>When the next risky
|
|
855
|
+
<p>When the next risky review happens, you have evidence links, reports, and a founder-supported path to harden the failure that keeps returning.</p>
|
|
921
856
|
<ul>
|
|
922
857
|
<li>Review-ready evidence for one operator's high-risk workflows.</li>
|
|
923
858
|
<li>Founder support on force-pushes, deploys, migrations, and CI.</li>
|
|
@@ -1026,7 +961,7 @@ __GA_BOOTSTRAP__
|
|
|
1026
961
|
<h2>Stop losing time to the same AI-agent failure.</h2>
|
|
1027
962
|
<p>Start Pro, harden one repeated mistake, and keep the proof trail: blocked action, lesson, prevention rule, and export path.</p>
|
|
1028
963
|
<div class="hero-actions" style="justify-content:center;">
|
|
1029
|
-
<a class="btn-primary btn-pro-checkout" href="/checkout/pro?utm_source=website&utm_medium=pro_page_final&utm_campaign=pro_pack&cta_id=final_go_pro&cta_placement=final&plan_id=pro&landing_path=%2Fpro">Start
|
|
964
|
+
<a class="btn-primary btn-pro-checkout" href="/checkout/pro?utm_source=website&utm_medium=pro_page_final&utm_campaign=pro_pack&cta_id=final_go_pro&cta_placement=final&plan_id=pro&landing_path=%2Fpro">Start Pro Now</a>
|
|
1030
965
|
<a class="btn-secondary btn-demo" href="/dashboard?utm_source=website&utm_medium=pro_page_final&utm_campaign=pro_pack">Open dashboard demo</a>
|
|
1031
966
|
</div>
|
|
1032
967
|
</div>
|
|
@@ -1088,6 +1023,9 @@ function sendFirstPartyTelemetry(eventType, props) {
|
|
|
1088
1023
|
}).catch(function() {});
|
|
1089
1024
|
}
|
|
1090
1025
|
|
|
1026
|
+
function sendGa4Event(e,p){if(typeof gtag==='function')gtag('event',e,p||{})}
|
|
1027
|
+
function initializeProPaidRecovery(){var c=document.querySelector('[data-pro-paid-recovery]');if(!c)return;var n=0;c.querySelectorAll('a[href]').forEach(function(a){var h=a.getAttribute('href')||'';if(/^https?:\/\//.test(h))n+=1;else a.hidden=true});c.hidden=n===0}
|
|
1028
|
+
|
|
1091
1029
|
function initializeBuyerIntent() {
|
|
1092
1030
|
globalThis.ThumbGateBuyerIntent.initializeBuyerIntent({
|
|
1093
1031
|
page: 'pro',
|
|
@@ -1129,6 +1067,7 @@ trackClick('.btn-pro-checkout', 'pro_checkout_start', { tier: 'pro', page: 'pro'
|
|
|
1129
1067
|
trackClick('.btn-demo', 'pro_demo_click', { page: 'pro' });
|
|
1130
1068
|
trackClick('.btn-free-path', 'pro_free_path_click', { page: 'pro' });
|
|
1131
1069
|
trackClick('.proof-links a', 'pro_proof_click', { page: 'pro' });
|
|
1070
|
+
initializeProPaidRecovery();
|
|
1132
1071
|
initializeBuyerIntent();
|
|
1133
1072
|
globalThis.buyerJourney = globalThis.ThumbGateBuyerIntent.initializeBehaviorAnalytics({
|
|
1134
1073
|
pageType: 'marketing',
|
|
@@ -1145,6 +1084,10 @@ globalThis.buyerJourney = globalThis.ThumbGateBuyerIntent.initializeBehaviorAnal
|
|
|
1145
1084
|
],
|
|
1146
1085
|
ctaImpressions: [
|
|
1147
1086
|
{ selector: '.btn-pro-checkout', ctaId: 'pro_checkout', ctaPlacement: 'pro_page', planId: 'pro' },
|
|
1087
|
+
{ selector: '[data-first-rule-link]', ctaId: 'pro_page_first_failure_rule_checkout', ctaPlacement: 'pro_paid_recovery', planId: 'first_failure_rule' },
|
|
1088
|
+
{ selector: '[data-quick-read-link]', ctaId: 'pro_page_quick_read_checkout', ctaPlacement: 'pro_paid_recovery', planId: 'quick_read' },
|
|
1089
|
+
{ selector: '[data-sprint-diagnostic-link]', ctaId: 'pro_page_sprint_diagnostic_checkout', ctaPlacement: 'pro_paid_recovery', planId: 'sprint_diagnostic' },
|
|
1090
|
+
{ selector: '[data-workflow-sprint-link]', ctaId: 'pro_page_workflow_sprint_checkout', ctaPlacement: 'pro_paid_recovery', planId: 'workflow_sprint' },
|
|
1148
1091
|
{ selector: '.btn-demo', ctaId: 'pro_demo', ctaPlacement: 'pro_page', planId: 'proof' },
|
|
1149
1092
|
{ selector: '.btn-free-path', ctaId: 'pro_free_path', ctaPlacement: 'pro_page', planId: 'free' }
|
|
1150
1093
|
]
|
package/scripts/billing.js
CHANGED
|
@@ -412,9 +412,7 @@ function resolveCheckoutBrandUrls(appOrigin) {
|
|
|
412
412
|
};
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
-
// Resolve the per-tier product image
|
|
416
|
-
// Keeping three distinct URLs means the Stripe dashboard and checkout surface
|
|
417
|
-
// never show twins for Free/Pro/Team; see tests/billing-tier-icons.test.js.
|
|
415
|
+
// Resolve the per-tier product image used by Stripe Checkout.
|
|
418
416
|
function resolveTierIconUrl(planId, appOrigin) {
|
|
419
417
|
const brandUrls = resolveCheckoutBrandUrls(appOrigin);
|
|
420
418
|
const normalized = typeof planId === 'string' ? planId.toLowerCase() : '';
|
|
@@ -2546,6 +2544,8 @@ function buildCheckoutSessionPayload({ successUrl, cancelUrl, customerEmail, che
|
|
|
2546
2544
|
payment_method_types: ['card', 'link'],
|
|
2547
2545
|
mode: pack ? 'payment' : 'subscription',
|
|
2548
2546
|
line_items: lineItems,
|
|
2547
|
+
allow_promotion_codes: true,
|
|
2548
|
+
after_expiration: { recovery: { enabled: true, allow_promotion_codes: true } },
|
|
2549
2549
|
branding_settings: buildCheckoutBrandingSettings(appOrigin),
|
|
2550
2550
|
metadata: serializeStripeMetadata({
|
|
2551
2551
|
...checkoutMetadata,
|
package/scripts/gate-stats.js
CHANGED
|
@@ -43,10 +43,12 @@ function calculateStats() {
|
|
|
43
43
|
.filter((g) => g.action === 'warn')
|
|
44
44
|
.reduce((sum, g) => sum + (g.occurrences || 0), 0);
|
|
45
45
|
|
|
46
|
-
// Top blocked gate
|
|
46
|
+
// Top blocked gate. A configured block rule with zero occurrences is not a
|
|
47
|
+
// "top blocker"; only recorded block events should appear here.
|
|
47
48
|
const topBlocked = [...allGates]
|
|
49
|
+
.filter((g) => g.action === 'block' && Number(g.occurrences || 0) > 0)
|
|
48
50
|
.sort((a, b) => (b.occurrences || 0) - (a.occurrences || 0))
|
|
49
|
-
.
|
|
51
|
+
.at(0) || null;
|
|
50
52
|
|
|
51
53
|
// Last promotion event
|
|
52
54
|
const lastPromotion = promotionLog.length > 0
|
|
@@ -58,11 +60,9 @@ function calculateStats() {
|
|
|
58
60
|
const estimatedHoursSaved = (estimatedMinutesSaved / 60).toFixed(1);
|
|
59
61
|
|
|
60
62
|
// Bayes error rate: irreducible error floor of the current scorer given its
|
|
61
|
-
// feature set (tag signatures).
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
// and we should add features (file path, recency, commit context) rather
|
|
65
|
-
// than tune thresholds. Null when no feedback sequences have been recorded.
|
|
63
|
+
// feature set (tag signatures). It is only meaningful when the local sample
|
|
64
|
+
// contains both harmful and safe outcomes; an all-negative or all-positive
|
|
65
|
+
// sample can produce a misleading 0.0% floor.
|
|
66
66
|
const bayesErrorRate = tryComputeBayesErrorRate();
|
|
67
67
|
|
|
68
68
|
return {
|
|
@@ -90,12 +90,33 @@ function tryComputeBayesErrorRate() {
|
|
|
90
90
|
.filter(Boolean)
|
|
91
91
|
.map((line) => { try { return JSON.parse(line); } catch { return null; } })
|
|
92
92
|
.filter(Boolean);
|
|
93
|
+
if (!hasMixedOutcomeClasses(rows)) return null;
|
|
93
94
|
return computeBayesErrorRate(rows);
|
|
94
95
|
} catch {
|
|
95
96
|
return null;
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
function hasMixedOutcomeClasses(rows) {
|
|
101
|
+
if (!Array.isArray(rows) || rows.length < 2) return false;
|
|
102
|
+
let harmful = 0;
|
|
103
|
+
let safe = 0;
|
|
104
|
+
for (const row of rows) {
|
|
105
|
+
if (sequenceIsHarmful(row)) harmful += 1;
|
|
106
|
+
else safe += 1;
|
|
107
|
+
if (harmful > 0 && safe > 0) return true;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function sequenceIsHarmful(row) {
|
|
113
|
+
if (!row || typeof row !== 'object') return false;
|
|
114
|
+
if (typeof row.targetRisk === 'number') return row.targetRisk > 0;
|
|
115
|
+
if (typeof row.accepted === 'boolean' && row.accepted === false) return true;
|
|
116
|
+
const label = String(row.label || row.signal || '').toLowerCase();
|
|
117
|
+
return label === 'negative';
|
|
118
|
+
}
|
|
119
|
+
|
|
99
120
|
function formatLastPromotion(promo) {
|
|
100
121
|
if (!promo) return 'none';
|
|
101
122
|
const ts = promo.timestamp ? new Date(promo.timestamp) : null;
|
|
@@ -125,7 +146,7 @@ function formatStats(stats) {
|
|
|
125
146
|
}
|
|
126
147
|
|
|
127
148
|
function formatBayesErrorRate(rate) {
|
|
128
|
-
if (rate === null || rate === undefined) return 'n/a (
|
|
149
|
+
if (rate === null || rate === undefined) return 'n/a (need both safe and harmful feedback sequences)';
|
|
129
150
|
const pct = (rate * 100).toFixed(1);
|
|
130
151
|
if (rate < 0.02) return `${pct}% — scorer is near-optimal; add features, don't tune thresholds`;
|
|
131
152
|
if (rate < 0.10) return `${pct}% — scorer has modest headroom`;
|
|
@@ -253,6 +253,190 @@ function buildHarnessOptimizationAudit(options = {}) {
|
|
|
253
253
|
return scoreHarnessAudit(inputs, options);
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
function normalizeBoolean(value) {
|
|
257
|
+
if (value === true) return true;
|
|
258
|
+
if (value === false || value === undefined || value === null) return false;
|
|
259
|
+
return /^(1|true|yes|on)$/i.test(String(value).trim());
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function normalizeOptionalBoolean(value, fallback = true) {
|
|
263
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
264
|
+
if (value === true) return true;
|
|
265
|
+
if (value === false) return false;
|
|
266
|
+
return /^(1|true|yes|on)$/i.test(String(value).trim());
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function toNumber(value) {
|
|
270
|
+
if (value === undefined || value === null || value === '') return null;
|
|
271
|
+
const num = Number(value);
|
|
272
|
+
return Number.isFinite(num) ? num : null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function buildHarnessFitAudit(options = {}) {
|
|
276
|
+
const nativeHarness = String(options['native-harness'] || options.native || 'native').trim() || 'native';
|
|
277
|
+
const genericHarness = String(options['generic-harness'] || options.generic || 'generic').trim() || 'generic';
|
|
278
|
+
const sameModelDifferentHarness = normalizeBoolean(options['same-model-different-harness'] || options['same-model'] || options.crossHarness);
|
|
279
|
+
const controls = {
|
|
280
|
+
toolSchemaParity: normalizeOptionalBoolean(options['tool-schema-parity']),
|
|
281
|
+
permissionParity: normalizeOptionalBoolean(options['permission-parity']),
|
|
282
|
+
stateIsolation: normalizeOptionalBoolean(options['state-isolation']),
|
|
283
|
+
patchLoopParity: normalizeOptionalBoolean(options['patch-loop-parity']),
|
|
284
|
+
verificationParity: normalizeOptionalBoolean(options['verification-parity']),
|
|
285
|
+
};
|
|
286
|
+
const handoffDrift = toNumber(options['handoff-drift'] || options['handoff-drift-percent']);
|
|
287
|
+
const gaps = Object.entries(controls)
|
|
288
|
+
.filter(([, value]) => value === false)
|
|
289
|
+
.map(([key]) => key);
|
|
290
|
+
|
|
291
|
+
let score = 100;
|
|
292
|
+
if (sameModelDifferentHarness) score -= 15;
|
|
293
|
+
score -= gaps.length * 12;
|
|
294
|
+
if (handoffDrift !== null && handoffDrift > 0) score -= Math.min(20, Math.ceil(handoffDrift));
|
|
295
|
+
|
|
296
|
+
const signals = [];
|
|
297
|
+
if (sameModelDifferentHarness || gaps.length > 0) {
|
|
298
|
+
signals.push({
|
|
299
|
+
id: 'model_harness_fit',
|
|
300
|
+
label: 'Same model, different harness',
|
|
301
|
+
values: [
|
|
302
|
+
`${nativeHarness} vs ${genericHarness}`,
|
|
303
|
+
sameModelDifferentHarness ? 'same model run across harnesses' : null,
|
|
304
|
+
...gaps.map((gap) => `${gap} gap`),
|
|
305
|
+
].filter(Boolean),
|
|
306
|
+
risk: 'model quality can change when tool schemas, permissions, state, patch loops, or verification differ by harness',
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
if (handoffDrift !== null && handoffDrift > 0) {
|
|
310
|
+
signals.push({
|
|
311
|
+
id: 'handoff_drift',
|
|
312
|
+
label: 'Cross-harness handoff drift',
|
|
313
|
+
values: [`${handoffDrift}% drift`],
|
|
314
|
+
risk: 'handoffs between generic and native harnesses can lose task state or weaken verification',
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const normalizedScore = Math.max(0, Math.min(100, score));
|
|
319
|
+
return {
|
|
320
|
+
name: 'thumbgate-model-harness-fit-audit',
|
|
321
|
+
status: normalizedScore >= 85 ? 'portable' : normalizedScore >= 65 ? 'watch' : 'native-required',
|
|
322
|
+
score: normalizedScore,
|
|
323
|
+
nativeHarness,
|
|
324
|
+
genericHarness,
|
|
325
|
+
controls,
|
|
326
|
+
metrics: { sameModelDifferentHarness, handoffDrift },
|
|
327
|
+
signals,
|
|
328
|
+
recommendations: [
|
|
329
|
+
'Benchmark the same task, same model, and same repository in native and generic harnesses before standardizing.',
|
|
330
|
+
'Require parity proof for tool schemas, permissions, state isolation, patch application, and verification loops.',
|
|
331
|
+
'Use the native harness for production edits when parity gaps remain; reserve generic harnesses for exploration and read-only analysis.',
|
|
332
|
+
],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function formatHarnessFitAudit(report) {
|
|
337
|
+
const lines = [
|
|
338
|
+
'',
|
|
339
|
+
'ThumbGate Model-Harness Fit Audit',
|
|
340
|
+
'-'.repeat(37),
|
|
341
|
+
`Status : ${report.status}`,
|
|
342
|
+
`Score : ${report.score}/100`,
|
|
343
|
+
`Harness: ${report.nativeHarness} vs ${report.genericHarness}`,
|
|
344
|
+
`Signals: ${report.signals.length}`,
|
|
345
|
+
];
|
|
346
|
+
if (report.signals.length > 0) {
|
|
347
|
+
lines.push('', 'Detected harness-fit risks:');
|
|
348
|
+
for (const signal of report.signals) {
|
|
349
|
+
lines.push(` - ${signal.label}: ${signal.values.join(', ')}`);
|
|
350
|
+
lines.push(` Risk: ${signal.risk}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
lines.push('', 'Recommendations:');
|
|
354
|
+
for (const recommendation of report.recommendations) lines.push(` - ${recommendation}`);
|
|
355
|
+
return `${lines.join('\n')}\n\n`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function buildSolverWorkflowGovernance(options = {}) {
|
|
359
|
+
const solver = String(options.solver || options['solver-engine'] || 'solver').trim() || 'solver';
|
|
360
|
+
const multiAgent = normalizeBoolean(options['multi-agent'] || options.multiAgent || options.agentic);
|
|
361
|
+
const controls = {
|
|
362
|
+
objectiveDefined: normalizeOptionalBoolean(options['objective-defined']),
|
|
363
|
+
constraintsDefined: normalizeOptionalBoolean(options['constraints-defined']),
|
|
364
|
+
scenarioReplay: normalizeOptionalBoolean(options['scenario-replay']),
|
|
365
|
+
approvalGate: normalizeOptionalBoolean(options['approval-gate']),
|
|
366
|
+
rollbackPlan: normalizeOptionalBoolean(options['rollback-plan']),
|
|
367
|
+
solverProvenance: normalizeOptionalBoolean(options['solver-provenance']),
|
|
368
|
+
};
|
|
369
|
+
const dataFreshnessHours = toNumber(options['data-freshness-hours'] || options['freshness-hours']);
|
|
370
|
+
const gaps = Object.entries(controls)
|
|
371
|
+
.filter(([, value]) => value === false)
|
|
372
|
+
.map(([key]) => key);
|
|
373
|
+
|
|
374
|
+
let score = 100;
|
|
375
|
+
if (multiAgent) score -= 8;
|
|
376
|
+
score -= gaps.length * 13;
|
|
377
|
+
if (dataFreshnessHours !== null && dataFreshnessHours > 24) score -= 10;
|
|
378
|
+
|
|
379
|
+
const signals = [];
|
|
380
|
+
if (multiAgent || gaps.length > 0) {
|
|
381
|
+
signals.push({
|
|
382
|
+
id: 'solver_workflow_governance',
|
|
383
|
+
label: 'Solver-backed agent workflow',
|
|
384
|
+
values: [
|
|
385
|
+
solver,
|
|
386
|
+
multiAgent ? 'multi-agent orchestration' : null,
|
|
387
|
+
...gaps.map((gap) => `${gap} gap`),
|
|
388
|
+
].filter(Boolean),
|
|
389
|
+
risk: 'natural-language-to-optimization workflows need objective, constraint, replay, approval, rollback, and provenance gates',
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
if (dataFreshnessHours !== null && dataFreshnessHours > 24) {
|
|
393
|
+
signals.push({
|
|
394
|
+
id: 'solver_data_freshness',
|
|
395
|
+
label: 'Solver data freshness',
|
|
396
|
+
values: [`${dataFreshnessHours}h old`],
|
|
397
|
+
risk: 'optimization results can look mathematically valid while using stale operational data',
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const normalizedScore = Math.max(0, Math.min(100, score));
|
|
402
|
+
return {
|
|
403
|
+
name: 'thumbgate-solver-workflow-governance',
|
|
404
|
+
status: normalizedScore >= 85 ? 'ready' : normalizedScore >= 65 ? 'approval-required' : 'blocked',
|
|
405
|
+
score: normalizedScore,
|
|
406
|
+
solver,
|
|
407
|
+
controls,
|
|
408
|
+
metrics: { multiAgent, dataFreshnessHours },
|
|
409
|
+
signals,
|
|
410
|
+
recommendations: [
|
|
411
|
+
'Capture the objective function, hard constraints, soft constraints, and data freshness before invoking the solver.',
|
|
412
|
+
'Replay at least one baseline scenario and one counterfactual before approving optimized actions.',
|
|
413
|
+
'Require human approval and rollback evidence before solver output changes supply chain, routing, scheduling, or pricing decisions.',
|
|
414
|
+
],
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function formatSolverWorkflowGovernance(report) {
|
|
419
|
+
const lines = [
|
|
420
|
+
'',
|
|
421
|
+
'ThumbGate Solver Workflow Governance',
|
|
422
|
+
'-'.repeat(38),
|
|
423
|
+
`Status: ${report.status}`,
|
|
424
|
+
`Score : ${report.score}/100`,
|
|
425
|
+
`Solver: ${report.solver}`,
|
|
426
|
+
`Signals: ${report.signals.length}`,
|
|
427
|
+
];
|
|
428
|
+
if (report.signals.length > 0) {
|
|
429
|
+
lines.push('', 'Detected solver workflow risks:');
|
|
430
|
+
for (const signal of report.signals) {
|
|
431
|
+
lines.push(` - ${signal.label}: ${signal.values.join(', ')}`);
|
|
432
|
+
lines.push(` Risk: ${signal.risk}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
lines.push('', 'Recommendations:');
|
|
436
|
+
for (const recommendation of report.recommendations) lines.push(` - ${recommendation}`);
|
|
437
|
+
return `${lines.join('\n')}\n\n`;
|
|
438
|
+
}
|
|
439
|
+
|
|
256
440
|
// ---------------------------------------------------------------------------
|
|
257
441
|
// Internal helpers
|
|
258
442
|
// ---------------------------------------------------------------------------
|
|
@@ -284,6 +468,10 @@ module.exports = {
|
|
|
284
468
|
collectDefaultHarnessAuditInputs,
|
|
285
469
|
scoreHarnessAudit,
|
|
286
470
|
buildHarnessOptimizationAudit,
|
|
471
|
+
buildHarnessFitAudit,
|
|
472
|
+
formatHarnessFitAudit,
|
|
473
|
+
buildSolverWorkflowGovernance,
|
|
474
|
+
formatSolverWorkflowGovernance,
|
|
287
475
|
extractCommandText,
|
|
288
476
|
HARNESSES,
|
|
289
477
|
DEPLOY_PATTERNS,
|
|
@@ -34,6 +34,14 @@ function normalizeOptions(options = {}) {
|
|
|
34
34
|
embeddingFineTune: normalizeBoolean(options['embedding-finetune'] || options['embedding-fine-tune'] || options['fine-tune']),
|
|
35
35
|
structuralNearMisses: normalizeBoolean(options['structural-near-misses'] || options['near-misses']),
|
|
36
36
|
verifier: normalizeBoolean(options.verifier || options.reranker || options['second-stage']),
|
|
37
|
+
hybridRetrieval: normalizeBoolean(options['hybrid-retrieval'] || options.hybrid),
|
|
38
|
+
denseRetrieval: normalizeBoolean(options.dense || options['dense-retrieval'] || options.embeddings),
|
|
39
|
+
sparseRetrieval: normalizeBoolean(options.sparse || options['sparse-retrieval'] || options.keyword),
|
|
40
|
+
reranker: normalizeBoolean(options.reranker || options.rerank),
|
|
41
|
+
sourceGrounding: normalizeBoolean(options['source-grounding'] || options.grounding || options.citations),
|
|
42
|
+
aclFilter: normalizeBoolean(options['acl-filter'] || options.acl || options['access-control']),
|
|
43
|
+
freshnessWindowHours: toNumber(options['freshness-window-hours'] || options['freshness-hours']),
|
|
44
|
+
scaleCorpusDocuments: toNumber(options['scale-corpus-documents'] || options['corpus-documents'] || options.documents),
|
|
37
45
|
latencyMs: toNumber(options['latency-ms'] || options.latency),
|
|
38
46
|
latencyBudgetMs: toNumber(options['latency-budget-ms'] || options['latency-budget']),
|
|
39
47
|
agenticPipeline: normalizeBoolean(options.agentic || options['agentic-pipeline']),
|
|
@@ -69,6 +77,8 @@ function buildSignals(options) {
|
|
|
69
77
|
precisionTuningSignal(options, drop),
|
|
70
78
|
ragCascadeSignal(options),
|
|
71
79
|
verifierLatencySignal(options),
|
|
80
|
+
hybridScaleSignal(options),
|
|
81
|
+
retrievalGovernanceSignal(options),
|
|
72
82
|
].filter(Boolean);
|
|
73
83
|
}
|
|
74
84
|
|
|
@@ -113,6 +123,48 @@ function verifierLatencySignal(options) {
|
|
|
113
123
|
};
|
|
114
124
|
}
|
|
115
125
|
|
|
126
|
+
function hybridScaleSignal(options) {
|
|
127
|
+
const hybridIntent = options.hybridRetrieval || (options.denseRetrieval && options.sparseRetrieval);
|
|
128
|
+
const largeCorpus = options.scaleCorpusDocuments !== null && options.scaleCorpusDocuments >= 100000;
|
|
129
|
+
if (!(hybridIntent || largeCorpus)) return null;
|
|
130
|
+
const missingControls = [
|
|
131
|
+
options.denseRetrieval ? null : 'dense recall unmeasured',
|
|
132
|
+
options.sparseRetrieval ? null : 'sparse recall unmeasured',
|
|
133
|
+
options.reranker ? null : 'missing reranker',
|
|
134
|
+
options.sourceGrounding ? null : 'missing source grounding',
|
|
135
|
+
options.aclFilter ? null : 'missing ACL filter',
|
|
136
|
+
].filter(Boolean);
|
|
137
|
+
return {
|
|
138
|
+
id: 'hybrid_retrieval_scale_wall',
|
|
139
|
+
label: 'Hybrid retrieval scale wall',
|
|
140
|
+
values: [
|
|
141
|
+
options.hybridRetrieval ? 'hybrid retrieval' : null,
|
|
142
|
+
options.denseRetrieval ? 'dense retrieval' : null,
|
|
143
|
+
options.sparseRetrieval ? 'sparse retrieval' : null,
|
|
144
|
+
options.scaleCorpusDocuments !== null ? `${options.scaleCorpusDocuments} documents` : null,
|
|
145
|
+
...missingControls,
|
|
146
|
+
].filter(Boolean),
|
|
147
|
+
risk: 'scaled RAG needs dense, sparse, reranking, grounding, and access-control evidence instead of vector-only correctness',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function retrievalGovernanceSignal(options) {
|
|
152
|
+
if (!options.agenticPipeline && options.sourceGrounding && options.aclFilter) return null;
|
|
153
|
+
if (!(options.agenticPipeline || options.hybridRetrieval || options.scaleCorpusDocuments !== null)) return null;
|
|
154
|
+
const gaps = [
|
|
155
|
+
options.sourceGrounding ? null : 'source evidence not enforced',
|
|
156
|
+
options.aclFilter ? null : 'access control not enforced',
|
|
157
|
+
options.freshnessWindowHours === null ? 'freshness window missing' : `${options.freshnessWindowHours}h freshness window`,
|
|
158
|
+
].filter(Boolean);
|
|
159
|
+
if (gaps.length === 0) return null;
|
|
160
|
+
return {
|
|
161
|
+
id: 'retrieval_governance_gap',
|
|
162
|
+
label: 'Retrieval governance gap',
|
|
163
|
+
values: gaps,
|
|
164
|
+
risk: 'agentic retrieval output can leak stale or unauthorized context into downstream actions',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
116
168
|
function buildRagPrecisionGuardrailsPlan(rawOptions = {}, templatesPath) {
|
|
117
169
|
const options = normalizeOptions(rawOptions);
|
|
118
170
|
const templates = listGateTemplates(templatesPath)
|
|
@@ -135,6 +187,14 @@ function buildRagPrecisionGuardrailsPlan(rawOptions = {}, templatesPath) {
|
|
|
135
187
|
recallDropPercent: recallDropPercent(options),
|
|
136
188
|
baselinePrecision: options.baselinePrecision,
|
|
137
189
|
newPrecision: options.newPrecision,
|
|
190
|
+
hybridRetrieval: options.hybridRetrieval,
|
|
191
|
+
denseRetrieval: options.denseRetrieval,
|
|
192
|
+
sparseRetrieval: options.sparseRetrieval,
|
|
193
|
+
reranker: options.reranker,
|
|
194
|
+
sourceGrounding: options.sourceGrounding,
|
|
195
|
+
aclFilter: options.aclFilter,
|
|
196
|
+
freshnessWindowHours: options.freshnessWindowHours,
|
|
197
|
+
scaleCorpusDocuments: options.scaleCorpusDocuments,
|
|
138
198
|
latencyMs: options.latencyMs,
|
|
139
199
|
latencyBudgetMs: options.latencyBudgetMs,
|
|
140
200
|
},
|
|
@@ -150,8 +210,10 @@ function buildRagPrecisionGuardrailsPlan(rawOptions = {}, templatesPath) {
|
|
|
150
210
|
'Block embedding or threshold changes when recall drops without an approved rollback plan.',
|
|
151
211
|
'Use a second-stage verifier or reranker for structural near misses such as negation and role reversal.',
|
|
152
212
|
'Attach verifier latency budgets before routing the retrieval output into autonomous agent actions.',
|
|
213
|
+
'Measure dense recall, sparse recall, reranked relevance, source grounding, ACL filtering, and freshness as separate production gates.',
|
|
214
|
+
'Treat the retrieval layer as the agent ground truth: every autonomous action should carry source evidence and access-control proof.',
|
|
153
215
|
],
|
|
154
|
-
exampleCommand: 'npx thumbgate rag-precision-guardrails --
|
|
216
|
+
exampleCommand: 'npx thumbgate rag-precision-guardrails --hybrid-retrieval --dense --sparse --scale-corpus-documents=1000000 --agentic --json',
|
|
155
217
|
};
|
|
156
218
|
}
|
|
157
219
|
|
package/scripts/rate-limiter.js
CHANGED
|
@@ -36,7 +36,7 @@ const PAYWALL_MESSAGES = {
|
|
|
36
36
|
prevention_rules: 'Free tier includes 1 prevention rule. Your agents need more protection — upgrade to Pro for unlimited rules.',
|
|
37
37
|
recall: 'Recall is a Pro feature. Your past feedback is stored locally — upgrade to search and reuse it.',
|
|
38
38
|
search_lessons: 'Lesson search is a Pro feature. Upgrade to find patterns in your agent\'s mistakes.',
|
|
39
|
-
default: 'This feature requires Pro. Start
|
|
39
|
+
default: 'This feature requires Pro. Start Pro — card required; billed today.',
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
function isProTier(authContext) {
|