thumbgate 1.20.0 → 1.21.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.
@@ -0,0 +1,345 @@
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
+ __GOOGLE_SITE_VERIFICATION_META__
7
+ <title>Pricing — ThumbGate</title>
8
+ <meta name="description" content="ThumbGate pricing: free CLI forever, $19/mo Pro dashboard, and intake-led Team enforcement at $49/seat after scope. One clear subscription path.">
9
+ <meta property="og:title" content="Pricing — ThumbGate">
10
+ <meta property="og:description" content="Free CLI, Pro self-serve, and Team after workflow scope. No mixed consulting checkout maze.">
11
+ <meta property="og:type" content="website">
12
+ <meta property="og:url" content="__APP_ORIGIN__/pricing">
13
+ <meta property="og:image" content="/og.png">
14
+ <link rel="canonical" href="__APP_ORIGIN__/pricing">
15
+ <link rel="icon" type="image/png" href="/thumbgate-icon.png">
16
+ <link rel="apple-touch-icon" href="/assets/brand/thumbgate-mark.svg">
17
+
18
+ <script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.tagged-events.js"></script>
19
+ __GA_BOOTSTRAP__
20
+
21
+ <script>
22
+ const gaMeasurementId = '__GA_MEASUREMENT_ID__';
23
+ const serverVisitorId = '__SERVER_VISITOR_ID__';
24
+ const serverSessionId = '__SERVER_SESSION_ID__';
25
+ const serverAcquisitionId = '__SERVER_ACQUISITION_ID__';
26
+ const serverTelemetryCaptured = '__SERVER_TELEMETRY_CAPTURED__' === 'true';
27
+ const proPriceDollars = Number('__PRO_PRICE_DOLLARS__') || 19;
28
+ </script>
29
+
30
+ <script type="application/ld+json">
31
+ {
32
+ "@context": "https://schema.org",
33
+ "@type": "SoftwareApplication",
34
+ "name": "ThumbGate",
35
+ "applicationCategory": "DeveloperApplication",
36
+ "operatingSystem": "Cross-platform, Node.js >=18.18.0",
37
+ "url": "__APP_ORIGIN__/pricing",
38
+ "offers": [
39
+ { "@type": "Offer", "name": "ThumbGate CLI (Free)", "price": "0", "priceCurrency": "USD" },
40
+ { "@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=%2Fpricing" },
41
+ { "@type": "Offer", "name": "ThumbGate Pro Annual", "price": "149", "priceCurrency": "USD", "url": "__APP_ORIGIN__/checkout/pro?plan_id=pro&billing_cycle=annual&landing_path=%2Fpricing" },
42
+ { "@type": "Offer", "name": "ThumbGate Team", "price": "49", "priceCurrency": "USD", "url": "__APP_ORIGIN__/#workflow-sprint-intake" }
43
+ ]
44
+ }
45
+ </script>
46
+
47
+ <script type="application/ld+json">
48
+ {
49
+ "@context": "https://schema.org",
50
+ "@type": "FAQPage",
51
+ "mainEntity": [
52
+ { "@type": "Question", "name": "What does Pro add over the free CLI?", "acceptedAnswer": { "@type": "Answer", "text": "Free gives you unlimited captures and 5 active rules, running entirely on your machine. Pro is the hosted layer: lesson sync across machines, a dashboard without self-hosting, managed adapter updates, unlimited rules, and DPO export. You're paying for infrastructure we run, not features we hide." } },
53
+ { "@type": "Question", "name": "Does ThumbGate send my code to the cloud?", "acceptedAnswer": { "@type": "Answer", "text": "No. The CLI is local-first — no data leaves your machine. Pro and Team add hosted sync for dashboards and shared lessons, but your source code stays local." } },
54
+ { "@type": "Question", "name": "When should I pick Team over Pro?", "acceptedAnswer": { "@type": "Answer", "text": "When one engineer's correction should protect the whole team. Team shares the lesson database across seats so a fix in one repo prevents the same mistake in every repo." } },
55
+ { "@type": "Question", "name": "Can I cancel anytime?", "acceptedAnswer": { "@type": "Answer", "text": "Yes. Pro and Team are month-to-month with a 7-day refund window on the first charge. Cancel from the billing portal and your subscription ends at the period close." } }
56
+ ]
57
+ }
58
+ </script>
59
+
60
+ <style>
61
+ *, *::before, *::after { box-sizing: border-box; }
62
+
63
+ :root {
64
+ --bg: #0a0a0b;
65
+ --bg-raised: #111113;
66
+ --bg-card: #161618;
67
+ --border: #232327;
68
+ --text: #ececf1;
69
+ --text-muted: #9a9aa6;
70
+ --text-dim: #6b6b78;
71
+ --cyan: #22d3ee;
72
+ --cyan-dim: rgba(34, 211, 238, 0.12);
73
+ --cyan-glow: rgba(34, 211, 238, 0.22);
74
+ --green: #4ade80;
75
+ --green-dim: rgba(74, 222, 128, 0.12);
76
+ --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto, sans-serif;
77
+ --mono: 'SF Mono', 'Cascadia Code', 'JetBrains Mono', 'Fira Code', Consolas, monospace;
78
+ }
79
+
80
+ html { scroll-behavior: smooth; }
81
+ body {
82
+ margin: 0;
83
+ font-family: var(--font);
84
+ background:
85
+ radial-gradient(circle at top, rgba(34, 211, 238, 0.16) 0%, rgba(34, 211, 238, 0) 28%),
86
+ linear-gradient(180deg, #0a0a0b 0%, #0d1016 48%, #0a0a0b 100%);
87
+ color: var(--text);
88
+ line-height: 1.6;
89
+ -webkit-font-smoothing: antialiased;
90
+ }
91
+
92
+ a { color: inherit; }
93
+ .container { max-width: 1080px; margin: 0 auto; padding: 0 24px; }
94
+
95
+ /* NAV */
96
+ nav {
97
+ position: sticky; top: 0; z-index: 50;
98
+ backdrop-filter: blur(12px);
99
+ background: rgba(10, 10, 11, 0.86);
100
+ border-bottom: 1px solid rgba(35, 35, 39, 0.92);
101
+ }
102
+ nav .container { min-height: 68px; display: flex; align-items: center; justify-content: space-between; gap: 20px; }
103
+ .nav-logo { font-size: 15px; font-weight: 700; letter-spacing: -0.02em; text-decoration: none; }
104
+ .nav-links { display: flex; gap: 20px; align-items: center; }
105
+ .nav-links a { font-size: 13px; color: var(--text-muted); text-decoration: none; transition: color 0.15s; }
106
+ .nav-links a:hover { color: var(--text); }
107
+ .nav-cta { background: var(--cyan); color: var(--bg); padding: 7px 16px; border-radius: 6px; font-size: 13px; font-weight: 600; text-decoration: none; transition: opacity 0.15s; }
108
+ .nav-cta:hover { opacity: 0.85; }
109
+
110
+ /* HERO */
111
+ .pricing-hero { padding: 80px 0 24px; text-align: center; }
112
+ .pricing-hero h1 { font-size: clamp(28px, 4vw, 40px); font-weight: 700; letter-spacing: -0.03em; margin: 0 0 12px; }
113
+ .pricing-hero .lede { color: var(--text-muted); font-size: 17px; max-width: 560px; margin: 0 auto; }
114
+
115
+ /* GRID */
116
+ .pricing-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin: 40px 0 32px; }
117
+ @media (max-width: 768px) { .pricing-grid { grid-template-columns: 1fr; } }
118
+
119
+ /* CARDS */
120
+ .price-card {
121
+ background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px;
122
+ padding: 32px 28px; display: flex; flex-direction: column;
123
+ }
124
+ .price-card.highlight {
125
+ border-color: var(--cyan);
126
+ box-shadow: 0 0 40px var(--cyan-dim), inset 0 1px 0 rgba(34, 211, 238, 0.15);
127
+ position: relative;
128
+ }
129
+ .price-card.highlight::before {
130
+ content: "Most popular";
131
+ position: absolute; top: -10px; left: 50%; transform: translateX(-50%);
132
+ background: var(--cyan); color: var(--bg);
133
+ padding: 3px 12px; border-radius: 6px;
134
+ font-size: 11px; font-weight: 700; letter-spacing: 0.5px; text-transform: uppercase;
135
+ }
136
+ .price-card.team-card { border-color: rgba(74, 222, 128, 0.45); box-shadow: inset 0 1px 0 rgba(74, 222, 128, 0.16); }
137
+
138
+ .tier { font-size: 13px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); font-weight: 600; margin-bottom: 8px; }
139
+ .price-card.highlight .tier { color: var(--cyan); }
140
+ .price-card.team-card .tier { color: var(--green); }
141
+
142
+ .price { font-size: 40px; font-weight: 700; letter-spacing: -0.03em; margin-bottom: 4px; }
143
+ .price span { font-size: 16px; color: var(--text-dim); }
144
+ .price-sub { font-size: 13px; color: var(--text-muted); margin-bottom: 24px; line-height: 1.55; }
145
+
146
+ .price-card ul { list-style: none; padding: 0; margin: 0 0 28px; }
147
+ .price-card li { font-size: 14px; color: var(--text-muted); padding: 6px 0; display: flex; align-items: flex-start; gap: 8px; }
148
+ .price-card li::before { content: "\2713"; color: var(--cyan); font-weight: 700; flex-shrink: 0; }
149
+ .price-card.team-card li::before { color: var(--green); }
150
+
151
+ /* BUTTONS */
152
+ .btn-install {
153
+ display: block; text-align: center; padding: 12px; border: 1px solid var(--border);
154
+ border-radius: 8px; color: var(--text); text-decoration: none; font-size: 14px; font-weight: 500;
155
+ transition: border-color 0.15s; margin-top: auto;
156
+ }
157
+ .btn-install:hover { border-color: var(--text-muted); }
158
+
159
+ .btn-pro {
160
+ display: block; text-align: center; padding: 12px;
161
+ background: var(--cyan); color: var(--bg); border-radius: 8px;
162
+ text-decoration: none; font-size: 14px; font-weight: 600; transition: opacity 0.15s;
163
+ margin-top: auto;
164
+ }
165
+ .btn-pro:hover { opacity: 0.85; }
166
+
167
+ .btn-team {
168
+ display: block; text-align: center; padding: 12px;
169
+ background: rgba(74, 222, 128, 0.14); color: var(--green);
170
+ border: 1px solid rgba(74, 222, 128, 0.4); border-radius: 8px;
171
+ text-decoration: none; font-size: 14px; font-weight: 600;
172
+ transition: border-color 0.15s, transform 0.15s; margin-top: auto;
173
+ }
174
+ .btn-team:hover { border-color: var(--green); transform: translateY(-1px); }
175
+
176
+ .btn-sub { font-size: 11px; color: var(--text-muted); margin-top: 8px; text-align: center; }
177
+
178
+ /* FAQ */
179
+ .faq { padding: 48px 0; }
180
+ .faq h2 { text-align: center; font-size: 24px; margin-bottom: 24px; }
181
+ .faq-list { max-width: 640px; margin: 0 auto; }
182
+ .faq-item { border-bottom: 1px solid var(--border); }
183
+ .faq-q {
184
+ padding: 20px 0; font-size: 15px; font-weight: 600; cursor: pointer;
185
+ display: flex; justify-content: space-between; align-items: center;
186
+ }
187
+ .faq-q::after { content: "+"; font-size: 18px; color: var(--text-muted); transition: transform 0.2s; }
188
+ .faq-item.open .faq-q::after { transform: rotate(45deg); }
189
+ .faq-a { font-size: 14px; color: var(--text-muted); line-height: 1.65; padding-bottom: 20px; display: none; }
190
+ .faq-item.open .faq-a { display: block; }
191
+
192
+ /* FOOTER */
193
+ footer { border-top: 1px solid var(--border); padding: 32px 0; margin-top: 48px; }
194
+ footer .container { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; }
195
+ .footer-links { display: flex; gap: 20px; }
196
+ .footer-links a { color: var(--text-muted); text-decoration: none; font-size: 13px; transition: color 0.15s; }
197
+ .footer-links a:hover { color: var(--text); }
198
+ .footer-copy { font-size: 12px; color: var(--text-muted); }
199
+ </style>
200
+ <script>
201
+ !function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace("/ingest","")+ "/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group identify setPersonProperties setPersonPropertiesForFlags".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
202
+ posthog.init('__POSTHOG_API_KEY__', {
203
+ api_host: '/ingest',
204
+ ui_host: 'https://us.posthog.com',
205
+ person_profiles: 'identified_only',
206
+ });
207
+ posthog.capture('$pageview');
208
+ </script>
209
+ </head>
210
+ <body>
211
+
212
+ <nav>
213
+ <div class="container">
214
+ <a href="/" class="nav-logo">ThumbGate</a>
215
+ <div class="nav-links">
216
+ <a href="/#features">Features</a>
217
+ <a href="/pricing" style="color:var(--text);">Pricing</a>
218
+ <a href="/guide">Guide</a>
219
+ <a href="/dashboard">Dashboard</a>
220
+ <a class="nav-cta" href="/checkout/pro?utm_source=pricing&utm_medium=nav&utm_campaign=pricing_page&plan_id=pro&landing_path=%2Fpricing">Start Pro</a>
221
+ </div>
222
+ </div>
223
+ </nav>
224
+
225
+ <section class="pricing-hero">
226
+ <div class="container">
227
+ <h1>Stop paying for the same AI mistake twice.</h1>
228
+ <p class="lede">One self-serve paid path for solo operators. Teams start with workflow scope so shared enforcement is mapped before checkout.</p>
229
+ </div>
230
+ </section>
231
+
232
+ <section class="container">
233
+ <div class="pricing-grid">
234
+
235
+ <div class="price-card">
236
+ <div class="tier" style="color:var(--cyan);">Free</div>
237
+ <div class="price">$0</div>
238
+ <div class="price-sub">Block repeated mistakes daily. Forever free for solo devs.</div>
239
+ <ul>
240
+ <li>Unlimited feedback captures — every thumbs-down, every session</li>
241
+ <li>Up to 5 active prevention rules</li>
242
+ <li>All MCP integrations (Claude Code, Cursor, Codex, Gemini, Amp)</li>
243
+ <li>PreToolUse hook blocking with built-in safety checks</li>
244
+ <li>Runs 100% local — no account, no signup, no data leaves your machine</li>
245
+ </ul>
246
+ <a class="btn-install" href="/go/install?utm_source=pricing&utm_medium=free_card" onclick="try{posthog.capture('pricing_cta_click',{cta:'install_free',tier:'free',placement:'pricing_page'})}catch(_){};try{plausible('pricing_cta_click',{props:{cta:'install_free',tier:'free'}})}catch(_){}">Install free</a>
247
+ </div>
248
+
249
+ <div class="price-card highlight" id="pro">
250
+ <div class="tier">Pro</div>
251
+ <div class="price">$19<span>/mo</span></div>
252
+ <div class="price-sub">
253
+ The free CLI runs your gates locally and never expires.
254
+ Pro is what we operate for you: hosted lesson sync across all your machines, adapter matrix kept current as agent runtimes ship breaking changes, and a dashboard you never have to self-host.
255
+ </div>
256
+ <ul>
257
+ <li><strong>Hosted lesson sync</strong> — corrections follow you across machines, no manual export</li>
258
+ <li><strong>Managed adapter matrix</strong> — we track runtime changes in Claude Code, Cursor, Codex, Gemini, Amp so you don't</li>
259
+ <li><strong>Hosted dashboard</strong> — see every blocked action, every rule that fired, without running your own server</li>
260
+ <li><strong>Unlimited prevention rules</strong> — free caps at 5 auto-promoted rules</li>
261
+ <li><strong>DPO + HuggingFace export</strong> — training data from your real corrections</li>
262
+ <li><strong>Auto-connect</strong> — new agent surfaces appear automatically after setup</li>
263
+ <li>7-day refund window. Cancel anytime.</li>
264
+ </ul>
265
+ <a class="btn-pro" href="/checkout/pro?utm_source=pricing&utm_medium=hero_card&utm_campaign=pricing_page&plan_id=pro&landing_path=%2Fpricing" onclick="try{posthog.capture('pricing_cta_click',{cta:'start_pro',tier:'pro',placement:'pricing_page',price:19})}catch(_){};try{plausible('pricing_cta_click',{props:{cta:'start_pro',tier:'pro'}})}catch(_){}">Start Pro — $19/mo</a>
266
+ <p class="btn-sub">or $149/year (save 35%)</p>
267
+ </div>
268
+
269
+ <div class="price-card team-card" id="team">
270
+ <div class="tier">Team</div>
271
+ <div class="price">$49<span>/seat/mo</span></div>
272
+ <div class="price-sub">Shared enforcement memory for the whole team. One engineer's save protects every agent.</div>
273
+ <ul>
274
+ <li>Everything in Pro for each seat</li>
275
+ <li><strong>Shared lesson database</strong> — one engineer's fix protects every agent on the team</li>
276
+ <li><strong>Org dashboard</strong> — visibility across all agent surfaces and developers</li>
277
+ <li>Check template library for deploys, publish, and DB operations</li>
278
+ <li>Email support during pilot rollout</li>
279
+ <li>3-seat minimum after scope; rollout starts only after workflow and proof review are explicit</li>
280
+ </ul>
281
+ <a class="btn-team" href="/?utm_source=pricing&utm_medium=team_card&utm_campaign=team_intake&cta_id=pricing_team_intake&cta_placement=pricing&plan_id=team#workflow-sprint-intake" onclick="try{posthog.capture('pricing_cta_click',{cta:'team_intake',tier:'team',placement:'pricing_page',price:0})}catch(_){};try{plausible('pricing_cta_click',{props:{cta:'team_intake',tier:'team'}})}catch(_){}">Send team workflow first</a>
282
+ <p class="btn-sub">Rollout starts through intake so the workflow is scoped before checkout.</p>
283
+ </div>
284
+
285
+ </div>
286
+
287
+ <div style="text-align:center;margin:32px 0;color:var(--text-muted);font-size:14px;">
288
+ Need founder help? Do not buy a blind diagnostic from a pricing table.
289
+ <a href="/?utm_source=pricing&utm_medium=scope_first&utm_campaign=team_intake&cta_id=pricing_scope_first&cta_placement=pricing_note&plan_id=team#workflow-sprint-intake" style="color:var(--cyan);text-decoration:none;font-weight:600;" onclick="try{posthog.capture('pricing_cta_click',{cta:'scope_first',tier:'team',price:0})}catch(_){};try{plausible('pricing_cta_click',{props:{cta:'scope_first',tier:'team'}})}catch(_){}">Send the workflow first</a> — then we scope the smallest paid rollout that can prove one repeated failure is blocked.
290
+ </div>
291
+ </section>
292
+
293
+ <section class="faq">
294
+ <div class="container">
295
+ <h2>Questions</h2>
296
+ <div class="faq-list">
297
+ <div class="faq-item">
298
+ <div class="faq-q">What does Pro add over the free CLI?</div>
299
+ <div class="faq-a">Free gives you unlimited captures and 5 active rules, running entirely on your machine. Pro is the hosted layer: lesson sync across machines, a dashboard without self-hosting, managed adapter updates, unlimited rules, and DPO export. You're paying for infrastructure we run, not features we hide.</div>
300
+ </div>
301
+ <div class="faq-item">
302
+ <div class="faq-q">Does ThumbGate send my code to the cloud?</div>
303
+ <div class="faq-a">No. The CLI is local-first and no source code leaves your machine. Pro and Team add hosted sync for dashboards and shared lessons, but your code stays local.</div>
304
+ </div>
305
+ <div class="faq-item">
306
+ <div class="faq-q">When should I pick Team over Pro?</div>
307
+ <div class="faq-a">When one engineer's correction should protect the whole team. Team shares the lesson database across seats so a fix in one repo prevents the same mistake in every repo.</div>
308
+ </div>
309
+ <div class="faq-item">
310
+ <div class="faq-q">Can I cancel anytime?</div>
311
+ <div class="faq-a">Yes. Pro and Team are month-to-month with a 7-day refund window on the first charge. Cancel from the billing portal and your subscription ends at the period close.</div>
312
+ </div>
313
+ <div class="faq-item">
314
+ <div class="faq-q">What happens if I stop paying?</div>
315
+ <div class="faq-a">You keep the free CLI with 5-rule cap. Your existing rules and captures stay on your machine. You lose dashboard access, lesson search, exports, and auto-connect.</div>
316
+ </div>
317
+ </div>
318
+ </div>
319
+ </section>
320
+
321
+ <footer>
322
+ <div class="container">
323
+ <div class="footer-links">
324
+ <a href="/">Home</a>
325
+ <a href="/guide">Guide</a>
326
+ <a href="/support">Support</a>
327
+ <a href="/privacy">Privacy</a>
328
+ <a href="/terms">Terms</a>
329
+ </div>
330
+ <span class="footer-copy">One source of truth for ThumbGate pricing. Numbers here override anything stale elsewhere.</span>
331
+ </div>
332
+ </footer>
333
+
334
+ <script>
335
+ document.querySelectorAll('.faq-q').forEach(function(q) {
336
+ q.addEventListener('click', function() {
337
+ this.parentElement.classList.toggle('open');
338
+ });
339
+ });
340
+ </script>
341
+ <script>
342
+ window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) };
343
+ </script>
344
+ </body>
345
+ </html>
@@ -193,9 +193,9 @@ function patternToGateId(key) {
193
193
  return 'auto-' + key.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').slice(0, 50).toLowerCase();
194
194
  }
