thumbgate 1.21.2 β†’ 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thumbgate-marketplace",
3
- "version": "1.21.2",
3
+ "version": "1.22.0",
4
4
  "owner": {
5
5
  "name": "Igor Ganapolsky",
6
6
  "email": "ig5973700@gmail.com"
@@ -14,7 +14,7 @@
14
14
  "source": "npm",
15
15
  "package": "thumbgate"
16
16
  },
17
- "version": "1.21.2",
17
+ "version": "1.22.0",
18
18
  "author": {
19
19
  "name": "Igor Ganapolsky",
20
20
  "email": "ig5973700@gmail.com",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "thumbgate",
3
3
  "description": "One πŸ‘Ž becomes a hard rule the agent cannot bypass. Captures thumbs-down feedback, distills it into PreToolUse Pre-Action Checks, enforced across every future Claude Code session.",
4
- "version": "1.21.2",
4
+ "version": "1.22.0",
5
5
  "author": {
6
6
  "name": "Igor Ganapolsky",
7
7
  "email": "ig5973700@gmail.com",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thumbgate",
3
- "version": "1.21.2",
3
+ "version": "1.22.0",
4
4
  "description": "ThumbGate β€” πŸ‘πŸ‘Ž feedback that teaches your AI agent. Thumbs down a mistake, it never happens again.",
5
5
  "homepage": "https://thumbgate-production.up.railway.app",
6
6
  "transport": "stdio",
@@ -2,13 +2,13 @@
2
2
  "mcpServers": {
3
3
  "thumbgate": {
4
4
  "command": "npx",
5
- "args": ["--yes", "--package", "thumbgate@1.21.2", "thumbgate", "serve"]
5
+ "args": ["--yes", "--package", "thumbgate@1.22.0", "thumbgate", "serve"]
6
6
  }
7
7
  },
8
8
  "hooks": {
9
9
  "preToolUse": {
10
10
  "command": "npx",
11
- "args": ["--yes", "--package", "thumbgate@1.21.2", "thumbgate", "gate-check"]
11
+ "args": ["--yes", "--package", "thumbgate@1.22.0", "thumbgate", "gate-check"]
12
12
  }
13
13
  }
14
14
  }
@@ -216,7 +216,7 @@ const {
216
216
  finalizeSession: finalizeFeedbackSession,
217
217
  } = require('../../scripts/feedback-session');
218
218
 
219
- const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.21.2' };
219
+ const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.22.0' };
220
220
  const COMMERCE_CATEGORIES = [
221
221
  'product_recommendation',
222
222
  'brand_compliance',
@@ -368,6 +368,111 @@ function buildRecallResponse(args = {}) {
368
368
  return toTextResult(text);
369
369
  }
370
370
 
371
+ function buildSuggestFixResponse(args = {}) {
372
+ const context = String(args.context || '').trim();
373
+ const rawLimit = Number(args.limit);
374
+ const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, 5) : 3;
375
+
376
+ // If no context provided, return generic suggestion
377
+ if (!context) {
378
+ return toTextResult({
379
+ suggestions: [
380
+ {
381
+ action: 'Capture feedback about what went wrong so ThumbGate can learn and prevent recurrence.',
382
+ source: 'generic',
383
+ },
384
+ ],
385
+ query: '',
386
+ totalFound: 0,
387
+ });
388
+ }
389
+
390
+ // Search lessons via lesson-search module
391
+ const lessonModule = loadPrivateMcpModule('lessonSearch');
392
+ let lessonActions = [];
393
+ if (lessonModule) {
394
+ try {
395
+ const searchResult = lessonModule.searchLessons(context, { limit: 10 });
396
+ const results = Array.isArray(searchResult && searchResult.results) ? searchResult.results : [];
397
+ for (const result of results) {
398
+ const correctiveActions = (result.systemResponse && Array.isArray(result.systemResponse.correctiveActions))
399
+ ? result.systemResponse.correctiveActions
400
+ : [];
401
+ for (const action of correctiveActions) {
402
+ const text = String(action.text || '').trim();
403
+ if (text) {
404
+ lessonActions.push({
405
+ action: text,
406
+ source: action.source || `lesson:${result.id || 'unknown'}`,
407
+ score: result.score || 0,
408
+ });
409
+ }
410
+ }
411
+ // Also pick up lesson-level howToAvoid / actionNeeded when no explicit correctiveActions
412
+ if (correctiveActions.length === 0 && result.lesson) {
413
+ const text = result.lesson.howToAvoid || result.lesson.actionNeeded || '';
414
+ if (text) {
415
+ lessonActions.push({
416
+ action: String(text).trim(),
417
+ source: `lesson:${result.id || 'unknown'}`,
418
+ score: result.score || 0,
419
+ });
420
+ }
421
+ }
422
+ }
423
+ } catch {
424
+ // lesson search failure is non-fatal
425
+ }
426
+ }
427
+
428
+ // Search prevention rules directly via lesson-search module's helper
429
+ let ruleActions = [];
430
+ try {
431
+ const { readPreventionRuleMatches } = require('../../scripts/lesson-search');
432
+ const ruleMatches = readPreventionRuleMatches(context, limit);
433
+ for (const rule of ruleMatches) {
434
+ const text = rule.summary || rule.title || '';
435
+ if (text) {
436
+ ruleActions.push({
437
+ action: String(text).trim(),
438
+ source: `rule:${String(rule.title || 'prevention_rules').trim()}`,
439
+ score: rule.score || 0,
440
+ });
441
+ }
442
+ }
443
+ } catch {
444
+ // rule search failure is non-fatal
445
+ }
446
+
447
+ // Merge, deduplicate, sort by score, and take top `limit`
448
+ const seen = new Set();
449
+ const all = [...lessonActions, ...ruleActions]
450
+ .filter((item) => {
451
+ if (!item.action) return false;
452
+ const key = item.action.toLowerCase();
453
+ if (seen.has(key)) return false;
454
+ seen.add(key);
455
+ return true;
456
+ })
457
+ .sort((a, b) => (b.score || 0) - (a.score || 0))
458
+ .slice(0, limit)
459
+ .map(({ action, source }) => ({ action, source }));
460
+
461
+ // If nothing matched, add generic fallback
462
+ if (all.length === 0) {
463
+ all.push({
464
+ action: 'No matching lessons or rules found. Capture feedback via capture_feedback so ThumbGate can learn from this failure.',
465
+ source: 'generic',
466
+ });
467
+ }
468
+
469
+ return toTextResult({
470
+ suggestions: all,
471
+ query: context,
472
+ totalFound: all.length,
473
+ });
474
+ }
475
+
371
476
  function buildDiagnoseFailureResponse(args = {}) {
372
477
  let intentPlan = null;
373
478
  const requestedProfile = args.mcpProfile || getActiveMcpProfile();
@@ -579,6 +684,8 @@ async function callToolInner(name, args) {
579
684
  tags: Array.isArray(args.tags) ? args.tags : [],
580
685
  }));
581
686
  }