195
195
 
196
- function buildGateRule(group) {
197
- const action = group.count === 'MANUAL' ? group.manualAction || 'block' : (group.count >= BLOCK_THRESHOLD ? 'block' : 'warn');
198
- const severity = action === 'block' ? 'critical' : 'medium';
196
+ function buildGateRule(group, actionOverride) {
197
+ const action = actionOverride || (group.count === 'MANUAL' ? group.manualAction || 'block' : (group.count >= BLOCK_THRESHOLD ? 'block' : 'warn'));
198
+ const severity = action === 'block' ? 'critical' : action === 'approve' ? 'high' : 'medium';
199
199
  const context = group.latestContext.slice(0, 120);
200
200
  const kind = group.key.startsWith('diagnosis:')
201
201
  ? 'repeated diagnosis'
@@ -332,7 +332,8 @@ function forcePromote(context, action = 'block') {
332
332
  return { gateId, action, totalGates: data.gates.length };
333
333
  }
334
334
 
335
- function promote(feedbackLogPath) {
335
+ function promote(feedbackLogPath, options) {
336
+ const opts = options || {};
336
337
  const logPath = feedbackLogPath || getFeedbackLogPath();
337
338
  const entries = readJSONL(logPath);
338
339
  const groups = groupNegativeFeedback(entries, WINDOW_DAYS);
@@ -366,8 +367,8 @@ function promote(feedbackLogPath) {
366
367
  continue;
367
368
  }
368
369
 
369
- // New gate
370
- const gate = buildGateRule(group);
370
+ // New gate — respect explicit gateAction override (e.g. 'approve' for human-approval rules)
371
+ const gate = buildGateRule(group, opts.gateAction);
371
372
 
372
373
  // Enforce max limit — rotate oldest
373
374
  if (data.gates.length >= MAX_AUTO_GATES) {
@@ -444,6 +444,57 @@ function buildCheckoutProductData({ name, description, appOrigin, planId }) {
444
444
  };
445
445
  }
446
446
 
447
+ /**
448
+ * Verify an ACTIVE Stripe product exists for the given plan name before
449
+ * we let buildSubscriptionPriceData create inline price_data under it.
450
+ *
451
+ * Stripe matches product_data by `name`. If only an archived product
452
+ * matches, new prices created via that path inherit active=false and the
453
+ * generated checkout URL renders "Something went wrong / The page you
454
+ * were looking for could not be found." for the buyer. Stripe Dashboard
455
+ * shows the session as `open` with no email captured — looks like the
456
+ * buyer abandoned, but they were never given a working page.
457
+ *
458
+ * Failing here surfaces the misconfiguration at the first checkout
459
+ * attempt instead of silently breaking every buyer for days.
460
+ *
461
+ * Verified incident: ThumbGate#2188 (May 2026) — 20 sessions abandoned in
462
+ * 7 days, all because the only product named "ThumbGate Pro" matching the
463
+ * inline product_data was archived (prod_UXxOHAfbDsPyRb), while an active
464
+ * product with the same name existed (prod_UW82THPxfNvwKT) that should
465
+ * have been used instead.
466
+ */
467
+ async function verifyActiveProductForPlan(stripe, planId) {
468
+ const expectedName = planId === 'team' ? 'ThumbGate Team' : 'ThumbGate Pro';
469
+ let products;
470
+ try {
471
+ products = await stripe.products.list({ limit: 100 });
472
+ } catch (err) {
473
+ // Network/transient failures shouldn't block checkout creation.
474
+ // The original session.create call will surface real Stripe errors.
475
+ return;
476
+ }
477
+ const matching = (products && products.data ? products.data : [])
478
+ .filter((p) => p && p.name === expectedName);
479
+ if (matching.length === 0) {
480
+ // No product with this name exists; Stripe will create a new one when
481
+ // session.create fires with inline product_data. Safe path.
482
+ return;
483
+ }
484
+ const active = matching.find((p) => p.active === true);
485
+ if (!active) {
486
+ const archived = matching[0];
487
+ throw new Error(
488
+ `Refusing to create checkout session: Stripe product named "${expectedName}" ` +
489
+ `exists only in archived state (id=${archived.id}, active=false). New prices ` +
490
+ `created via inline product_data would inherit active=false, rendering ` +
491
+ `"page not found" on Stripe checkout for every buyer. Fix: reactivate the ` +
492
+ `archived product in Stripe Dashboard, or rename the active product to ` +
493
+ `match "${expectedName}". See ThumbGate#2188 for the May 2026 incident.`
494
+ );
495
+ }
496
+ }
497
+
447
498
  function buildSubscriptionPriceData(checkoutSelection, appOrigin) {
448
499
  const isTeam = checkoutSelection.planId === 'team';
449
500
  const annual = checkoutSelection.billingCycle === 'annual';
@@ -2525,6 +2576,18 @@ async function createCheckoutSession({ successUrl, cancelUrl, customerEmail, ins
2525
2576
  }
2526
2577
 
2527
2578
  const stripe = getStripeClient();
2579
+
2580
+ // Defensive guard against ThumbGate#2188:
2581
+ // When buildSubscriptionPriceData passes inline `product_data` to Stripe,
2582
+ // Stripe name-matches existing products. If the only existing product with
2583
+ // that name is ARCHIVED (active=false), the new price inherits active=false
2584
+ // and every Stripe checkout page renders "page not found" for the buyer.
2585
+ // That bug burnt 20+ silent abandoned sessions in May 2026. Fail fast
2586
+ // instead of letting the broken page ship.
2587
+ if (!packId) {
2588
+ await verifyActiveProductForPlan(stripe, checkoutSelection.planId);
2589
+ }
2590
+
2528
2591
  const sessionPayload = buildCheckoutSessionPayload({
2529
2592
  successUrl,
2530
2593
  cancelUrl,
@@ -3127,6 +3190,7 @@ module.exports = {
3127
3190
  _buildTrialActivationEmail: buildTrialActivationEmail,
3128
3191
  _sendTrialActivationEmail: sendTrialActivationEmail,
3129
3192
  _resolveSubscriptionCheckoutSelection: resolveSubscriptionCheckoutSelection,
3193
+ _verifyActiveProductForPlan: verifyActiveProductForPlan,
3130
3194
  _API_KEYS_PATH: () => CONFIG.API_KEYS_PATH,
3131
3195
  _FUNNEL_LEDGER_PATH: () => CONFIG.FUNNEL_LEDGER_PATH,
3132
3196
  _REVENUE_LEDGER_PATH: () => CONFIG.REVENUE_LEDGER_PATH,
@@ -111,13 +111,41 @@ function assembleGuards(toolName, toolInput) {
111
111
  }
112
112
  }
113
113
 
114
- function assembleContextPack(query, agentProfile) {
114
+ function assembleContextPack(query, agentProfile, options = {}) {
115
+ const { guards } = options;
115
116
  try {
116
117
  ensureContextFs();
118
+
119
+ // 1. Proactive Governance: Filter what the agent sees based on prevention rules
120
+ let structuredQuery = query;
121
+ if (guards && guards.mode === 'block') {
122
+ structuredQuery = `${query} (Active Block Policy: ${guards.reason})`;
123
+ }
124
+
125
+ // 2. Elevate Thompson Sampling to Architecture Level
126
+ let strategy = null;
127
+ try {
128
+ const ts = require('./thompson-sampling');
129
+ const model = ts.loadModel();
130
+ const bestCategory = ts.argmaxPosteriors(model);
131
+
132
+ // Route between context-building strategies based on TS posterior mean
133
+ if (bestCategory === 'architecture' || bestCategory === 'infra') {
134
+ strategy = 'hierarchical';
135
+ } else if (bestCategory === 'observability' || bestCategory === 'debugging') {
136
+ strategy = 'summarize-then-expand';
137
+ } else {
138
+ strategy = 'semantic';
139
+ }
140
+ } catch (e) {
141
+ // Fallback to default routing
142
+ }
143
+
117
144
  return constructContextPack({
118
- query,
145
+ query: structuredQuery,
119
146
  maxItems: Math.min(8, Math.ceil(agentProfile.contextBudget / 1000)),
120
147
  maxChars: agentProfile.contextBudget,
148
+ strategy
121
149
  });
122
150
  } catch {
123
151
  return null;
@@ -228,6 +256,12 @@ function assembleUnifiedContext(params = {}) {
228
256
  })),
229
257
  visibility: contextPack.visibility || null,
230
258
  cached: !!(contextPack.cache && contextPack.cache.hit),
259
+ layers: {
260
+ localState: session || null,
261
+ graphState: codeGraph || null,
262
+ policyState: guards || null,
263
+ sessionState: contextPack.items ? contextPack.items.filter(i => i.namespace === 'session') : []
264
+ }
231
265
  } : null,
232
266
  codeGraph: codeGraph || null,
233
267
  assembledAt: new Date().toISOString(),
@@ -306,6 +340,12 @@ function formatUnifiedContext(ctx) {
306
340
 
307
341
  // Context pack
308
342
  if (ctx.contextPack) {
343
+ lines.push(`### Context Architecture Layers`);
344
+ lines.push(`- Local State: ${ctx.contextPack.layers.localState ? 'Active' : 'Empty'}`);
345
+ lines.push(`- Graph State: ${ctx.contextPack.layers.graphState ? 'Active' : 'Empty'}`);
346
+ lines.push(`- Policy State: ${ctx.contextPack.layers.policyState ? ctx.contextPack.layers.policyState.mode : 'Empty'}`);
347
+ lines.push(`- Session State: ${ctx.contextPack.layers.sessionState.length} items`);
348
+ lines.push('');
309
349
  lines.push(`### Context Pack (${ctx.contextPack.itemCount} items)`);
310
350
  ctx.contextPack.items.forEach((item) => {
311
351
  lines.push(`- [${item.namespace}] ${item.title} (score: ${item.score})`);
@@ -1059,6 +1059,7 @@ function captureFeedback(params) {
1059
1059
  : null),
1060
1060
  structuredRule: structuredRule || null,
1061
1061
  ...(reflection && { reflection }),
1062
+ gateAction: params.gateAction || null,
1062
1063
  timestamp: now,
1063
1064
  };
1064
1065
 
@@ -1398,7 +1399,7 @@ function captureFeedback(params) {
1398
1399
  if (feedbackEvent.signal === 'negative') {
1399
1400
  try {
1400
1401
  const autoPromote = require('./auto-promote-gates');
1401
- const promoteResult = autoPromote.promote(FEEDBACK_LOG_PATH);
1402
+ const promoteResult = autoPromote.promote(FEEDBACK_LOG_PATH, { gateAction: feedbackEvent.gateAction });
1402
1403
  // First-rule activation telemetry: anonymous ping the first time
1403
1404
  // a prevention rule auto-promotes for this install. Idempotent —
1404
1405
  // see scripts/activation-tracker.js. Critical for activation funnel