687
+ case 'suggest_fix':
688
+ return buildSuggestFixResponse(args);
582
689
  case 'retrieve_lessons': {
583
690
  // Cross-encoder reranking: retrieve more candidates, then rerank for precision
584
691
  const { retrieveWithRerankingSync } = loadOptionalModule(path.join(__dirname, '../../scripts/cross-encoder-reranker'), () => ({
@@ -1330,6 +1437,7 @@ module.exports = {
1330
1437
  acquireLock,
1331
1438
  toCaptureFeedbackTextResult,
1332
1439
  formatCorrectiveActionsReminder,
1440
+ buildSuggestFixResponse,
1333
1441
  __test__: {
1334
1442
  PRIVATE_MCP_MODULES,
1335
1443
  loadPrivateMcpModule,
@@ -7,7 +7,7 @@
7
7
  "npx",
8
8
  "--yes",
9
9
  "--package",
10
- "thumbgate@1.21.2",
10
+ "thumbgate@1.22.0",
11
11
  "thumbgate",
12
12
  "serve"
13
13
  ],
package/bin/cli.js CHANGED
@@ -878,7 +878,14 @@ function stats() {
878
878
  const { analyzeFeedback } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
879
879
  const data = analyzeFeedback();
880
880
 
881
+ // Gate enforcement stats β€” runtime intercepts + configured gates
882
+ let gateData = { blocked: 0, warned: 0, passed: 0, byGate: {} };
883
+ try { gateData = require(path.join(PKG_ROOT, 'scripts', 'gates-engine')).loadStats(); } catch {}
884
+ let gateConfigData = { totalGates: 0, autoPromotedGates: 0, estimatedHoursSaved: '0.0', topBlocked: null, firstTimeFixRate: null };
885
+ try { gateConfigData = require(path.join(PKG_ROOT, 'scripts', 'gate-stats')).calculateStats(); } catch {}
886
+
881
887
  const avgCostOfMistake = 2.50;
888
+ const totalInterceptions = gateData.blocked + gateData.warned;
882
889
  const payload = {
883
890
  total: data.total,
884
891
  positives: data.totalPositive,
@@ -888,6 +895,13 @@ function stats() {
888
895
  revenueAtRisk: Number((data.totalNegative * avgCostOfMistake).toFixed(2)),
889
896
  topTags: data.topTags || [],
890
897
  recentActivity: data.recentActivity || [],
898
+ gatesBlocked: gateData.blocked,
899
+ gatesWarned: gateData.warned,
900
+ totalGates: gateConfigData.totalGates,
901
+ autoPromotedGates: gateConfigData.autoPromotedGates,
902
+ estimatedHoursSaved: gateConfigData.estimatedHoursSaved,
903
+ topBlockedGate: gateConfigData.topBlocked ? gateConfigData.topBlocked.id : null,
904
+ firstTimeFixRate: gateConfigData.firstTimeFixRate,
891
905
  };
892
906
 
893
907
  if (args.json) {
@@ -901,6 +915,18 @@ function stats() {
901
915
  console.log(` Approval Rate : ${payload.approvalRate}%`);
902
916
  console.log(` Recent Trend : ${payload.recentTrend}%`);
903
917
 
918
+ // Gate enforcement β€” the high-ROI section
919
+ if (totalInterceptions > 0 || payload.totalGates > 0) {
920
+ console.log('\nπŸ›‘οΈ PRE-ACTION GATE ENFORCEMENT');
921
+ console.log(` Actions blocked : ${payload.gatesBlocked}`);
922
+ console.log(` Actions warned : ${payload.gatesWarned}`);
923
+ console.log(` Active gates : ${payload.totalGates} (${payload.autoPromotedGates} auto-promoted)`);
924
+ if (payload.topBlockedGate) console.log(` Top blocker : ${payload.topBlockedGate}`);
925
+ console.log(` Est. time saved : ~${payload.estimatedHoursSaved} hours`);
926
+ const { formatFirstTimeFixRate } = require(path.join(PKG_ROOT, 'scripts', 'gate-stats'));
927
+ console.log(` First-time fix : ${formatFirstTimeFixRate(payload.firstTimeFixRate)}`);
928
+ }
929
+
904
930
  if (payload.negatives > 0) {
905
931
  console.log('\n⚠️ REVENUE-AT-RISK ANALYSIS');
906
932
  console.log(` Repeated Failures detected: ${payload.negatives}`);
@@ -1684,6 +1710,25 @@ function gateStats() {
1684
1710
  console.log('\n' + formatStats(stats) + '\n');
1685
1711
  }
1686
1712
 
1713
+ function contextPacks() {
1714
+ const args = parseArgs(process.argv.slice(3));
1715
+ const { generateAutoContextPacks } = require(path.join(PKG_ROOT, 'scripts', 'auto-context-packs'));
1716
+ const result = generateAutoContextPacks();
1717
+ if (args.json) {
1718
+ console.log(JSON.stringify(result, null, 2));
1719
+ return;
1720
+ }
1721
+ console.log(`\nGenerated ${result.packCount} context pack(s):`);
1722
+ for (const p of result.packs) {
1723
+ console.log(` [${p.type}] ${p.name}`);
1724
+ console.log(` -> ${p.filePath}`);
1725
+ }
1726
+ if (result.packCount === 0) {
1727
+ console.log(' (No failure patterns found yet β€” capture some feedback first.)');
1728
+ }
1729
+ console.log('');
1730
+ }
1731
+
1687
1732
  function harnessAudit() {
1688
1733
  const args = parseArgs(process.argv.slice(3));
1689
1734
  const {
@@ -2486,6 +2531,11 @@ switch (COMMAND) {
2486
2531
  case 'search-lessons':
2487
2532
  lessons();
2488
2533
  break;
2534
+ case 'notes': {
2535
+ const { cli: notesCli } = require(path.join(PKG_ROOT, 'scripts', 'implementation-notes'));
2536
+ notesCli(process.argv.slice(3));
2537
+ break;
2538
+ }
2489
2539
  case 'lesson-health':
2490
2540
  case 'stale': {
2491
2541
  const { initDB } = require(path.join(PKG_ROOT, 'scripts', 'lesson-db'));
@@ -2627,6 +2677,9 @@ switch (COMMAND) {
2627
2677
  case 'rules':
2628
2678
  rules();
2629
2679
  break;
2680
+ case 'context-packs':
2681
+ contextPacks();
2682
+ break;
2630
2683
  case 'harness-audit':
2631
2684
  case 'harness':
2632
2685
  harnessAudit();
@@ -68,7 +68,8 @@
68
68
  "perplexity_search",
69
69
  "perplexity_ask",
70
70
  "perplexity_research",
71
- "perplexity_reason"
71
+ "perplexity_reason",
72
+ "suggest_fix"
72
73
  ],
73
74
  "essential": [
74
75
  "capture_feedback",
@@ -104,7 +105,8 @@
104
105
  "report_product_issue",
105
106
  "require_evidence_for_claim",
106
107
  "session_report",
107
- "generate_operator_artifact"
108
+ "generate_operator_artifact",
109
+ "suggest_fix"
108
110
  ],
109
111
  "commerce": [
110
112
  "capture_feedback",
@@ -123,7 +125,8 @@
123
125
  "workflow_sentinel",
124
126
  "prevention_rules",
125
127
  "feedback_stats",
126
- "feedback_summary"
128
+ "feedback_summary",
129
+ "suggest_fix"
127
130
  ],
128
131
  "readonly": [
129
132
  "recall",
@@ -164,7 +167,8 @@
164
167
  "session_report",
165
168
  "generate_operator_artifact",
166
169
  "perplexity_search",
167
- "perplexity_ask"
170
+ "perplexity_ask",
171
+ "suggest_fix"
168
172
  ],
169
173
  "dispatch": [
170
174
  "recall",
@@ -204,7 +208,8 @@
204
208
  "session_report",
205
209
  "generate_operator_artifact",
206
210
  "perplexity_search",
207
- "perplexity_ask"
211
+ "perplexity_ask",
212
+ "suggest_fix"
208
213
  ],
209
214
  "locked": [
210
215
  "feedback_summary",
@@ -228,7 +233,8 @@
228
233
  "workflow_sentinel",
229
234
  "settings_status",
230
235
  "native_messaging_audit",
231
- "generate_operator_artifact"
236
+ "generate_operator_artifact",
237
+ "suggest_fix"
232
238
  ]
233
239
  }
234
240
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thumbgate",
3
- "version": "1.21.2",
3
+ "version": "1.22.0",
4
4
  "description": "ThumbGate self-improving agent governance: thumbs-up/down turns every mistake into a prevention rule and blocks repeat patterns. 33 pre-action checks, budget enforcement, and self-protection for Claude Code, Cursor, Codex, Gemini CLI, and Amp.",
5
5
  "homepage": "https://thumbgate-production.up.railway.app",
6
6
  "repository": {
@@ -223,6 +223,7 @@
223
223
  "openapi/",
224
224
  "public/agent-manager.html",
225
225
  "public/blog.html",
226
+ "public/codex-enterprise.html",
226
227
  "public/codex-plugin.html",
227
228
  "public/compare.html",
228
229
  "public/dashboard.html",
@@ -333,7 +334,7 @@
333
334
  "social:prospect:bluesky:dry": "node scripts/social-bluesky-prospecting.js --dry-run",
334
335
  "social:reply-publish:bluesky:dry": "node scripts/social-reply-monitor-bluesky.js --publish-approved --dry-run",
335
336
  "test:python": "python3 -m pytest tests/*.py",
336
- "test": "npm run test:python && npm run test:schema && npm run test:loop && npm run test:dpo && npm run test:kto && npm run test:api && npm run test:proof && npm run test:e2e && npm run test:rlaif && npm run test:attribution && npm run test:quality && npm run test:intelligence && npm run test:training-export && npm run test:deployment && npm run test:operational-integrity && npm run test:workflow && npm run test:billing && npm run test:cli && npm run test:watcher && npm run test:autoresearch && npm run test:ops && npm run test:session-analyzer && npm run test:tessl && npm run test:gates && npm run test:evoskill && npm run test:gates-hardening && npm run test:workers && npm run test:social-analytics && npm run test:memalign && npm run test:xmemory-lite && npm run test:filesystem-search && npm run test:zernio && npm run test:platform-limits && npm run test:post-video && npm run test:post-everywhere-instagram && npm run test:post-everywhere-channels && npm run test:post-everywhere-zernio-default && npm run test:zernio-canonical-pollers && npm run test:zernio-status && npm run test:obsidian-export && npm run test:lesson-db && npm run test:lesson-rotation && npm run test:memory-dedup && npm run test:feedback-quality && npm run test:sync-version && npm run test:check-congruence && npm run test:tool-registry && npm run test:feedback-to-rules && npm run test:memory-firewall && npm run test:memory-scope-readiness && npm run test:belief-update && npm run test:hosted-config && npm run test:operational-summary && npm run test:operational-dashboard && npm run test:operator-artifacts && npm run test:operator-key-auth && npm run test:cloudflare-sandbox && npm run test:mcp-config && npm run test:plan-gate && npm run test:pulse && npm run test:semantic-layer && npm run test:data-pipeline && npm run test:optimize-context && npm run test:principle-extractor && npm run test:analytics-window && npm run test:funnel-analytics && npm run test:experiment-tracker && npm run test:build-metadata && npm run test:context-engine && npm run test:hf-papers && npm run test:marketing-experiment && npm run test:seo-gsd && npm run test:verify-run && npm run test:export-dpo-pairs && npm run test:export-hf-dataset && npm run test:license && npm run test:bot-detector && npm run test:audit-pr-bot-contamination && npm run test:stripe-bootstrap-saas-catalog && npm run test:postinstall && npm run test:funnel-invariants && npm run test:cli-telemetry && npm run test:pro-parity && npm run test:model-tier-router && npm run test:computer-use-firewall && npm run test:skill-exporter && npm run test:statusline && npm run test:evolution && npm run test:org-dashboard && npm run test:multi-hop-recall && npm run test:synthetic-dpo && npm run test:thumbgate-skill && npm run test:learn-hub && npm run test:feedback-fallback && npm run test:metaclaw && npm run test:server-lock && npm run test:control-tower && npm run test:pii-scanner && npm run test:data-governance && npm run test:lesson-inference && npm run test:semantic-dedup && npm run test:fs-utils && npm run test:cli-schema && npm run test:explore && npm run test:lesson-reranker && npm run test:lesson-retrieval && npm run test:cross-encoder && npm run test:reflector-agent && npm run test:feedback-session && npm run test:feedback-history-distiller && npm run test:hallucination-detector && npm run test:history-distiller && npm run test:predictive-insights && npm run test:prove-predictive-insights && npm run test:statusbar-cli && npm run test:generate-instagram-card && npm run test:instagram-thumbgate-post && npm run test:publish-instagram-thumbgate && npm run test:lesson-synthesis && npm run test:lesson-canonical && npm run test:background-governance && npm run test:memory-migration && npm run test:prompt-dlp && npm run test:ephemeral-store && npm run test:agent-security && npm run test:skill-progressive && npm run test:per-step-scoring && npm run test:weekly-auto-post && npm run test:social-post-hourly && npm run test:social-quality-gate && npm run test:a2ui-engine && npm run test:gate-satisfy && npm run test:money-watcher && npm run test:budget && npm run test:quick-start && npm run test:utm && npm run test:product-feedback && npm run test:feedback-root-consolidator && npm run test:engagement-audit && npm run test:install-growth-automation && npm run test:publish-thumbgate-launch && npm run test:community-course-platform-launch-kit && npm run test:reconcile-thumbgate-campaign && npm run test:reddit-publisher && npm run test:schedule-thumbgate-campaign && npm run test:social-reply-monitor && npm run test:social-dedupe-cleanup && npm run test:sync-launch-assets && npm run test:ai-search-visibility && npm run test:perplexity && npm run test:security-scanner && npm run test:llm-client && npm run test:managed-lesson-agent && npm run test:self-distill && npm run test:meta-agent && npm run test:harness-selector && npm run test:thumbgate-bench && npm run test:seo-guides && npm run test:enforcement-loop && npm run test:cli-agent-experience && npm run test:bot-detection && npm run test:checkout-archived-product-guard && npm run test:postgres-guard && npm run test:checkout-bot-guard && npm run test:checkout-pro-confirmation-gate && npm run test:session-health && npm run test:session-episodes && npm run test:spec-gate && npm run test:decision-trace && npm run test:dashboard-insights && npm run test:telemetry-tracked-link-slug && npm run test:prompt-eval && npm run test:demo-voiceover && npm run test:gate-coherence && npm run test:gate-eval && npm run test:high-roi && npm run test:public-static-assets && npm run test:token-savings && npm run test:numbers-page && npm run test:workflow-gate-checkpoint && npm run test:lesson-export-import && npm run test:landing-page-claims && npm run test:competitive-positioning-marketing && npm run test:medium-weekly && npm run test:dashboard-deeplink-e2e && npm run test:public-package-parity && npm run test:token-savings-dashboard && npm run test:cursor-wiring && npm run test:pretooluse-injection && npm run test:recent-corrective-context && npm run test:durability-step && npm run test:mailer && npm run test:brand-assets && npm run test:enforcement-teeth && npm run test:bayes-optimal-gate && npm run test:swarm-coordinator && npm run test:session-report && npm run test:agent-reasoning-traces && npm run test:judge-reward && npm run test:llm-behavior-monitor && npm run test:prompting-os && npm run test:single-use-credential-gate && npm run test:structured-prompt-driven && npm run test:require-evidence-gate && npm run test:rule-validator && npm run test:bluesky-atproto && npm run test:social-reply-monitor-bluesky && npm run test:bluesky-delete-replies && npm run test:architect-kit-memory-bridge && npm run test:sonar-review-hotspots && npm run test:actionable-remediations && npm run test:gemini-embedding-policy && npm run test:agent-design-governance && npm run test:public-core-boundary && npm run test:hook-stop-verify-deploy && npm run test:hook-stop-anti-claim && npm run test:plausible-server-events && npm run test:activation-tracker && npm run test:unified-revenue-rollup && npm run test:conversion-rate-stats && npm run test:external-customer-audit && npm run test:telemetry-export && npm run test:stripe-checkout-diagnostic && npm run test:stripe-business-identity-probe && npm run test:revenue-observability-doctor && npm run test:public-bundle-ratchet && npm run test:stripe-payment-link-update && npm run test:ci-cd-hygiene-audit && npm run test:verify-marketing-pages-deployed && npm run test:install-email-capture",
337
+ "test": "npm run test:python && npm run test:schema && npm run test:loop && npm run test:dpo && npm run test:kto && npm run test:api && npm run test:proof && npm run test:e2e && npm run test:rlaif && npm run test:attribution && npm run test:quality && npm run test:intelligence && npm run test:training-export && npm run test:deployment && npm run test:operational-integrity && npm run test:workflow && npm run test:billing && npm run test:cli && npm run test:watcher && npm run test:autoresearch && npm run test:ops && npm run test:session-analyzer && npm run test:tessl && npm run test:gates && npm run test:evoskill && npm run test:gates-hardening && npm run test:workers && npm run test:social-analytics && npm run test:memalign && npm run test:xmemory-lite && npm run test:filesystem-search && npm run test:zernio && npm run test:platform-limits && npm run test:post-video && npm run test:post-everywhere-instagram && npm run test:post-everywhere-channels && npm run test:post-everywhere-zernio-default && npm run test:zernio-canonical-pollers && npm run test:zernio-status && npm run test:obsidian-export && npm run test:lesson-db && npm run test:lesson-rotation && npm run test:memory-dedup && npm run test:feedback-quality && npm run test:sync-version && npm run test:check-congruence && npm run test:tool-registry && npm run test:feedback-to-rules && npm run test:memory-firewall && npm run test:memory-scope-readiness && npm run test:belief-update && npm run test:hosted-config && npm run test:operational-summary && npm run test:operational-dashboard && npm run test:operator-artifacts && npm run test:operator-key-auth && npm run test:cloudflare-sandbox && npm run test:mcp-config && npm run test:plan-gate && npm run test:pulse && npm run test:semantic-layer && npm run test:data-pipeline && npm run test:optimize-context && npm run test:principle-extractor && npm run test:analytics-window && npm run test:funnel-analytics && npm run test:experiment-tracker && npm run test:build-metadata && npm run test:context-engine && npm run test:hf-papers && npm run test:marketing-experiment && npm run test:seo-gsd && npm run test:verify-run && npm run test:export-dpo-pairs && npm run test:export-hf-dataset && npm run test:license && npm run test:bot-detector && npm run test:audit-pr-bot-contamination && npm run test:stripe-bootstrap-saas-catalog && npm run test:postinstall && npm run test:funnel-invariants && npm run test:cli-telemetry && npm run test:pro-parity && npm run test:model-tier-router && npm run test:computer-use-firewall && npm run test:skill-exporter && npm run test:statusline && npm run test:evolution && npm run test:org-dashboard && npm run test:multi-hop-recall && npm run test:synthetic-dpo && npm run test:thumbgate-skill && npm run test:learn-hub && npm run test:feedback-fallback && npm run test:metaclaw && npm run test:server-lock && npm run test:control-tower && npm run test:pii-scanner && npm run test:data-governance && npm run test:lesson-inference && npm run test:semantic-dedup && npm run test:fs-utils && npm run test:cli-schema && npm run test:explore && npm run test:lesson-reranker && npm run test:lesson-retrieval && npm run test:cross-encoder && npm run test:reflector-agent && npm run test:feedback-session && npm run test:feedback-history-distiller && npm run test:hallucination-detector && npm run test:history-distiller && npm run test:predictive-insights && npm run test:prove-predictive-insights && npm run test:statusbar-cli && npm run test:generate-instagram-card && npm run test:instagram-thumbgate-post && npm run test:publish-instagram-thumbgate && npm run test:lesson-synthesis && npm run test:lesson-canonical && npm run test:background-governance && npm run test:memory-migration && npm run test:prompt-dlp && npm run test:ephemeral-store && npm run test:agent-security && npm run test:skill-progressive && npm run test:per-step-scoring && npm run test:weekly-auto-post && npm run test:social-post-hourly && npm run test:social-quality-gate && npm run test:a2ui-engine && npm run test:gate-satisfy && npm run test:money-watcher && npm run test:budget && npm run test:quick-start && npm run test:utm && npm run test:product-feedback && npm run test:feedback-root-consolidator && npm run test:engagement-audit && npm run test:install-growth-automation && npm run test:publish-thumbgate-launch && npm run test:community-course-platform-launch-kit && npm run test:reconcile-thumbgate-campaign && npm run test:reddit-publisher && npm run test:schedule-thumbgate-campaign && npm run test:social-reply-monitor && npm run test:social-dedupe-cleanup && npm run test:sync-launch-assets && npm run test:ai-search-visibility && npm run test:perplexity && npm run test:security-scanner && npm run test:llm-client && npm run test:managed-lesson-agent && npm run test:self-distill && npm run test:meta-agent && npm run test:harness-selector && npm run test:thumbgate-bench && npm run test:seo-guides && npm run test:enforcement-loop && npm run test:cli-agent-experience && npm run test:bot-detection && npm run test:checkout-archived-product-guard && npm run test:postgres-guard && npm run test:checkout-bot-guard && npm run test:checkout-pro-confirmation-gate && npm run test:session-health && npm run test:session-episodes && npm run test:spec-gate && npm run test:decision-trace && npm run test:dashboard-insights && npm run test:telemetry-tracked-link-slug && npm run test:prompt-eval && npm run test:demo-voiceover && npm run test:gate-coherence && npm run test:gate-eval && npm run test:high-roi && npm run test:public-static-assets && npm run test:token-savings && npm run test:numbers-page && npm run test:workflow-gate-checkpoint && npm run test:lesson-export-import && npm run test:landing-page-claims && npm run test:competitive-positioning-marketing && npm run test:medium-weekly && npm run test:dashboard-deeplink-e2e && npm run test:public-package-parity && npm run test:token-savings-dashboard && npm run test:cursor-wiring && npm run test:pretooluse-injection && npm run test:recent-corrective-context && npm run test:durability-step && npm run test:mailer && npm run test:brand-assets && npm run test:enforcement-teeth && npm run test:bayes-optimal-gate && npm run test:swarm-coordinator && npm run test:session-report && npm run test:agent-reasoning-traces && npm run test:judge-reward && npm run test:llm-behavior-monitor && npm run test:prompting-os && npm run test:single-use-credential-gate && npm run test:structured-prompt-driven && npm run test:require-evidence-gate && npm run test:rule-validator && npm run test:bluesky-atproto && npm run test:social-reply-monitor-bluesky && npm run test:bluesky-delete-replies && npm run test:architect-kit-memory-bridge && npm run test:sonar-review-hotspots && npm run test:actionable-remediations && npm run test:gemini-embedding-policy && npm run test:agent-design-governance && npm run test:public-core-boundary && npm run test:hook-stop-verify-deploy && npm run test:hook-stop-anti-claim && npm run test:plausible-server-events && npm run test:activation-tracker && npm run test:unified-revenue-rollup && npm run test:conversion-rate-stats && npm run test:external-customer-audit && npm run test:telemetry-export && npm run test:stripe-checkout-diagnostic && npm run test:stripe-business-identity-probe && npm run test:revenue-observability-doctor && npm run test:public-bundle-ratchet && npm run test:stripe-payment-link-update && npm run test:ci-cd-hygiene-audit && npm run test:verify-marketing-pages-deployed && npm run test:install-email-capture && npm run test:install-shim && npm run test:hook-runtime-subcommands && npm run test:implementation-notes",
337
338
  "test:hook-stop-verify-deploy": "node --test tests/hook-stop-verify-deploy.test.js",
338
339
  "test:hook-stop-anti-claim": "node --test tests/hook-stop-anti-claim.test.js",
339
340
  "test:plausible-server-events": "node --test tests/plausible-server-events.test.js",
@@ -443,10 +444,10 @@
443
444
  "test:evolution": "node --test tests/workspace-evolver.test.js",
444
445
  "test:watcher": "node --test tests/jsonl-watcher.test.js",
445
446
  "test:autoresearch": "node --test tests/autoresearch.test.js",
446
- "test:ops": "node --test tests/adk-consolidator.test.js tests/anthropic-partner-strategy.test.js tests/auto-promote-gates.test.js tests/auto-wire-hooks.test.js tests/claude-skill.test.js tests/codegraph-context.test.js tests/commercial-signals.test.js tests/decision-journal.test.js tests/delegation-runtime.test.js tests/disagreement-mining.test.js tests/failure-diagnostics.test.js tests/gate-stats.test.js tests/git-hook-installer.test.js tests/github-billing.test.js tests/intervention-policy.test.js tests/markdown-escape.test.js tests/mcp-tools-gates.test.js tests/native-messaging-audit.test.js tests/project-bayes-e2e.test.js tests/project-bayes.test.js tests/rate-limiter.test.js tests/schedule-manager.test.js tests/session-handoff.test.js tests/skill-generator.test.js tests/smart-learning.test.js tests/spike-and-sink.test.js tests/stripe-revenue.test.js tests/stripe-webhook-route.test.js tests/stripe-webhook-rotation.test.js tests/train-from-feedback.test.js tests/workflow-hardening-sprint.test.js tests/workflow-sentinel.test.js tests/test-suite-parity.test.js tests/a2ui-engine.test.js tests/webhook-delivery.test.js",
447
+ "test:ops": "node --test tests/adk-consolidator.test.js tests/anthropic-partner-strategy.test.js tests/auto-promote-gates.test.js tests/auto-wire-hooks.test.js tests/claude-skill.test.js tests/codegraph-context.test.js tests/commercial-signals.test.js tests/decision-journal.test.js tests/delegation-runtime.test.js tests/disagreement-mining.test.js tests/failure-diagnostics.test.js tests/gate-stats.test.js tests/git-hook-installer.test.js tests/github-billing.test.js tests/intervention-policy.test.js tests/markdown-escape.test.js tests/mcp-tools-gates.test.js tests/native-messaging-audit.test.js tests/project-bayes-e2e.test.js tests/project-bayes.test.js tests/rate-limiter.test.js tests/schedule-manager.test.js tests/session-handoff.test.js tests/skill-generator.test.js tests/smart-learning.test.js tests/spike-and-sink.test.js tests/stripe-revenue.test.js tests/stripe-webhook-route.test.js tests/stripe-webhook-rotation.test.js tests/train-from-feedback.test.js tests/workflow-hardening-sprint.test.js tests/workflow-sentinel.test.js tests/test-suite-parity.test.js tests/a2ui-engine.test.js tests/webhook-delivery.test.js tests/auto-context-packs.test.js",
447
448
  "test:session-analyzer": "node --test tests/session-analyzer.test.js",
448
449
  "test:tessl": "node --test tests/tessl-export.test.js",
449
- "test:gates": "node --test tests/gate-templates.test.js tests/gates-engine.test.js tests/claim-verification.test.js tests/secret-scanner.test.js tests/secret-fixture-safety.test.js tests/prompt-guard.test.js tests/audit-trail.test.js tests/profile-router.test.js tests/workflow-sentinel.test.js tests/docker-sandbox-planner.test.js",
450
+ "test:gates": "node --test tests/gate-templates.test.js tests/gates-engine.test.js tests/claim-verification.test.js tests/secret-scanner.test.js tests/secret-fixture-safety.test.js tests/prompt-guard.test.js tests/audit-trail.test.js tests/profile-router.test.js tests/workflow-sentinel.test.js tests/docker-sandbox-planner.test.js tests/mcp-tools-suggest-fix.test.js",
450
451
  "test:budget": "node --test tests/budget-enforcer.test.js",
451
452
  "test:workers": "npm --prefix workers ci && npm --prefix workers test",
452
453
  "test:evoskill": "node --test tests/evoskill.test.js",
@@ -640,6 +641,10 @@
640
641
  "test:competitive-positioning-marketing": "node --test tests/competitive-positioning-marketing.test.js tests/knowledge-graph-guardrails.test.js tests/supply-chain-guardrails.test.js",
641
642
  "test:medium-weekly": "node --test tests/medium-weekly.test.js",
642
643
  "test:dashboard-deeplink-e2e": "node --test tests/dashboard-deeplink-e2e.test.js",
644
+ "test:e2e:playwright": "playwright test",
645
+ "test:e2e:playwright:headed": "playwright test --headed",
646
+ "test:e2e:playwright:ui": "playwright test --ui",
647
+ "test:e2e:playwright:report": "playwright show-report",
643
648
  "test:public-package-parity": "node --test tests/public-package-parity.test.js",
644
649
  "prepare": "bash bin/install-hooks.sh >/dev/null 2>&1 || true",
645
650
  "install:hooks": "bash bin/install-hooks.sh",
@@ -656,7 +661,15 @@
656
661
  "test:stripe-payment-link-update": "node --test tests/stripe-payment-link-update.test.js",
657
662
  "test:verify-marketing-pages-deployed": "node --test tests/verify-marketing-pages-deployed.test.js",
658
663
  "verify:marketing-pages": "node scripts/verify-marketing-pages-deployed.js",
659
- "test:install-email-capture": "node --test tests/install-email-capture.test.js"
664
+ "test:install-email-capture": "node --test tests/install-email-capture.test.js",
665
+ "test:install-shim": "node --test tests/install-shim.test.js",
666
+ "test:hook-runtime-subcommands": "node --test tests/hook-runtime-subcommands.test.js",
667
+ "test:implementation-notes": "node --test tests/implementation-notes.test.js",
668
+ "test:lessons-page-clickability": "playwright test tests/e2e/lessons-page-clickability.spec.js",
669
+ "test:index-page-clickability": "playwright test tests/e2e/index-page-clickability.spec.js",
670
+ "test:dashboard-page-clickability": "playwright test tests/e2e/dashboard-page-clickability.spec.js",
671
+ "test:agent-manager-page-clickability": "playwright test tests/e2e/agent-manager-page-clickability.spec.js",
672
+ "test:pricing-page-clickability": "playwright test tests/e2e/pricing-page-clickability.spec.js"
660
673
  },
661
674
  "keywords": [
662
675
  "mcp",
@@ -731,6 +744,7 @@
731
744
  "devDependencies": {
732
745
  "@changesets/changelog-github": "^0.7.0",
733
746
  "@changesets/cli": "^2.31.0",
747
+ "@playwright/test": "^1.60.0",
734
748
  "c8": "^11.0.0",
735
749
  "undici": "^8.2.0"
736
750
  }
@@ -76,7 +76,7 @@
76
76
  </tr>
77
77
  <tr>
78
78
  <td><strong>Plugin marketplace</strong><br>Deciding which Claude Code / Cursor / Codex plugins are blessed and which are not.</td>
79
- <td>ThumbGate ships as a Claude Code plugin, a Cursor extension, a Codex plugin, and a Gemini CLI hook. One install, every supported agent. Adapter compatibility matrix kept current as runtimes change.</td>
79
+ <td>ThumbGate ships as a Claude Code plugin, a Cursor extension (Marketplace listing pending Cursor's review since 2026-05-19; runtime install works today via <code>npx thumbgate init --agent cursor</code>), a Codex plugin, and a Gemini CLI hook. One install, every supported agent. Adapter compatibility matrix kept current as runtimes change.</td>
80
80
  </tr>
81
81
  <tr>
82
82
  <td><strong>Permissions policy</strong><br>What an agent is allowed to execute, against which surfaces, with which evidence required.</td>
@@ -0,0 +1,123 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ThumbGate for Codex in the Enterprise β€” Governance for OpenAI Codex (Dell-Distributed or Self-Hosted)</title>
7
+ <script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
8
+ <meta name="description" content="OpenAI and Dell are distributing Codex into the enterprise. Codex in production needs a governance layer β€” capture every agent decision, promote repeat failures to PreToolUse gates, ship the audit trail procurement requires.">
9
+ <meta property="og:title" content="ThumbGate for Codex in the Enterprise">
10
+ <meta property="og:description" content="Dell-distributed or self-hosted, Codex agents repeat the same mistakes. ThumbGate is the governance layer underneath β€” capture, promote, audit.">
11
+ <meta property="og:type" content="article">
12
+ <meta property="og:image" content="https://thumbgate-production.up.railway.app/og.png">
13
+ <link rel="canonical" href="https://thumbgate-production.up.railway.app/codex-enterprise">
14
+ <script type="application/ld+json">
15
+ {
16
+ "@context": "https://schema.org",
17
+ "@type": "TechArticle",
18
+ "headline": "ThumbGate for Codex in the Enterprise",
19
+ "description": "Dell-distributed or self-hosted, Codex agents in production need a governance layer. ThumbGate captures every agent decision, promotes repeat failures to PreToolUse gates, and ships the audit trail enterprise procurement requires.",
20
+ "datePublished": "2026-05-20",
21
+ "dateModified": "2026-05-20",
22
+ "author": { "@type": "Person", "name": "Igor Ganapolsky", "url": "https://github.com/IgorGanapolsky" },
23
+ "publisher": { "@type": "Organization", "name": "ThumbGate", "url": "https://thumbgate-production.up.railway.app" },
24
+ "about": [
25
+ { "@type": "Thing", "name": "OpenAI Codex" },
26
+ { "@type": "Thing", "name": "Dell Codex Enterprise" },
27
+ { "@type": "Thing", "name": "Agent Governance" },
28
+ { "@type": "Thing", "name": "PreToolUse Gates" }
29
+ ]
30
+ }
31
+ </script>
32
+ <style>
33
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
34
+ :root { --bg:#0a0a0b; --card:#161618; --border:#222225; --text:#e8e8ec; --muted:#8b8b94; --cyan:#22d3ee; }
35
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--text); line-height: 1.7; }
36
+ .container { max-width: 860px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
37
+ nav { padding: 1rem 2rem; border-bottom: 1px solid var(--border); display:flex; gap:1.5rem; flex-wrap:wrap; }
38
+ nav a { color: var(--muted); text-decoration:none; font-size:0.9rem; }
39
+ nav .brand { color: var(--text); font-weight:700; }
40
+ .pill { display:inline-block; font-size:0.75rem; letter-spacing:0.08em; text-transform:uppercase; color:var(--cyan); background:rgba(34,211,238,0.08); border:1px solid rgba(34,211,238,0.2); padding:4px 12px; border-radius:100px; margin-top:1.5rem; font-weight:600; }
41
+ h1 { font-size:2.2rem; line-height:1.15; margin:1rem 0 1rem; }
42
+ h2 { font-size:1.45rem; margin:2.2rem 0 1rem; color:var(--cyan); }
43
+ h3 { margin:0.6rem 0; font-size:1rem; }
44
+ p, li { margin-bottom:0.75rem; }
45
+ ul, ol { padding-left:1.25rem; }
46
+ .card { background: var(--card); border:1px solid var(--border); border-radius:12px; padding:1.25rem; margin:1rem 0; }
47
+ .grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:1rem; margin:1rem 0; }
48
+ .grid .card h3 { color:var(--cyan); }
49
+ .cta { display:inline-block; background:var(--cyan); color:#000; padding:0.8rem 1.2rem; border-radius:8px; text-decoration:none; font-weight:700; }
50
+ .secondary { color:var(--cyan); text-decoration:underline; margin-left:1rem; }
51
+ .quote { border-left:3px solid var(--cyan); padding:0.75rem 1rem; margin:1rem 0; color:var(--muted); font-style:italic; }
52
+ code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; background:#0f0f11; border:1px solid var(--border); border-radius:6px; padding:0.15rem 0.4rem; font-size:0.9rem; }
53
+ pre { padding:0.85rem 1rem; overflow-x:auto; }
54
+ .footer-links { margin-top:2.5rem; padding-top:1.25rem; border-top:1px solid var(--border); color:var(--muted); font-size:0.9rem; }
55
+ .footer-links a { color:var(--cyan); text-decoration:none; }
56
+ </style>
57
+ </head>
58
+ <body>
59
+ <nav>
60
+ <a href="/" class="brand">ThumbGate</a>
61
+ <a href="/guide">Guide</a>
62
+ <a href="/agent-manager">Agent Manager</a>
63
+ <a href="/codex-plugin">Codex plugin</a>
64
+ <a href="/dashboard">Dashboard demo</a>
65
+ <a href="https://github.com/IgorGanapolsky/ThumbGate" target="_blank" rel="noopener">GitHub</a>
66
+ </nav>
67
+ <div class="container">
68
+ <span class="pill">Codex in the Enterprise</span>
69
+ <h1>Codex in production needs a governance layer. Dell-distributed or self-hosted, agents repeat the same mistakes.</h1>
70
+ <p>OpenAI and Dell <a href="https://openai.com/index/dell-codex-enterprise-partnership/" target="_blank" rel="noopener" style="color:var(--cyan)">just announced</a> a partnership to distribute Codex into the enterprise β€” Dell PCs, Dell servers, and Dell's enterprise sales motion become a delivery channel for OpenAI's coding agent. Codex's addressable market jumps from individual developer install to org-wide procurement. The governance gap jumps with it: every enterprise that turns Codex on now needs a runtime layer that captures what the agent did, blocks the repeat failures, and produces the audit trail their security review will ask for.</p>
71
+ <p>ThumbGate already ships a <a href="/codex-plugin">Codex plugin</a>. The free CLI is real, MIT-licensed, and the gates work locally without a hosted account. This page is what that plugin maps to once Codex is no longer one developer's experiment but a procurement line item.</p>
72
+
73
+ <h2>What the governance layer ships</h2>
74
+ <div class="grid">
75
+ <div class="card">
76
+ <h3>Capture every agent decision as it happens</h3>
77
+ <p>The Thariq pattern β€” running implementation notes that record decisions, assumptions (marked VERIFIED or UNVERIFIED), tradeoffs, and corrections β€” productionized as a Codex hook. Every multi-step task gets a structured journal you can review async without re-reading the entire transcript.</p>
78
+ </div>
79
+ <div class="card">
80
+ <h3>Promote repeat failures to PreToolUse gates</h3>
81
+ <p>When the same agent mistake shows up twice, ThumbGate distills it into a prevention rule and blocks the next attempt at the tool-call boundary β€” with the rule that fired in the agent's reasoning trace, so Codex chooses a safer plan instead of being told to "be more careful."</p>
82
+ </div>
83
+ <div class="card">
84
+ <h3>Audit trail enterprise procurement requires</h3>
85
+ <p>Per-tool-call evidence, per-rule provenance, exportable for SOC 2 / ISO 27001 / EU AI Act review. The hosted dashboard rolls this up across repos so the Agent Manager role has one surface instead of N developer machines.</p>
86
+ </div>
87
+ </div>
88
+
89
+ <h2>Why this matters now</h2>
90
+ <p>The Dell distribution channel changes who buys Codex. The individual-developer install is opt-in; the enterprise procurement install is policy-driven. The teams approving the Codex line item will ask three questions ThumbGate is built to answer:</p>
91
+ <ol>
92
+ <li><strong>What did the agent do?</strong> β€” capture, with evidence, on every tool call.</li>
93
+ <li><strong>What did we stop it from doing?</strong> β€” PreToolUse gates with the rule that fired and why.</li>
94
+ <li><strong>How do you keep this current as Codex updates?</strong> β€” adapter matrix that's CI-checked against upstream.</li>
95
+ </ol>
96
+ <div class="quote">"Dell-distributed Codex into the enterprise is the moment governance moves from optional to procurement-required. The runtime that captures, blocks, and audits is the line item underneath the line item."</div>
97
+
98
+ <h2>Install</h2>
99
+ <p>One repo, one command:</p>
100
+ <pre><code>npx thumbgate init --agent codex</code></pre>
101
+ <p>This wires the Codex hook, sets up the local lesson DB, and gives you the capture/promote/block loop without a hosted account. If you want the standalone Codex plugin as a self-contained zip β€” for offline distribution to Dell-managed machines or for security review β€” grab it from <a href="https://github.com/IgorGanapolsky/ThumbGate/releases" target="_blank" rel="noopener" style="color:var(--cyan)">GitHub releases</a> (look for <code>codex-plugin-*.zip</code>).</p>
102
+
103
+ <div class="card">
104
+ <p><strong>The free CLI is real. The paid tier is the hosted dashboard, the org-wide rule library, and the operator the Agent Manager doesn't have to be themselves.</strong></p>
105
+ <p>
106
+ <a href="/#workflow-sprint-intake?utm_source=website&amp;utm_medium=codex_enterprise_page&amp;utm_campaign=codex_enterprise_sprint&amp;cta_id=codex_enterprise_sprint_intake&amp;cta_placement=codex_enterprise_page" class="cta">Start the Workflow Hardening Sprint</a>
107
+ <a href="/checkout/pro?utm_source=website&amp;utm_medium=codex_enterprise_page&amp;utm_campaign=pro_upgrade&amp;cta_id=codex_enterprise_pro_checkout&amp;cta_placement=codex_enterprise_page&amp;plan_id=pro" class="secondary">Or start Pro at $19/mo β†’</a>
108
+ </p>
109
+ </div>
110
+
111
+ <h2>Related reading</h2>
112
+ <ul>
113
+ <li><a href="/agent-manager">ThumbGate for the Agent Manager</a> β€” the role inside the enterprise that owns Codex rollout policy.</li>
114
+ <li><a href="/codex-plugin">Codex plugin overview</a> β€” the standalone plugin surface this page rides on top of.</li>
115
+ <li><a href="/compare">Compare</a> β€” how governance compares to orchestration suites under the same Codex install.</li>
116
+ </ul>
117
+
118
+ <div class="footer-links">
119
+ Built for teams who turned on Codex and discovered "tell the model to be more careful" doesn't scale. See also <a href="/agent-manager">/agent-manager</a> for the role-level framing.
120
+ </div>
121
+ </div>
122
+ </body>
123
+ </html>
@@ -247,9 +247,9 @@
247
247
 
248
248
  <!-- STATS -->
249
249
  <div class="stats-grid" id="statsGrid">
250
- <a class="stat-card" data-card-action="all" onclick="selectCard(this,'all')" href="/lessons" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view all feedback β†’ Lessons page"><div class="stat-label">Total Feedback</div><div class="stat-value cyan" id="statTotal">β€”</div></a>
251
- <a class="stat-card" data-card-action="up" onclick="selectCard(this,'up')" href="/lessons?signal=positive" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view positive feedback β†’ Lessons page"><div class="stat-label">πŸ‘ Positive</div><div class="stat-value green" id="statPositive">β€”</div></a>
252
- <a class="stat-card" data-card-action="down" onclick="selectCard(this,'down')" href="/lessons?signal=negative" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view negative feedback β†’ Lessons page"><div class="stat-label">πŸ‘Ž Negative</div><div class="stat-value red" id="statNegative">β€”</div></a>
250
+ <a class="stat-card" data-card-action="all" onclick="selectCard(this,'all')" href="/lessons?signal=all" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view all feedback β†’ Lessons page"><div class="stat-label">Total Feedback</div><div class="stat-value cyan" id="statTotal">β€”</div></a>
251
+ <a class="stat-card" data-card-action="up" onclick="selectCard(this,'up')" href="/lessons?signal=up" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view positive feedback β†’ Lessons page"><div class="stat-label">πŸ‘ Positive</div><div class="stat-value green" id="statPositive">β€”</div></a>
252
+ <a class="stat-card" data-card-action="down" onclick="selectCard(this,'down')" href="/lessons?signal=down" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view negative feedback β†’ Lessons page"><div class="stat-label">πŸ‘Ž Negative</div><div class="stat-value red" id="statNegative">β€”</div></a>
253
253
  <a class="stat-card" data-card-action="gates" onclick="selectCard(this,'gates');return false;" href="#" style="cursor:pointer;text-decoration:none;color:inherit;display:block;" title="Click to view active checks"><div class="stat-label">Active Gates</div><div class="stat-value cyan" id="statGates">β€”</div></a>
254
254
  </div>
255
255
 
@@ -750,10 +750,23 @@ function setSource(el, source) {
750
750
  function switchTab(name) {
751
751
  document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
752
752
  document.querySelectorAll('.tab-content').forEach(function(c) { c.classList.remove('active'); });
753
- var tabEl = document.querySelector('[onclick*="' + name + '"]');
753
+ // Scope the header lookup to .tab β€” the prior selector
754
+ // [onclick*="<name>"] also matched the stat-cards (which carry onclick
755
+ // attributes like selectCard(this,'gates')), and a stat-card appears
756
+ // before the tab header in DOM order, so for 'gates' the wrong element
757
+ // (the card) got the .active class and the tab header stayed dormant.
758
+ var tabEl = document.querySelector('.tab[onclick*="' + name + '"]');
754
759
  var contentEl = document.getElementById('tab-' + name);
755
760
  if (tabEl) tabEl.classList.add('active');
756
- if (contentEl) contentEl.classList.add('active');
761
+ if (contentEl) {
762
+ contentEl.classList.add('active');
763
+ // Stat-card clicks fire switchTab from above the fold; without this scroll
764
+ // the user sees "nothing happen" because the just-activated content sits
765
+ // below the viewport. Same class of bug as the /lessons tile fix in #2268.
766
+ try {
767
+ contentEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
768
+ } catch (_e) { /* older browsers without smooth-scroll: no-op */ }
769
+ }
757
770
  // Sync URL hash so deep-links stay shareable without scroll jump
758
771
  try {
759
772
  if (('#' + name) !== window.location.hash) {
package/public/index.html CHANGED
@@ -19,7 +19,7 @@ __GOOGLE_SITE_VERIFICATION_META__
19
19
  <meta property="og:image" content="https://thumbgate-production.up.railway.app/og.png">
20
20
  <meta name="twitter:card" content="summary_large_image">
21
21
  <meta name="twitter:image" content="https://thumbgate-production.up.railway.app/og.png">
22
- <meta name="thumbgate-version" content="1.21.2">
22
+ <meta name="thumbgate-version" content="1.22.0">
23
23
  <meta name="keywords" content="ThumbGate, thumbgate, AI agent orchestration, AI experience orchestration, agent enforcement layer, save LLM tokens, reduce Claude API cost, reduce OpenAI cost, AI agent token savings, prevent LLM retries, prevent hallucination retries, stop AI token waste, pre-action checks, agent governance, Claude Code, Cursor, Codex, Gemini, Amp, Cline, OpenCode, workflow hardening, context engineering, AI authenticity, brand authenticity AI">
24
24
  <link rel="apple-touch-icon" href="/apple-touch-icon.png">
25
25
 
@@ -928,8 +928,8 @@ __GA_BOOTSTRAP__
928
928
  <div class="card-arrow">Open the Codex install page β†’</div>
929
929
  </a>
930
930
  <a class="compat-card" href="/guides/cursor-prevent-repeated-mistakes" rel="noopener">
931
- <h3>🎯 Cursor plugin</h3>
932
- <p>Drop the ThumbGate MCP config into <code>.cursor/mcp.json</code> and Cursor gets the same pre-action checks as Claude Code and Codex. Ships with bundled rules, commands, hooks, and agents.</p>
931
+ <h3>🎯 Cursor plugin <span style="font-size:12px;font-weight:500;color:var(--text-muted);">(Marketplace review pending)</span></h3>
932
+ <p>Drop the ThumbGate MCP config into <code>.cursor/mcp.json</code> and Cursor gets the same pre-action checks as Claude Code and Codex. Ships with bundled rules, commands, hooks, and agents. The runtime install works today via <code>npx thumbgate init --agent cursor</code>; the official Cursor Marketplace listing was submitted 2026-05-19 and is awaiting Cursor's manual review.</p>
933
933
  <div class="card-arrow">Read the Cursor guide β†’</div>
934
934
  </a>
935
935
  <a class="compat-card" href="/guide" rel="noopener">
@@ -1408,7 +1408,7 @@ __GA_BOOTSTRAP__
1408
1408
  </div>
1409
1409
  <div class="faq-item">
1410
1410
  <div class="faq-q" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="handleFaqKeydown(event)">What AI agents and editors does this work with?</div>
1411
- <div class="faq-a">ThumbGate works with Claude Code, Cursor, Codex, Gemini CLI, Amp, Cline, OpenCode, and any other MCP-compatible agent. Cursor ships with a plugin bundle in this repo. Codex now ships both a standalone plugin bundle and a repo-local app plugin profile, and the published download is linked directly from this page. VS Code works when you run an MCP-compatible agent inside it, but this repo does not ship a standalone VS Code extension today.</div>
1411
+ <div class="faq-a">ThumbGate works with Claude Code, Cursor, Codex, Gemini CLI, Amp, Cline, OpenCode, and any other MCP-compatible agent. The Cursor plugin bundle ships in this repo and installs today via <code>npx thumbgate init --agent cursor</code>; the Cursor Marketplace listing was submitted 2026-05-19 and is still pending Cursor's manual review, so it is not yet discoverable from the in-app Marketplace. Codex now ships both a standalone plugin bundle and a repo-local app plugin profile, and the published download is linked directly from this page. VS Code works when you run an MCP-compatible agent inside it, but this repo does not ship a standalone VS Code extension today.</div>
1412
1412
  </div>
1413
1413
  <div class="faq-item">
1414
1414
  <div class="faq-q" role="button" tabindex="0" aria-expanded="false" onclick="toggleFaq(this)" onkeydown="handleFaqKeydown(event)">Do I have to chat inside the ThumbGate GPT for enforcement?</div>
@@ -1492,7 +1492,7 @@ __GA_BOOTSTRAP__
1492
1492
  <a href="https://www.linkedin.com/in/igorganapolsky" target="_blank" rel="noopener">LinkedIn</a>
1493
1493
  <a href="/blog">Blog</a>
1494
1494
  </div>
1495
- <span class="footer-copy">Β© 2026 ThumbGate Β· MIT License Β· npm v1.21.2</span>
1495
+ <span class="footer-copy">Β© 2026 ThumbGate Β· MIT License Β· npm v1.22.0</span>
1496
1496
  </div>
1497
1497
  </footer>
1498
1498
 
@@ -1672,6 +1672,13 @@ function copyInstall(el) {
1672
1672
  toggleFaq(event.currentTarget);
1673
1673
  }
1674
1674
 
1675
+ // Hoist FAQ handlers to window scope so the inline `onclick="toggleFaq(this)"`
1676
+ // attributes on every FAQ question can resolve them. Without this, every FAQ
1677
+ // click silently throws ReferenceError β€” all 13 FAQ items on the landing
1678
+ // page are dead. Discovered by comprehensive E2E coverage in this PR.
1679
+ window.toggleFaq = toggleFaq;
1680
+ window.handleFaqKeydown = handleFaqKeydown;
1681
+
1675
1682
  /* CTA clicks */
1676
1683
  trackClick('.btn-pro', 'checkout_start', { tier: 'pro', price: 19, billing: 'monthly' });
1677
1684
  trackClick('.btn-gpt-page:not(.btn-install-hero)', 'chatgpt_gpt_click', { tier: 'free', source: 'homepage_gpt' });
@@ -449,6 +449,18 @@ function switchTab(name) {
449
449
  // Highlight the corresponding stat card
450
450
  var cardMap = { rules: 0, timeline: 2, insights: 3 };
451
451
  highlightCard(cardMap[name] !== undefined ? cardMap[name] : -1);
452
+ // Scroll the active tab content into view so the click has a visible effect.
453
+ // Without this, clicking a stat card or tab header when its content is below
454
+ // the fold appears to do nothing β€” the tab changes silently and the user
455
+ // never sees the new content. The tab-strip itself stays visible so the
456
+ // selected-state is still observable.
457
+ if (content && typeof content.scrollIntoView === 'function') {
458
+ try {
459
+ content.scrollIntoView({ behavior: 'smooth', block: 'start' });
460
+ } catch (_err) {
461
+ content.scrollIntoView();
462
+ }
463
+ }
452
464
  if (typeof plausible === 'function') plausible('lessons_tab', { props: { tab: name } });
453
465
  }
454
466
 
@@ -922,6 +934,28 @@ async function loadLive() {
922
934
  }
923
935
 
924
936
  loadLive().then(function() {
937
+ // Handle ?signal= query param from dashboard stat-card navigation.
938
+ // Vocabulary: 'up' | 'down' | 'all' (canonical). Also accepts the legacy
939
+ // 'positive' | 'negative' aliases the dashboard once emitted.
940
+ var qsSignal = new URLSearchParams(window.location.search).get('signal');
941
+ if (qsSignal) {
942
+ var signalMap = { positive: 'up', negative: 'down', up: 'up', down: 'down', all: 'all' };
943
+ var mapped = signalMap[qsSignal];
944
+ if (mapped) {
945
+ switchTab('timeline');
946
+ filterTimeline(mapped, null);
947
+ var filterBtns = document.querySelectorAll('#tab-timeline .filter-btn');
948
+ filterBtns.forEach(function(b) {
949
+ var label = b.textContent.trim().toLowerCase();
950
+ var match = (mapped === 'all' && label === 'all') ||
951
+ (mapped === 'up' && (label.indexOf('πŸ‘') !== -1 || label.indexOf('positive') !== -1 || label === 'up')) ||
952
+ (mapped === 'down' && (label.indexOf('πŸ‘Ž') !== -1 || label.indexOf('negative') !== -1 || label === 'down'));
953
+ b.classList.toggle('active', match);
954
+ });
955
+ return;
956
+ }
957
+ }
958
+
925
959
  // Default: highlight Active Rules card on page load
926
960
  highlightCard(0);
927
961
 
@@ -25,7 +25,7 @@
25
25
  "alternateName": "thumbgate",
26
26
  "applicationCategory": "DeveloperApplication",
27
27
  "operatingSystem": "Cross-platform, Node.js >=18.18.0",
28
- "softwareVersion": "1.21.2",
28
+ "softwareVersion": "1.22.0",
29
29
  "url": "https://thumbgate-production.up.railway.app/numbers",
30
30
  "dateModified": "2026-05-07",
31
31
  "creator": {
@@ -202,7 +202,7 @@
202
202
  <main class="container">
203
203
  <h1>The Numbers</h1>
204
204
  <p class="subtitle">Generated first-party operational snapshot from the ThumbGate runtime. This is not customer traction, install volume, revenue, or proof that a configured gate has fired.</p>
205
- <div class="freshness">Updated: 2026-05-07 Β· Version 1.21.2</div>
205
+ <div class="freshness">Updated: 2026-05-07 Β· Version 1.22.0</div>
206
206
  <div class="truth-note"><strong>Read this first:</strong> configured checks are inventory. Recorded blocks and warnings are usage evidence. This snapshot currently reports 0 recorded hard-block event(s) and 0 recorded warning event(s).</div>
207
207
 
208
208
  <h2>Gate enforcement</h2>
@@ -27,6 +27,7 @@ const {
27
27
  statuslineCommand,
28
28
  userPromptHookCommand,
29
29
  } = require('./hook-runtime');
30
+ const { installShim } = require('./install-shim');
30
31
 
31
32
  function getHome() {
32
33
  return process.env.HOME || process.env.USERPROFILE || '';
@@ -338,6 +339,19 @@ function wireClaudeHooks(options) {
338
339
  options.projectSettingsPath || claudeProjectSettingsPath(options.projectDir);
339
340
  const dryRun = options.dryRun || false;
340
341
  const projectDir = options.projectDir || process.cwd();
342
+
343
+ // --- Install stable shim before resolving hook commands ---
344
+ // The shim at ~/.thumbgate/bin/thumbgate-hook always resolves @latest,
345
+ // so hooks never go stale across version bumps (Volta-style pattern).
346
+ // Skip in source-checkout mode β€” developers use direct node commands.
347
+ if (!dryRun && !require('./mcp-config').isSourceCheckout(path.join(__dirname, '..'))) {
348
+ try {
349
+ installShim();
350
+ } catch {
351
+ // Non-fatal: fall back to version-pinned commands
352
+ }
353
+ }
354
+
341
355
  const desiredStatusLine = statuslineCommand();
342
356
 
343
357
  // --- Step 0: clean up stale hooks from BOTH settings locations ---
@@ -16,6 +16,13 @@ function normalizeNullableText(value) {
16
16
  }
17
17
 
18
18
  function resolveBuildMetadata({ env = process.env, filePath } = {}) {
19
+ // Precedence: immutable JSON file (baked into Docker image at build time, so it
20
+ // ALWAYS matches the deployed code) wins over runtime env vars. Env vars are
21
+ // mutable Railway/host config that can drift β€” they shadowed the freshly-stamped
22
+ // SHA in prod on 2026-05-20 and made /health lie about the deployed commit.
23
+ // Fall back to env vars only when the file is missing or its values are null,
24
+ // and require an explicit SHA env var (not just a stray GENERATED_AT) before
25
+ // trusting the env branch.
19
26
  const resolvedPath =
20
27
  normalizeNullableText(filePath) ||
21
28
  normalizeNullableText(env.THUMBGATE_BUILD_METADATA_PATH) ||
@@ -23,28 +30,40 @@ function resolveBuildMetadata({ env = process.env, filePath } = {}) {
23
30
  const envBuildSha = normalizeNullableText(env[BUILD_SHA_ENV_KEY]);
24
31
  const envGeneratedAt = normalizeNullableText(env[BUILD_GENERATED_AT_ENV_KEY]);
25
32
 
26
- if (envBuildSha || envGeneratedAt) {
27
- return {
28
- path: resolvedPath,
29
- buildSha: envBuildSha,
30
- generatedAt: envGeneratedAt,
31
- };
32
- }
33
-
33
+ let fileBuildSha = null;
34
+ let fileGeneratedAt = null;
34
35
  try {
35
36
  const parsed = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
37
+ fileBuildSha = normalizeNullableText(parsed.buildSha);
38
+ fileGeneratedAt = normalizeNullableText(parsed.generatedAt);
39
+ } catch {
40
+ // file missing or unreadable β€” fall through to env branch
41
+ }
42
+
43
+ if (fileBuildSha) {
36
44
  return {
37
45
  path: resolvedPath,
38
- buildSha: normalizeNullableText(parsed.buildSha),
39
- generatedAt: normalizeNullableText(parsed.generatedAt),
46
+ buildSha: fileBuildSha,
47
+ generatedAt: fileGeneratedAt || envGeneratedAt,
40
48
  };
41
- } catch {
49
+ }
50
+
51
+ // No SHA in the file β€” fall back to env only if an explicit SHA is set.
52
+ // (Previously a bare GENERATED_AT with no SHA could short-circuit and return
53
+ // { buildSha: null }, losing both signals; now we require the SHA.)
54
+ if (envBuildSha) {
42
55
  return {
43
56
  path: resolvedPath,
44
- buildSha: null,
45
- generatedAt: null,
57
+ buildSha: envBuildSha,
58
+ generatedAt: envGeneratedAt,
46
59
  };
47
60
  }
61
+
62
+ return {
63
+ path: resolvedPath,
64
+ buildSha: null,
65
+ generatedAt: fileGeneratedAt || envGeneratedAt,
66
+ };
48
67
  }
49
68
 
50
69
  function writeBuildMetadataFile({ sha, outputPath, generatedAt = new Date().toISOString() }) {
@@ -9,6 +9,7 @@ const { sequencePathFor } = require('./risk-scorer');
9
9
 
10
10
  const PROJECT_ROOT = path.join(__dirname, '..');
11
11
  const MANUAL_GATES_PATH = path.join(PROJECT_ROOT, 'config', 'gates', 'default.json');
12
+ const STATS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'gate-stats.json');
12
13
 
13
14
  function loadGatesFile(filePath) {
14
15
  if (!fs.existsSync(filePath)) return [];
@@ -65,6 +66,15 @@ function calculateStats() {
65
66
  // sample can produce a misleading 0.0% floor.
66
67
  const bayesErrorRate = tryComputeBayesErrorRate();
67
68
 
69
+ // Calibration: per-gate assessment of whether block actions are well-supported
70
+ // by negative feedback, or potentially over/under-blocking without confirmation.
71
+ const calibration = computeCalibration(allGates);
72
+
73
+ // First-time fix rate: 1 - (recurringBlocks / totalBlocksAndWarns)
74
+ // Measures how often a single gate fire resolves the issue vs the agent retrying.
75
+ // Returns null when there is no recorded block/warn data yet.
76
+ const firstTimeFixRate = computeFirstTimeFixRate();
77
+
68
78
  return {
69
79
  totalGates: allGates.length,
70
80
  manualGates: manualGates.length,
@@ -77,10 +87,70 @@ function calculateStats() {
77
87
  lastPromotion,
78
88
  estimatedHoursSaved,
79
89
  bayesErrorRate,
90
+ calibration,
91
+ firstTimeFixRate,
80
92
  gates: allGates,
81
93
  };
82
94
  }
83
95
 
96
+ /**
97
+ * Assess each gate's calibration by comparing block occurrences to confirmed
98
+ * negative feedback counts. A gate with many blocks but no confirming negative
99
+ * feedback may be over-blocking; one with matching feedback is well-calibrated.
100
+ *
101
+ * @param {Array} gates - Combined array of manual + auto-promoted gate objects
102
+ * @returns {Array<{gateId: string, occurrences: number, action: string, calibrationNote: string}>}
103
+ */
104
+ function computeCalibration(gates) {
105
+ const calibration = [];
106
+ for (const gate of gates || []) {
107
+ if (!gate || !gate.id) continue;
108
+ const occurrences = Number(gate.occurrences || 0);
109
+ const action = gate.action || 'unknown';
110
+ // Only annotate gates with recorded occurrence data
111
+ if (occurrences === 0) continue;
112
+
113
+ if (action === 'block') {
114
+ const confirmedNegative = Number(gate.confirmedNegative || gate.negativeCount || 0);
115
+ let calibrationNote;
116
+ if (occurrences > 10 && confirmedNegative === 0) {
117
+ calibrationNote = `over-blocking (${occurrences} blocks, 0 confirmed)`;
118
+ } else if (confirmedNegative > 0) {
119
+ calibrationNote = `well-calibrated (${occurrences} blocks, ${confirmedNegative} confirmed)`;
120
+ } else {
121
+ // Low occurrence count with no feedback β€” not enough data yet
122
+ calibrationNote = `insufficient data (${occurrences} blocks, 0 confirmed)`;
123
+ }
124
+ calibration.push({ gateId: gate.id, occurrences, action, calibrationNote });
125
+ }
126
+ }
127
+ return calibration;
128
+ }
129
+
130
+ /**
131
+ * Compute the first-time fix rate from the persisted gate-stats.json file.
132
+ *
133
+ * firstTimeFixRate = 1 - (recurringBlocks / totalBlocksAndWarns)
134
+ *
135
+ * Returns null when there are no recorded block/warn events yet.
136
+ * Returns a number in [0, 1] otherwise, where 1.0 means every gate fire
137
+ * was a first-time occurrence and 0.0 means every gate fired at least twice.
138
+ */
139
+ function computeFirstTimeFixRate() {
140
+ try {
141
+ if (!fs.existsSync(STATS_PATH)) return null;
142
+ const raw = fs.readFileSync(STATS_PATH, 'utf8');
143
+ const data = JSON.parse(raw);
144
+ const totalBlocksAndWarns = (data.blocked || 0) + (data.warned || 0);
145
+ if (totalBlocksAndWarns === 0) return null;
146
+ const recurring = data.recurringBlocks || 0;
147
+ const rate = 1 - (recurring / totalBlocksAndWarns);
148
+ return Math.max(0, Math.min(1, rate));
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+
84
154
  function tryComputeBayesErrorRate() {
85
155
  try {
86
156
  const seqPath = sequencePathFor();
@@ -142,6 +212,13 @@ function formatStats(stats) {
142
212
  lines.push(` Last promotion: ${formatLastPromotion(stats.lastPromotion)}`);
143
213
  lines.push(` Estimated time saved: ~${stats.estimatedHoursSaved} hours`);
144
214
  lines.push(` Bayes error rate: ${formatBayesErrorRate(stats.bayesErrorRate)}`);
215
+ lines.push(` First-time fix rate: ${formatFirstTimeFixRate(stats.firstTimeFixRate)}`);
216
+ if (Array.isArray(stats.calibration) && stats.calibration.length > 0) {
217
+ lines.push('Calibration:');
218
+ for (const entry of stats.calibration) {
219
+ lines.push(` - ${entry.gateId}: ${entry.calibrationNote}`);
220
+ }
221
+ }
145
222
  return lines.join('\n');
146
223
  }
147
224
 
@@ -153,6 +230,14 @@ function formatBayesErrorRate(rate) {
153
230
  return `${pct}% β€” high irreducible error; the feature set can't discriminate`;
154
231
  }
155
232
 
233
+ function formatFirstTimeFixRate(rate) {
234
+ if (rate === null || rate === undefined) return 'n/a (no blocks or warns recorded yet)';
235
+ const pct = (rate * 100).toFixed(1);
236
+ if (rate >= 0.95) return `${pct}% β€” agents fix issues on first block (excellent)`;
237
+ if (rate >= 0.80) return `${pct}% β€” most blocks resolved first time (good)`;
238
+ return `${pct}% β€” recurring blocks detected; agents may be ignoring gate feedback`;
239
+ }
240
+
156
241
  if (require.main === module) {
157
242
  try {
158
243
  const stats = calculateStats();
@@ -168,7 +253,11 @@ module.exports = {
168
253
  formatStats,
169
254
  formatLastPromotion,
170
255
  formatBayesErrorRate,
256
+ formatFirstTimeFixRate,
257
+ computeFirstTimeFixRate,
171
258
  loadGatesFile,
172
259
  tryComputeBayesErrorRate,
260
+ computeCalibration,
173
261
  MANUAL_GATES_PATH,
262
+ STATS_PATH,
174
263
  };
@@ -439,6 +439,20 @@ function recordStat(gateId, action, gate) {
439
439
  else if (action === 'warn') stats.byGate[gateId].warned += 1;
440
440
  else if (action === 'approve') stats.byGate[gateId].pendingApproval = (stats.byGate[gateId].pendingApproval || 0) + 1;
441
441
  else if (action === 'log') stats.byGate[gateId].logged = (stats.byGate[gateId].logged || 0) + 1;
442
+
443
+ // Track per-gate recurrence within a session for first-time fix rate
444
+ if (action === 'block' || action === 'warn') {
445
+ if (!stats.sessionFiredGates) stats.sessionFiredGates = {};
446
+ const sessionKey = `session_${Math.floor(Date.now() / SESSION_ACTION_TTL_MS)}`;
447
+ if (!stats.sessionFiredGates[sessionKey]) stats.sessionFiredGates[sessionKey] = {};
448
+ if (stats.sessionFiredGates[sessionKey][gateId]) {
449
+ // Same gate fired again in this session β€” it's a recurring block
450
+ stats.recurringBlocks = (stats.recurringBlocks || 0) + 1;
451
+ } else {
452
+ stats.sessionFiredGates[sessionKey][gateId] = true;
453
+ }
454
+ }
455
+
442
456
  saveStats(stats);
443
457
  // Track lesson freshness when an auto-promoted gate fires
444
458
  if (gate && gate.sourceLessonId) {
@@ -7,6 +7,7 @@ const {
7
7
  publishedCliAvailable,
8
8
  } = require('./mcp-config');
9
9
  const { publishedCliShellCommand } = require('./published-cli');
10
+ const { shimInstalled, shimPath } = require('./install-shim');
10
11
 
11
12
  const PKG_ROOT = path.join(__dirname, '..');
12
13
  const featureSupportCache = new Map();
@@ -34,13 +35,18 @@ function publishedHookCommandsAvailable(version) {
34
35
  }
35
36
 
36
37
  function resolveCliCommand(subcommand) {
38
+ // Source checkout: always use direct node command for development
39
+ if (isSourceCheckout(PKG_ROOT)) {
40
+ return `node ${shellQuote(path.join(PKG_ROOT, 'bin', 'cli.js'))} ${subcommand}`;
41
+ }
42
+ // Prefer stable shim β€” always resolves @latest, survives version bumps
43
+ if (shimInstalled()) {
44
+ return `${shellQuote(shimPath())} ${subcommand}`;
45
+ }
37
46
  const version = packageVersion();
38
47
  if (publishedHookCommandsAvailable(version)) {
39
48
  return publishedCliShellCommand(version, [subcommand]);
40
49
  }
41
- if (isSourceCheckout(PKG_ROOT)) {
42
- return `node ${shellQuote(path.join(PKG_ROOT, 'bin', 'cli.js'))} ${subcommand}`;
43
- }
44
50
  return publishedCliShellCommand(version, [subcommand]);
45
51
  }
46
52
 
@@ -397,6 +397,24 @@ const TOOLS = [
397
397
  },
398
398
  },
399
399
  }),
400
+ readOnlyTool({
401
+ name: 'suggest_fix',
402
+ description: 'Suggest corrective actions for a described failure by searching the lesson DB and prevention rules. Returns up to 3 ranked suggestions with their source. Call this when something goes wrong and you need guidance on what to do next.',
403
+ inputSchema: {
404
+ type: 'object',
405
+ required: ['context'],
406
+ properties: {
407
+ context: {
408
+ type: 'string',
409
+ description: 'Description of what went wrong or what the agent is trying to fix.',
410
+ },
411
+ limit: {
412
+ type: 'number',
413
+ description: 'Maximum number of suggestions to return (default 3, max 5).',
414
+ },
415
+ },
416
+ },
417
+ }),
400
418
  readOnlyTool({
401
419
  name: 'infer_lesson_from_history',
402
420
  description: 'Perform autonomous inference on chat history to identify why a failure occurred and what rule should be recorded.',
@@ -1178,10 +1178,15 @@ function chooseDecision({ riskScore, integrity, memoryGuard, learnedPolicy, blas
1178
1178
  if (lowRiskHandoff) {
1179
1179
  return 'allow';
1180
1180
  }
1181
+ // Background customer-system actions checkpoint (warn), never hard-deny.
1182
+ // The checkpoint IS the mitigation β€” blocking outright prevents legitimate work.
1183
+ if (backgroundAgent && customerSystemAction) {
1184
+ return 'warn';
1185
+ }
1181
1186
  if (destructiveBypass || learnedHardStop || repeatedHighBlast || (hasOperationalBlockers && riskScore >= 0.72) || riskScore >= 0.86) {
1182
1187
  return 'deny';
1183
1188
  }
1184
- if (economicAction || (backgroundAgent && customerSystemAction) || (backgroundAgent && riskScore >= 0.3)) {
1189
+ if (economicAction || (backgroundAgent && riskScore >= 0.3)) {
1185
1190
  return 'warn';
1186
1191
  }
1187
1192
  if ((workflowControl && workflowControl.mode === 'warn') || (costControl && costControl.mode === 'warn') || riskScore >= 0.45 || (learnedWarning && riskScore >= 0.3) || (learnedRecall && riskScore >= 0.34)) {
package/src/api/server.js CHANGED
@@ -2504,6 +2504,7 @@ function renderSitemapXml(runtimeConfig) {
2504
2504
  { path: '/agent-manager', changefreq: 'weekly', priority: '0.9' },
2505
2505
  { path: '/llm-context.md', changefreq: 'weekly', priority: '0.8' },
2506
2506
  { path: '/codex-plugin', changefreq: 'weekly', priority: '0.75' },
2507
+ { path: '/codex-enterprise', changefreq: 'weekly', priority: '0.85' },
2507
2508
  ...THUMBGATE_SEO_SITEMAP_ENTRIES,
2508
2509
  ];
2509
2510
  return [
@@ -4520,6 +4521,30 @@ async function addContext(){
4520
4521
  return;
4521
4522
  }
4522
4523
 
4524
+ if (isGetLikeRequest && (pathname === '/codex-enterprise' || pathname === '/codex-enterprise.html')) {
4525
+ // Landing page riding the 2026-05-20 OpenAIΓ—Dell Codex Enterprise
4526
+ // partnership announcement. Dell-distributed Codex expands the TAM
4527
+ // for ThumbGate's governance layer β€” capture every agent decision,
4528
+ // promote repeat failures to PreToolUse gates, ship the audit trail
4529
+ // procurement requires. Routed through servePublicMarketingPage so
4530
+ // arrivals via the partnership news cycle capture UTM attribution
4531
+ // and landing_page_view telemetry with pageType: 'codex_enterprise'.
4532
+ try {
4533
+ servePublicMarketingPage({
4534
+ req,
4535
+ res,
4536
+ parsed,
4537
+ hostedConfig,
4538
+ isHeadRequest,
4539
+ renderHtml: () => fs.readFileSync(path.join(PUBLIC_DIR, 'codex-enterprise.html'), 'utf-8'),
4540
+ extraTelemetry: { pageType: 'codex_enterprise' },
4541
+ });
4542
+ } catch {
4543
+ sendJson(res, 404, { error: 'Codex Enterprise page not found' });
4544
+ }
4545
+ return;
4546
+ }
4547
+
4523
4548
  if (isGetLikeRequest && pathname === '/learn/learn.css') {
4524
4549
  try {
4525
4550
  const cssPath = path.join(LEARN_DIR, 'learn.css');
@@ -7729,6 +7754,7 @@ function startServer({ port, host } = {}) {
7729
7754
  const listenPort = Number(port ?? process.env.PORT ?? 8787);
7730
7755
  const listenHost = String(host ?? process.env.HOST ?? '0.0.0.0').trim() || '0.0.0.0';
7731
7756
  const server = createApiServer();
7757
+ registerGracefulShutdown(server);
7732
7758
  return new Promise((resolve) => {
7733
7759
  server.listen(listenPort, listenHost, () => {
7734
7760
  const address = server.address();
@@ -7744,6 +7770,39 @@ function startServer({ port, host } = {}) {
7744
7770
  });
7745
7771
  }
7746
7772
 
7773
+ // Railway / Cloud Run / Kubernetes deploy rotations send SIGTERM to swap
7774
+ // containers. Without a handler, Node exits immediately β€” in-flight requests
7775
+ // are killed and the orchestrator may mark the container as "crashed" (instead
7776
+ // of "gracefully stopped"), wasting its restart-policy budget on a healthy
7777
+ // shutdown. Drain HTTP, give a deadline, then force-exit if anything hangs.
7778
+ function registerGracefulShutdown(server, { gracePeriodMs = 25_000 } = {}) {
7779
+ if (server[GRACEFUL_SHUTDOWN_KEY]) return;
7780
+ server[GRACEFUL_SHUTDOWN_KEY] = true;
7781
+ let shuttingDown = false;
7782
+ const stop = (signal) => {
7783
+ if (shuttingDown) return;
7784
+ shuttingDown = true;
7785
+ console.log(`[shutdown] ${signal} received β€” draining connections (deadline ${gracePeriodMs}ms)`);
7786
+ const forceTimer = setTimeout(() => {
7787
+ console.error('[shutdown] grace period elapsed β€” forcing exit');
7788
+ process.exit(1);
7789
+ }, gracePeriodMs);
7790
+ if (typeof forceTimer.unref === 'function') forceTimer.unref();
7791
+ server.close((err) => {
7792
+ if (err) {
7793
+ console.error('[shutdown] server.close error:', err.message);
7794
+ process.exit(1);
7795
+ }
7796
+ console.log('[shutdown] drained cleanly');
7797
+ process.exit(0);
7798
+ });
7799
+ };
7800
+ process.on('SIGTERM', () => stop('SIGTERM'));
7801
+ process.on('SIGINT', () => stop('SIGINT'));
7802
+ }
7803
+
7804
+ const GRACEFUL_SHUTDOWN_KEY = Symbol.for('thumbgate.gracefulShutdownRegistered');
7805
+
7747
7806
  module.exports = {
7748
7807
  createApiServer,
7749
7808
  startServer,