thumbgate 1.26.1 → 1.26.3

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.26.1",
3
+ "version": "1.26.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.26.1",
17
+ "version": "1.26.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.26.1",
4
+ "version": "1.26.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.26.1",
3
+ "version": "1.26.0",
4
4
  "description": "ThumbGate — 👍👎 feedback that teaches your AI agent. Thumbs down a mistake, it never happens again.",
5
5
  "homepage": "https://thumbgate.ai",
6
6
  "transport": "stdio",
@@ -2,13 +2,13 @@
2
2
  "mcpServers": {
3
3
  "thumbgate": {
4
4
  "command": "npx",
5
- "args": ["--yes", "--package", "thumbgate@1.26.1", "thumbgate", "serve"]
5
+ "args": ["--yes", "--package", "thumbgate@1.26.0", "thumbgate", "serve"]
6
6
  }
7
7
  },
8
8
  "hooks": {
9
9
  "preToolUse": {
10
10
  "command": "npx",
11
- "args": ["--yes", "--package", "thumbgate@1.26.1", "thumbgate", "gate-check"]
11
+ "args": ["--yes", "--package", "thumbgate@1.26.0", "thumbgate", "gate-check"]
12
12
  }
13
13
  }
14
14
  }
@@ -77,6 +77,7 @@ const {
77
77
  recordActionAttempt,
78
78
  isRepeatAttempt,
79
79
  } = require('../../scripts/noop-detect');
80
+ const { recordAuditEvent } = require('../../scripts/audit-trail');
80
81
  const {
81
82
  recordReceipt,
82
83
  getReceiptForAction,
@@ -230,7 +231,7 @@ const {
230
231
  finalizeSession: finalizeFeedbackSession,
231
232
  } = require('../../scripts/feedback-session');
232
233
 
233
- const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.26.1' };
234
+ const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.26.0' };
234
235
  const COMMERCE_CATEGORIES = [
235
236
  'product_recommendation',
236
237
  'brand_compliance',
@@ -7,7 +7,7 @@
7
7
  "npx",
8
8
  "--yes",
9
9
  "--package",
10
- "thumbgate@1.26.1",
10
+ "thumbgate@1.26.0",
11
11
  "thumbgate",
12
12
  "serve"
13
13
  ],
package/bin/cli.js CHANGED
@@ -239,6 +239,21 @@ function parseArgs(argv) {
239
239
  return args;
240
240
  }
241
241
 
242
+ function parseTtlMs(value, fallbackMs = 5 * 60 * 1000) {
243
+ if (value === undefined || value === null || value === true || value === '') return fallbackMs;
244
+ const raw = String(value).trim().toLowerCase();
245
+ const match = raw.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/);
246
+ if (!match) return fallbackMs;
247
+ const amount = Number(match[1]);
248
+ if (!Number.isFinite(amount) || amount <= 0) return fallbackMs;
249
+ const unit = match[2] || 'ms';
250
+ const factor = unit === 'h' ? 60 * 60 * 1000
251
+ : unit === 'm' ? 60 * 1000
252
+ : unit === 's' ? 1000
253
+ : 1;
254
+ return Math.round(amount * factor);
255
+ }
256
+
242
257
  function readStdinText() {
243
258
  try {
244
259
  return fs.readFileSync(0, 'utf8');
@@ -1003,10 +1018,21 @@ function capture() {
1003
1018
  }
1004
1019
 
1005
1020
  let signal = (args.feedback || '').toLowerCase();
1021
+ let consumedSignalArgs = 0;
1006
1022
  if (!signal && positionalArgs[0]) {
1007
- const firstPos = positionalArgs[0].toLowerCase();
1008
- if (['up', 'down', 'thumbsup', 'thumbsdown', 'thumbs_up', 'thumbs_down', 'positive', 'negative'].some(v => firstPos.includes(v))) {
1009
- signal = firstPos;
1023
+ const { detectFeedbackSignal } = require(path.join(PKG_ROOT, 'scripts', 'feedback-quality'));
1024
+ const oneWord = positionalArgs[0];
1025
+ const twoWords = positionalArgs.slice(0, 2).join(' ');
1026
+ const detected = detectFeedbackSignal(twoWords) || detectFeedbackSignal(oneWord);
1027
+ if (detected) {
1028
+ signal = detected.signal;
1029
+ consumedSignalArgs = detectFeedbackSignal(twoWords) ? Math.min(2, positionalArgs.length) : 1;
1030
+ } else {
1031
+ const firstPos = positionalArgs[0].toLowerCase();
1032
+ if (['up', 'down', 'thumbsup', 'thumbsdown', 'thumbs_up', 'thumbs_down', 'positive', 'negative'].some(v => firstPos.includes(v))) {
1033
+ signal = firstPos;
1034
+ consumedSignalArgs = 1;
1035
+ }
1010
1036
  }
1011
1037
  }
1012
1038
 
@@ -1026,19 +1052,25 @@ function capture() {
1026
1052
  }
1027
1053
 
1028
1054
  let context = args.context || '';
1029
- if (!context && positionalArgs[1]) {
1055
+ if (!context && consumedSignalArgs > 0) {
1056
+ context = positionalArgs.slice(consumedSignalArgs).join(' ');
1057
+ } else if (!context && positionalArgs[1]) {
1030
1058
  context = positionalArgs[1];
1031
1059
  }
1032
1060
 
1033
1061
  let whatWentWrong = args['what-went-wrong'];
1034
- if (!whatWentWrong && positionalArgs[2]) {
1062
+ if (!whatWentWrong && consumedSignalArgs > 0 && positionalArgs.length > consumedSignalArgs + 1) {
1063
+ whatWentWrong = positionalArgs.slice(consumedSignalArgs + 1).join(' ');
1064
+ } else if (!whatWentWrong && positionalArgs[2]) {
1035
1065
  whatWentWrong = positionalArgs[2];
1036
1066
  } else if (!whatWentWrong && normalized === 'down' && context) {
1037
1067
  whatWentWrong = context;
1038
1068
  }
1039
1069
 
1040
1070
  let whatToChange = args['what-to-change'];
1041
- if (!whatToChange && positionalArgs[3]) {
1071
+ if (!whatToChange && consumedSignalArgs > 0 && positionalArgs.length > consumedSignalArgs + 2) {
1072
+ whatToChange = positionalArgs.slice(consumedSignalArgs + 2).join(' ');
1073
+ } else if (!whatToChange && positionalArgs[3]) {
1042
1074
  whatToChange = positionalArgs[3];
1043
1075
  } else if (!whatToChange && normalized === 'down' && context) {
1044
1076
  whatToChange = `avoid: ${context}`;
@@ -2375,6 +2407,43 @@ function optimize() {
2375
2407
  doOptimize();
2376
2408
  }
2377
2409
 
2410
+ function cleanup() {
2411
+ console.log('Cleaning up ThumbGate processes...');
2412
+ try {
2413
+ const { execSync } = require('child_process');
2414
+ // Kill all 'thumbgate serve' and 'thumbgate dashboard' processes except this one
2415
+ const pids = execSync("ps aux | grep 'thumbgate' | grep -v 'grep' | awk '{print $2}'", { encoding: 'utf8' })
2416
+ .split('\n')
2417
+ .filter(Boolean)
2418
+ .map(Number)
2419
+ .filter(pid => pid !== process.pid);
2420
+
2421
+ if (pids.length > 0) {
2422
+ console.log(`Killing ${pids.length} process(es): ${pids.join(', ')}`);
2423
+ pids.forEach(pid => {
2424
+ try { process.kill(pid, 'SIGTERM'); } catch (_) {}
2425
+ });
2426
+ // Give them a moment to die
2427
+ execSync('sleep 1');
2428
+ } else {
2429
+ console.log('No other ThumbGate processes found.');
2430
+ }
2431
+
2432
+ // Check port 3456 specifically
2433
+ try {
2434
+ const portPid = execSync("lsof -ti :3456", { encoding: 'utf8' }).trim();
2435
+ if (portPid) {
2436
+ console.log(`Killing process ${portPid} holding port 3456`);
2437
+ try { process.kill(Number(portPid), 'SIGKILL'); } catch (_) {}
2438
+ }
2439
+ } catch (_) { /* port already free */ }
2440
+
2441
+ console.log('✅ Cleanup complete. Run "npx thumbgate pro" to restart the dashboard.');
2442
+ } catch (err) {
2443
+ console.error(`Cleanup failed: ${err.message}`);
2444
+ }
2445
+ }
2446
+
2378
2447
  function serve() {
2379
2448
  try {
2380
2449
  const { repairCodexHooks } = require(path.join(PKG_ROOT, 'scripts', 'codex-self-heal'));
@@ -2411,11 +2480,21 @@ function install() {
2411
2480
  }
2412
2481
 
2413
2482
  async function gateCheck() {
2414
- const payload = readStdinText();
2415
- const input = payload ? JSON.parse(payload) : {};
2416
- const gatesEngine = require(path.join(PKG_ROOT, 'scripts', 'gates-engine'));
2417
- const output = await gatesEngine.runAsync(input);
2418
- process.stdout.write(output + '\n');
2483
+ try {
2484
+ const payload = readStdinText();
2485
+ const input = payload ? JSON.parse(payload) : {};
2486
+ const gatesEngine = require(path.join(PKG_ROOT, 'scripts', 'gates-engine'));
2487
+ const output = await gatesEngine.runAsync(input);
2488
+ process.stdout.write(output + '\n');
2489
+ } catch (err) {
2490
+ process.stderr.write(`gate-check error: ${err.message}\n`);
2491
+ process.stdout.write(JSON.stringify({
2492
+ hookSpecificOutput: {
2493
+ hookEventName: 'PreToolUse',
2494
+ additionalContext: `[ThumbGate Error] ${err.message}`,
2495
+ }
2496
+ }) + '\n');
2497
+ }
2419
2498
  }
2420
2499
 
2421
2500
  function cacheUpdate() {
@@ -2442,9 +2521,14 @@ function statuslineRender() {
2442
2521
 
2443
2522
  function hookAutoCapture() {
2444
2523
  syncActiveProjectContext();
2445
- const prompt = process.env.CLAUDE_USER_PROMPT || process.env.THUMBGATE_USER_PROMPT || readStdinText().trim();
2524
+ const prompt = process.env.CLAUDE_USER_PROMPT
2525
+ || process.env.THUMBGATE_USER_PROMPT
2526
+ || process.env.CODEX_USER_PROMPT
2527
+ || process.env.USER_PROMPT
2528
+ || readStdinText().trim();
2446
2529
  const { evaluatePromptGuard } = require(path.join(PKG_ROOT, 'scripts', 'prompt-guard'));
2447
2530
  const { processInlineFeedback, formatCliOutput } = require(path.join(PKG_ROOT, 'scripts', 'cli-feedback'));
2531
+ const { detectFeedbackSignal } = require(path.join(PKG_ROOT, 'scripts', 'feedback-quality'));
2448
2532
  const { loadOptionalModule } = require(path.join(PKG_ROOT, 'scripts', 'private-core-boundary'));
2449
2533
  const { recordConversationEntry, readRecentConversationWindow } = loadOptionalModule(
2450
2534
  path.join(PKG_ROOT, 'scripts', 'feedback-history-distiller'),
@@ -2466,14 +2550,12 @@ function hookAutoCapture() {
2466
2550
  return;
2467
2551
  }
2468
2552
 
2469
- const lower = prompt.toLowerCase();
2470
- const isUp = /(thumbs?\s*up|that worked|looks good|nice work|perfect|good job)/i.test(lower);
2471
- const isDown = /(thumbs?\s*down|that failed|that was wrong|fix this)/i.test(lower);
2472
- if (!isUp && !isDown) {
2553
+ const detected = detectFeedbackSignal(prompt);
2554
+ if (!detected) {
2473
2555
  return;
2474
2556
  }
2475
2557
 
2476
- const signal = isDown ? 'down' : 'up';
2558
+ const signal = detected.signal;
2477
2559
  const conversationWindow = readRecentConversationWindow({ limit: 8 });
2478
2560
  const result = processInlineFeedback({
2479
2561
  signal,
@@ -2673,6 +2755,30 @@ function startApi() {
2673
2755
  }
2674
2756
  }
2675
2757
 
2758
+ function breakGlass() {
2759
+ const args = parseArgs(process.argv.slice(3));
2760
+ const positionalReason = process.argv.slice(3).find((arg) => !arg.startsWith('--'));
2761
+ const reason = String(args.reason || positionalReason || '').trim();
2762
+ if (!reason) {
2763
+ console.error('Usage: npx thumbgate break-glass --reason "why this recovery is needed" [--ttl=5m] [--json]');
2764
+ process.exit(1);
2765
+ }
2766
+
2767
+ const ttlMs = parseTtlMs(args.ttl, 5 * 60 * 1000);
2768
+ const { breakGlassEmergency } = require(path.join(PKG_ROOT, 'scripts', 'gates-engine'));
2769
+ const result = breakGlassEmergency({ reason, ttlMs });
2770
+ if (args.json) {
2771
+ console.log(JSON.stringify(result, null, 2));
2772
+ return;
2773
+ }
2774
+
2775
+ console.log('ThumbGate break-glass active.');
2776
+ console.log(` Reason : ${result.reason}`);
2777
+ console.log(` Expires : ${result.expiresAt}`);
2778
+ console.log(' Unlocked : hook settings edits, pr_create_allowed, pr_threads_checked');
2779
+ console.log(' Still gated: local-only scope, force-push, protected branch push, unsafe chmod, broad rm -rf');
2780
+ }
2781
+
2676
2782
  function help() {
2677
2783
  const v = pkgVersion();
2678
2784
  const helpArgs = process.argv.slice(3);
@@ -2695,6 +2801,7 @@ function help() {
2695
2801
  console.log(' explore Interactive TUI for lessons, gates, stats');
2696
2802
  console.log(' dashboard Open the local ThumbGate dashboard');
2697
2803
  console.log(' doctor Audit runtime isolation + bootstrap context');
2804
+ console.log(' break-glass --reason="..." Short TTL recovery if gates over-fire');
2698
2805
  console.log(' brain [--write] Build the agent-readable context brain (lessons + rules + gates)');
2699
2806
  console.log(' pro ThumbGate Pro (dashboard, exports, sync)');
2700
2807
  console.log(' subscribe <email> Get the 5-min setup guide + weekly tips by email');
@@ -2747,6 +2854,7 @@ function help() {
2747
2854
  console.log(' default: machine-wide (~/.claude/settings.json — shared dashboard)');
2748
2855
  console.log(' --project: per-repo (<cwd>/.claude/settings.json — isolated dashboard)');
2749
2856
  console.log(' --no-hooks: MCP only, skip hook wiring');
2857
+ console.log(' break-glass Short TTL recovery if gates over-fire');
2750
2858
  console.log(' cfo Hosted billing summary (local fallback JSON)');
2751
2859
  console.log(' billing:setup Generate operator key + print Railway setup instructions');
2752
2860
  console.log(' repair-github-marketplace Repair legacy GitHub Marketplace amount mappings');
@@ -2842,6 +2950,11 @@ const SUBCOMMAND_HELP = {
2842
2950
  lessons: 'Usage: npx thumbgate lessons [--query="..."] [--limit=N]\n\nSearch the lesson database (Pro feature).',
2843
2951
  search: 'Usage: npx thumbgate search <query>\n\nSearch ThumbGate knowledge base (Pro feature).',
2844
2952
  'gate-check': 'Usage: npx thumbgate gate-check\n\nPreToolUse hook interface: reads tool call JSON from stdin, outputs gate verdict.',
2953
+ 'break-glass': 'Usage: npx thumbgate break-glass --reason="why" [--ttl=5m] [--json]\n\nShort-lived recovery path for over-firing gates. Allows hook settings edits and satisfies PR-create/thread-check gates without disabling core destructive-action protections.',
2954
+ serve: 'Usage: npx thumbgate serve\n\nStart the MCP stdio server. This is for agent runtimes, not the local HTTP dashboard.',
2955
+ mcp: 'Usage: npx thumbgate mcp\n\nAlias for `thumbgate serve`.',
2956
+ dashboard: 'Usage: npx thumbgate dashboard [--window=today|7d|30d]\n\nPrint the operational dashboard summary. Use `npx thumbgate start-api` for the local HTTP dashboard on :3456.',
2957
+ 'start-api': 'Usage: npx thumbgate start-api\n\nStart the local ThumbGate HTTP API/dashboard. Defaults to PORT=8787; use PORT=3456 for statusline localhost links.',
2845
2958
  'export-dpo': 'Usage: npx thumbgate export-dpo [--format=jsonl|csv]\n\nExport feedback as DPO training pairs (Pro feature).',
2846
2959
  status: 'Usage: npx thumbgate status\n\nShow ThumbGate system health and active configuration.',
2847
2960
  watch: 'Usage: npx thumbgate watch\n\nWatch for feedback changes and auto-regenerate prevention rules.',
@@ -3004,6 +3117,9 @@ switch (COMMAND) {
3004
3117
  case 'mcp':
3005
3118
  serve();
3006
3119
  break;
3120
+ case 'cleanup':
3121
+ cleanup();
3122
+ break;
3007
3123
  case 'gate-check':
3008
3124
  gateCheck().catch((err) => {
3009
3125
  console.error(err && err.message ? err.message : err);
@@ -3058,7 +3174,7 @@ switch (COMMAND) {
3058
3174
  }
3059
3175
  case 'brain': {
3060
3176
  const brainArgs = parseArgs(process.argv.slice(3));
3061
- process.exit(cmdBrain(brainArgs));
3177
+ process.exitCode = cmdBrain(brainArgs);
3062
3178
  break;
3063
3179
  }
3064
3180
  case 'billing:setup':
@@ -3371,6 +3487,10 @@ switch (COMMAND) {
3371
3487
  case 'status':
3372
3488
  status();
3373
3489
  break;
3490
+ case 'break-glass':
3491
+ case 'breakglass':
3492
+ breakGlass();
3493
+ break;
3374
3494
  case 'funnel':
3375
3495
  funnel();
3376
3496
  break;
@@ -23,7 +23,7 @@
23
23
  "id": "task-scope-required",
24
24
  "layer": "Decisions",
25
25
  "toolNames": ["Bash"],
26
- "pattern": "^(git\\s+(add|commit|push)|gh\\s+pr\\s+(create|merge)|gh\\s+release\\s+create|git\\s+tag\\b|npm\\s+publish|yarn\\s+publish|pnpm\\s+publish)",
26
+ "pattern": "^(git\\s+(add|commit|push)|gh\\s+pr\\s+(create|merge)|gh\\s+api\\b(?=.*(?:/pulls\\b|repos/[^\\s]+/[^\\s]+/pulls\\b))(?=.*(?:-f\\b|--field\\b|-F\\b|--raw-field\\b|--method\\s+POST\\b|-X\\s+POST\\b))|gh\\s+release\\s+create|git\\s+tag\\b|npm\\s+publish|yarn\\s+publish|pnpm\\s+publish)",
27
27
  "requireTaskScope": true,
28
28
  "action": "block",
29
29
  "message": "Git write, PR, release, and publish operations require an explicit task scope.",
@@ -72,6 +72,15 @@
72
72
  "message": "PR creation requires explicit 'pr_create_allowed' satisfaction with evidence of user permission.",
73
73
  "severity": "high"
74
74
  },
75
+ {
76
+ "id": "gh-api-pr-create-restricted",
77
+ "layer": "Identity",
78
+ "pattern": "gh\\s+api\\b(?=.*(?:/pulls\\b|repos/[^\\s]+/[^\\s]+/pulls\\b))(?=.*(?:-f\\b|--field\\b|-F\\b|--raw-field\\b|--method\\s+POST\\b|-X\\s+POST\\b))",
79
+ "action": "block",
80
+ "unless": "pr_create_allowed",
81
+ "message": "GitHub API PR creation requires explicit 'pr_create_allowed' satisfaction with evidence of user permission. Use the same approval path as gh pr create.",
82
+ "severity": "high"
83
+ },
75
84
  {
76
85
  "id": "gh-pr-merge-restricted",
77
86
  "layer": "Identity",
@@ -85,7 +94,7 @@
85
94
  "id": "branch-governance-required",
86
95
  "layer": "Decisions",
87
96
  "toolNames": ["Bash"],
88
- "pattern": "^(gh\\s+pr\\s+(create|merge)|gh\\s+release\\s+create|git\\s+tag\\b|npm\\s+publish|yarn\\s+publish|pnpm\\s+publish)",
97
+ "pattern": "^(gh\\s+pr\\s+(create|merge)|gh\\s+api\\b(?=.*(?:/pulls\\b|repos/[^\\s]+/[^\\s]+/pulls\\b))(?=.*(?:-f\\b|--field\\b|-F\\b|--raw-field\\b|--method\\s+POST\\b|-X\\s+POST\\b))|gh\\s+release\\s+create|git\\s+tag\\b|npm\\s+publish|yarn\\s+publish|pnpm\\s+publish)",
89
98
  "requireBranchGovernance": true,
90
99
  "action": "block",
91
100
  "message": "PR, release, and publish actions require explicit branch governance.",
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "thumbgate",
3
- "version": "1.26.1",
3
+ "version": "1.26.3",
4
4
  "description": "ThumbGate self-improving agent governance: thumbs-up/down turns every mistake into a prevention rule and blocks repeat patterns. 36 pre-action checks, budget enforcement, and self-protection for Claude Code, Cursor, Codex, Gemini CLI, and Amp.",
5
5
  "homepage": "https://thumbgate.ai",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/IgorGanapolsky/ThumbGate.git"
8
+ "url": "git+https://github.com/IgorGanapolsky/ThumbGate.git"
9
9
  },
10
10
  "bugs": {
11
11
  "url": "https://github.com/IgorGanapolsky/ThumbGate/issues"
@@ -135,7 +135,6 @@
135
135
  "scripts/natural-language-harness.js",
136
136
  "scripts/noop-detect.js",
137
137
  "scripts/obsidian-export.js",
138
- "scripts/operational-dashboard.js",
139
138
  "scripts/operational-integrity.js",
140
139
  "scripts/oss-pr-opportunity-scout.js",
141
140
  "scripts/otel-declarative-config.js",
package/public/index.html CHANGED
@@ -20,7 +20,7 @@ __GOOGLE_SITE_VERIFICATION_META__
20
20
  <meta property="og:image" content="https://thumbgate.ai/og.png">
21
21
  <meta name="twitter:card" content="summary_large_image">
22
22
  <meta name="twitter:image" content="https://thumbgate.ai/og.png">
23
- <meta name="thumbgate-version" content="1.26.1">
23
+ <meta name="thumbgate-version" content="1.26.0">
24
24
  <meta name="keywords" content="ThumbGate, thumbgate, AI agent orchestration, AI experience orchestration, agentic development cycle, AC/DC framework, Guide Generate Verify Solve, 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">
25
25
  <link rel="canonical" href="__APP_ORIGIN__/">
26
26
  <link rel="alternate" type="text/markdown" title="ThumbGate LLM context" href="__APP_ORIGIN__/llm-context.md">
@@ -1594,7 +1594,7 @@ __GA_BOOTSTRAP__
1594
1594
  <a href="https://www.linkedin.com/in/igorganapolsky" target="_blank" rel="noopener">LinkedIn</a>
1595
1595
  <a href="/blog">Blog</a>
1596
1596
  </div>
1597
- <span class="footer-copy">© 2026 ThumbGate · MIT License · npm v1.26.1</span>
1597
+ <span class="footer-copy">© 2026 ThumbGate · MIT License · npm v1.26.0</span>
1598
1598
  </div>
1599
1599
  </footer>
1600
1600
 
@@ -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.26.1",
28
+ "softwareVersion": "1.26.0",
29
29
  "url": "https://thumbgate.ai/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.26.1</div>
205
+ <div class="freshness">Updated: 2026-05-07 · Version 1.26.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>
@@ -94,9 +94,26 @@ function detectRuntimeIsolation() {
94
94
  };
95
95
  }
96
96
 
97
- function collectBootstrapFiles(projectRoot = PROJECT_ROOT) {
97
+ function findProjectRoot(startDir = process.cwd()) {
98
+ try {
99
+ let curr = path.resolve(startDir);
100
+ while (true) {
101
+ const indicators = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md', '.mcp.json', '.git'];
102
+ if (indicators.some((f) => fs.existsSync(path.join(curr, f)))) {
103
+ return curr;
104
+ }
105
+ const parent = path.dirname(curr);
106
+ if (parent === curr) break;
107
+ curr = parent;
108
+ }
109
+ } catch (_) { /* fallback to startDir */ }
110
+ return startDir;
111
+ }
112
+
113
+ function collectBootstrapFiles(projectRoot) {
114
+ const effectiveRoot = projectRoot || findProjectRoot();
98
115
  const files = BOOTSTRAP_FILES.map((file) => {
99
- const absolutePath = path.join(projectRoot, file.path);
116
+ const absolutePath = path.join(effectiveRoot, file.path);
100
117
  return {
101
118
  id: file.id,
102
119
  path: file.path,
@@ -118,7 +135,7 @@ function collectBootstrapFiles(projectRoot = PROJECT_ROOT) {
118
135
  missingRequired,
119
136
  recommendation: missingRequired.length === 0
120
137
  ? 'Bootstrap context is present.'
121
- : `Add missing bootstrap files: ${missingRequired.join(', ')}`,
138
+ : `Add missing bootstrap files to project root (${effectiveRoot}): ${missingRequired.join(', ')}`,
122
139
  };
123
140
  }
124
141
 
@@ -88,11 +88,13 @@ function formatCliOutput(result) {
88
88
  if (result.feedbackResult && result.feedbackResult.accepted !== false) {
89
89
  lines.push(`${isDown ? R : G}${BD}${isDown ? '👎 Thumbs down recorded' : '👍 Thumbs up recorded'}${RST}`);
90
90
  const feedbackId = (result.feedbackResult.feedbackEvent && result.feedbackResult.feedbackEvent.id) || result.feedbackResult.id;
91
+ const memoryId = (result.feedbackResult.memoryRecord && result.feedbackResult.memoryRecord.id) || result.feedbackResult.memoryId;
91
92
  if (feedbackId) {
92
- lines.push(`${D} ID: ${feedbackId}${RST}`);
93
+ lines.push(`${D} Feedback ID: ${feedbackId}${RST}`);
94
+ if (memoryId) lines.push(`${D} Memory ID : ${memoryId}${RST}`);
93
95
  // Echo feedback ID to stderr so it's visible directly in the terminal,
94
96
  // not hidden behind Claude Code's "ctrl+o to expand" MCP call collapse.
95
- process.stderr.write(`✅ Feedback captured (${feedbackId})\n`);
97
+ process.stderr.write(`✅ Feedback captured (${feedbackId}${memoryId ? `, ${memoryId}` : ''})\n`);
96
98
  }
97
99
  } else {
98
100
  lines.push(`${R}Feedback not accepted: ${(result.feedbackResult && result.feedbackResult.reason) || 'unknown'}${RST}`);
@@ -68,8 +68,6 @@ function buildCaptureReceipt({ signal, feedbackId, memoryId, actionType } = {})
68
68
  '',
69
69
  ` Solo Pro : ${PRO_PRICE_LABEL} for hosted sync, search, dashboard, and exports`,
70
70
  ` Upgrade : ${trackedProUrl('cli_capture_receipt', actionType || normalizedSignal.toLowerCase())}`,
71
- ` Team path : ${TEAM_PRICE_LABEL}; start with one repeated workflow failure`,
72
- ' https://thumbgate.ai/#workflow-sprint-intake',
73
71
  '',
74
72
  ];
75
73
  return lines.join('\n');
@@ -102,7 +100,6 @@ function buildStatsReceipt(stats = {}) {
102
100
  lines.push(' Show the buyer : npx thumbgate cost');
103
101
  lines.push(' Pro sync value : keep these lessons/rules visible across laptops, CI, containers, and agent runtimes');
104
102
  lines.push(` Solo Pro : ${trackedProUrl('cli_stats_receipt', 'proof_seen')}`);
105
- lines.push(' Team workflow : https://thumbgate.ai/#workflow-sprint-intake');
106
103
  lines.push('');
107
104
  return lines.join('\n');
108
105
  }
@@ -62,6 +62,92 @@ function normalizeFeedbackText(value) {
62
62
  .trim();
63
63
  }
64
64
 
65
+ function editDistance(a, b) {
66
+ const left = String(a || '');
67
+ const right = String(b || '');
68
+ const dp = Array.from({ length: left.length + 1 }, () => Array(right.length + 1).fill(0));
69
+ for (let i = 0; i <= left.length; i++) dp[i][0] = i;
70
+ for (let j = 0; j <= right.length; j++) dp[0][j] = j;
71
+ for (let i = 1; i <= left.length; i++) {
72
+ for (let j = 1; j <= right.length; j++) {
73
+ const cost = left[i - 1] === right[j - 1] ? 0 : 1;
74
+ dp[i][j] = Math.min(
75
+ dp[i - 1][j] + 1,
76
+ dp[i][j - 1] + 1,
77
+ dp[i - 1][j - 1] + cost,
78
+ );
79
+ }
80
+ }
81
+ return dp[left.length][right.length];
82
+ }
83
+
84
+ function isNearThumbToken(token) {
85
+ const value = String(token || '');
86
+ if (value.length < 4) return false;
87
+ return editDistance(value, 'thumb') <= 1 || editDistance(value, 'thumbs') <= 2;
88
+ }
89
+
90
+ function isNearUpToken(token) {
91
+ const value = String(token || '');
92
+ return value === 'up' || editDistance(value, 'up') <= 1;
93
+ }
94
+
95
+ function isNearDownToken(token) {
96
+ const value = String(token || '');
97
+ if (value.length < 2) return false;
98
+ return editDistance(value, 'down') <= 1;
99
+ }
100
+
101
+ function detectFeedbackSignal(value) {
102
+ const raw = String(value || '');
103
+ if (/[👎👎🏻👎🏼👎🏽👎🏾👎🏿]/u.test(raw)) return { signal: 'down', confidence: 'emoji', match: '👎' };
104
+ if (/[👍👍🏻👍🏼👍🏽👍🏾👍🏿]/u.test(raw)) return { signal: 'up', confidence: 'emoji', match: '👍' };
105
+
106
+ const normalized = normalizeFeedbackText(raw);
107
+ if (!normalized) return null;
108
+
109
+ const exactDown = [
110
+ /\bthumbs?\s*down\b/,
111
+ /\bthumbs?down\b/,
112
+ /\bthat failed\b/,
113
+ /\bit failed\b/,
114
+ /\bthat was wrong\b/,
115
+ /\bfix this\b/,
116
+ ];
117
+ if (exactDown.some((pattern) => pattern.test(normalized))) {
118
+ return { signal: 'down', confidence: 'exact', match: normalized };
119
+ }
120
+
121
+ const exactUp = [
122
+ /\bthumbs?\s*up\b/,
123
+ /\bthumbs?up\b/,
124
+ /\bthat worked\b/,
125
+ /\bit worked\b/,
126
+ /\blooks good\b/,
127
+ /\bgood job\b/,
128
+ /\bgood work\b/,
129
+ /\bnice work\b/,
130
+ /\bperfect\b/,
131
+ /\blgtm\b/,
132
+ ];
133
+ if (exactUp.some((pattern) => pattern.test(normalized))) {
134
+ return { signal: 'up', confidence: 'exact', match: normalized };
135
+ }
136
+
137
+ const words = normalized.split(/\s+/).filter(Boolean);
138
+ for (let i = 0; i < words.length - 1; i++) {
139
+ if (!isNearThumbToken(words[i])) continue;
140
+ if (isNearDownToken(words[i + 1])) {
141
+ return { signal: 'down', confidence: 'fuzzy', match: `${words[i]} ${words[i + 1]}` };
142
+ }
143
+ if (isNearUpToken(words[i + 1])) {
144
+ return { signal: 'up', confidence: 'fuzzy', match: `${words[i]} ${words[i + 1]}` };
145
+ }
146
+ }
147
+
148
+ return null;
149
+ }
150
+
65
151
  function isGenericFeedbackText(value, signal) {
66
152
  const normalized = normalizeFeedbackText(value);
67
153
  if (!normalized) return false;
@@ -131,6 +217,7 @@ function buildClarificationMessage(params = {}) {
131
217
 
132
218
  module.exports = {
133
219
  GENERIC_PHRASE_RULES,
220
+ detectFeedbackSignal,
134
221
  normalizeFeedbackSignal,
135
222
  normalizeFeedbackText,
136
223
  isGenericFeedbackText,
@@ -109,9 +109,21 @@ const DEFAULT_PROTECTED_FILE_GLOBS = [
109
109
  const EDIT_LIKE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
110
110
  const HIGH_RISK_BASH_PATTERN = /\b(?:git\s+(?:add|commit|push)|gh\s+pr\s+(?:create|merge)|npm\s+publish|yarn\s+publish|pnpm\s+publish|rm\s+-rf)\b/i;
111
111
  const REMOTE_SIDE_EFFECT_BASH_PATTERN = /\b(?:git\s+push\b|gh\s+pr\s+(?:create|merge|close|reopen|ready|edit)\b|gh\s+release\s+(?:create|delete|edit|upload)\b|npm\s+publish\b|yarn\s+publish\b|pnpm\s+publish\b)\b/i;
112
+ const GH_API_PR_CREATE_PATTERN = /\bgh\s+api\b(?=.*(?:\/pulls\b|repos\/[^\s]+\/[^\s]+\/pulls\b))(?=.*(?:-f\b|--field\b|-F\b|--raw-field\b|--method\s+POST\b|-X\s+POST\b))/i;
112
113
  const BOOSTED_RISK_BLOCK_SCORE = 0.8;
113
114
  const BOOSTED_RISK_MIN_EXAMPLES = 3;
114
115
  const PR_THREAD_RESOLUTION_ACTION = 'pr_thread_resolution_verified_after_commit';
116
+ const KNOWLEDGE_ENTROPY_THRESHOLD = 0.7;
117
+ const KNOWLEDGE_CONFLICT_STRICT_BASH_PATTERN = /\b(?:git\s+push\b|gh\s+pr\s+merge\b|gh\s+release\s+(?:create|delete|edit|upload)\b|(?:npm|yarn|pnpm)\s+publish\b|rm\s+-rf\b|git\s+reset\s+--hard\b|git\s+clean\s+-f|railway\s+(?:deploy|up)\b|gcloud\s+(?:run\s+deploy|app\s+deploy)\b|firebase\s+deploy\b|vercel\s+--prod\b|kubectl\s+(?:apply|delete)\b|terraform\s+(?:apply|destroy)\b)\b/i;
118
+ const BREAK_GLASS_CONDITION = 'thumbgate_break_glass';
119
+ const BREAK_GLASS_SETTINGS_GLOBS = [
120
+ '.claude/settings.local.json',
121
+ '.claude/settings.json',
122
+ '**/.claude/settings.local.json',
123
+ '**/.claude/settings.json',
124
+ '.codex/config.toml',
125
+ '**/.codex/config.toml',
126
+ ];
115
127
 
116
128
  function isRuntimePlanGateEnabled() {
117
129
  return process.env.THUMBGATE_PLAN_GATE === '1' || process.env.THUMBGATE_PLAN_GATE === 'true';
@@ -360,6 +372,38 @@ function approveProtectedAction(input = {}) {
360
372
  return entry;
361
373
  }
362
374
 
375
+ function breakGlassEmergency(input = {}) {
376
+ const reason = String(input.reason || '').trim();
377
+ if (!reason) {
378
+ throw new Error('reason is required');
379
+ }
380
+
381
+ const ttlMs = Math.min(clampTtlMs(input.ttlMs, TTL_MS), TTL_MS);
382
+ const evidence = `BREAK GLASS: ${reason}`;
383
+ const gates = ['pr_create_allowed', 'pr_threads_checked', BREAK_GLASS_CONDITION];
384
+ const satisfied = {};
385
+ for (const gateId of gates) {
386
+ satisfied[gateId] = satisfyCondition(gateId, evidence);
387
+ }
388
+
389
+ const approval = approveProtectedAction({
390
+ pathGlobs: BREAK_GLASS_SETTINGS_GLOBS,
391
+ reason: evidence,
392
+ evidence,
393
+ ttlMs,
394
+ });
395
+
396
+ return {
397
+ ok: true,
398
+ reason,
399
+ ttlMs,
400
+ expiresAt: new Date(Date.now() + ttlMs).toISOString(),
401
+ satisfied,
402
+ approval,
403
+ settingsGlobs: BREAK_GLASS_SETTINGS_GLOBS.slice(),
404
+ };
405
+ }
406
+
363
407
  function setBranchGovernance(input = {}) {
364
408
  if (input && input.clear === true) {
365
409
  const state = loadGovernanceState();
@@ -693,7 +737,7 @@ function extractAffectedFiles(toolName, toolInput = {}) {
693
737
  }
694
738
  }
695
739
 
696
- if (/\bgit\s+push\b/i.test(command) || /\bgh\s+pr\s+(?:create|merge)\b/i.test(command)) {
740
+ if (/\bgit\s+push\b/i.test(command) || /\bgh\s+pr\s+(?:create|merge)\b/i.test(command) || GH_API_PR_CREATE_PATTERN.test(command)) {
697
741
  for (const filePath of getBranchDiffFiles(repoRoot)) {
698
742
  files.add(normalizePosix(filePath));
699
743
  }
@@ -712,6 +756,7 @@ function isHighRiskAction(toolName, toolInput = {}, affectedFiles = []) {
712
756
  const command = String(toolInput.command || '');
713
757
  // Original high-risk pattern (git writes, publishes, destructive ops)
714
758
  if (HIGH_RISK_BASH_PATTERN.test(command)) return true;
759
+ if (GH_API_PR_CREATE_PATTERN.test(command)) return true;
715
760
  // Broadened: any Bash command that modifies files or has side effects.
716
761
  // Excludes pure read/analysis commands (node --test, cat, ls, echo, etc.)
717
762
  // to avoid false positives on benign operations.
@@ -949,7 +994,8 @@ function getLocalOnlyScopeSources(governanceState = {}, constraints = {}) {
949
994
 
950
995
  function isRemoteSideEffectCommand(toolName, toolInput = {}) {
951
996
  if (toolName !== 'Bash') return false;
952
- return REMOTE_SIDE_EFFECT_BASH_PATTERN.test(String(toolInput.command || ''));
997
+ const command = String(toolInput.command || '');
998
+ return REMOTE_SIDE_EFFECT_BASH_PATTERN.test(command) || GH_API_PR_CREATE_PATTERN.test(command);
953
999
  }
954
1000
 
955
1001
  function evaluateLocalOnlyRemoteSideEffectGate(toolName, toolInput = {}, governanceState = {}, constraints = {}) {
@@ -1003,6 +1049,18 @@ function shouldEnforceTaskScope(gate, governanceState, toolName, toolInput = {},
1003
1049
  return isScopeEnforcedAction(toolName, toolInput, affectedFiles);
1004
1050
  }
1005
1051
 
1052
+ function isAgentHookSettingsFile(filePath) {
1053
+ return matchesAnyGlob(filePath, BREAK_GLASS_SETTINGS_GLOBS);
1054
+ }
1055
+
1056
+ function isBreakGlassSettingsBypass(gate, affectedFiles) {
1057
+ if (!gate || !['task-scope-edit-boundary', 'protected-file-approval-required'].includes(gate.id)) {
1058
+ return false;
1059
+ }
1060
+ if (!isConditionSatisfied(BREAK_GLASS_CONDITION)) return false;
1061
+ return Array.isArray(affectedFiles) && affectedFiles.length > 0 && affectedFiles.every(isAgentHookSettingsFile);
1062
+ }
1063
+
1006
1064
  function formatFileList(files, limit = 5) {
1007
1065
  const items = Array.isArray(files) ? files.filter(Boolean) : [];
1008
1066
  if (items.length === 0) return 'none';
@@ -1234,6 +1292,12 @@ function matchGate(gate, toolName, toolInput = {}) {
1234
1292
  try {
1235
1293
  const regex = new RegExp(gate.pattern);
1236
1294
  if (!regex.test(matchText)) return { matched: false, matchText, affectedFiles };
1295
+ if (gate.id === 'permission-change-approval' && isSafeLocalCredentialHardeningCommand(toolName, toolInput)) {
1296
+ return { matched: false, matchText, affectedFiles };
1297
+ }
1298
+ if (isBreakGlassSettingsBypass(gate, affectedFiles)) {
1299
+ return { matched: false, matchText, affectedFiles };
1300
+ }
1237
1301
  } catch {
1238
1302
  return { matched: false, matchText, affectedFiles };
1239
1303
  }
@@ -1251,6 +1315,9 @@ function matchGate(gate, toolName, toolInput = {}) {
1251
1315
 
1252
1316
  let taskScopeViolation = null;
1253
1317
  if (gate.requireTaskScope) {
1318
+ if (isBreakGlassSettingsBypass(gate, affectedFiles)) {
1319
+ return { matched: false, matchText, affectedFiles };
1320
+ }
1254
1321
  if (!shouldEnforceTaskScope(gate, governanceState, toolName, toolInput, affectedFiles)) {
1255
1322
  return { matched: false, matchText, affectedFiles };
1256
1323
  }
@@ -1260,6 +1327,9 @@ function matchGate(gate, toolName, toolInput = {}) {
1260
1327
 
1261
1328
  let protectedApprovalViolation = null;
1262
1329
  if (gate.requireProtectedApproval) {
1330
+ if (isBreakGlassSettingsBypass(gate, affectedFiles)) {
1331
+ return { matched: false, matchText, affectedFiles };
1332
+ }
1263
1333
  const protectedGlobs = sanitizeGlobList(
1264
1334
  Array.isArray(gate.protectedGlobs) && gate.protectedGlobs.length > 0
1265
1335
  ? gate.protectedGlobs
@@ -1299,6 +1369,27 @@ function matchesGate(gate, toolName, toolInput) {
1299
1369
  return matchGate(gate, toolName, toolInput).matched;
1300
1370
  }
1301
1371
 
1372
+ function isSafeLocalCredentialHardeningCommand(toolName, toolInput = {}) {
1373
+ if (toolName !== 'Bash') return false;
1374
+ const command = String(toolInput.command || '').trim();
1375
+ if (!command || /(?:^|\s)chmod\s+[^&;|]*\s+-R\b/i.test(command)) return false;
1376
+ if (/[;&|`$()<>*?[\]{}]/.test(command)) return false;
1377
+
1378
+ const match = command.match(/(?:^|\s)chmod\s+(?:-[fv]\s+)?0?([46]00)\s+(['"]?)(\S+)\2\s*$/i);
1379
+ if (!match) return false;
1380
+
1381
+ const target = match[3];
1382
+ if (!target || target === '/' || target === '~') return false;
1383
+ if (/^\.\.?(?:\/|$)/.test(target)) return false;
1384
+
1385
+ const normalized = target.replace(/^['"]|['"]$/g, '').toLowerCase();
1386
+ const looksLikeCredentialPath = /(?:^|\/)(?:\.config|\.ssh|\.gnupg|\.aws|\.gcloud|\.gemini|\.resume_secrets|\.thumbgate|secrets?|credentials?)(?:\/|$)/.test(normalized)
1387
+ || /(?:key|secret|token|credential|gemini|gcloud|google|operator).*\.(?:json|pem|key|env)$/i.test(normalized)
1388
+ || /\.(?:pem|key)$/i.test(normalized);
1389
+
1390
+ return looksLikeCredentialPath;
1391
+ }
1392
+
1302
1393
  function evaluateMemoryGuard(toolName, toolInput = {}) {
1303
1394
  const affected = extractAffectedFiles(toolName, toolInput);
1304
1395
  const affectedFiles = affected.files;
@@ -1315,7 +1406,10 @@ function evaluateMemoryGuard(toolName, toolInput = {}) {
1315
1406
  }
1316
1407
 
1317
1408
  const command = String(toolInput.command || '');
1318
- if (toolName === 'Bash' && /\bgh\s+pr\s+create\b/i.test(command) && isConditionSatisfied('pr_create_allowed')) {
1409
+ const isPrCreateCommand = toolName === 'Bash' && (
1410
+ /\bgh\s+pr\s+create\b/i.test(command) || GH_API_PR_CREATE_PATTERN.test(command)
1411
+ );
1412
+ if (isPrCreateCommand && isConditionSatisfied('pr_create_allowed')) {
1319
1413
  const branchGovernanceViolation = buildBranchGovernanceViolation(
1320
1414
  governanceState,
1321
1415
  toolInput,
@@ -1328,7 +1422,10 @@ function evaluateMemoryGuard(toolName, toolInput = {}) {
1328
1422
  }
1329
1423
  }
1330
1424
 
1331
- if (toolName === 'Bash' && /\b(?:gh\s+pr\s+(?:create|merge)|gh\s+release\s+create|git\s+tag\b|(?:npm|yarn|pnpm)\s+publish\b)\b/i.test(command)) {
1425
+ if (toolName === 'Bash' && (
1426
+ /\b(?:gh\s+pr\s+(?:create|merge)|gh\s+release\s+create|git\s+tag\b|(?:npm|yarn|pnpm)\s+publish\b)\b/i.test(command) ||
1427
+ GH_API_PR_CREATE_PATTERN.test(command)
1428
+ )) {
1332
1429
  const branchGovernanceViolation = buildBranchGovernanceViolation(
1333
1430
  governanceState,
1334
1431
  toolInput,
@@ -1989,12 +2086,19 @@ function isApprovalGatesEnabled() {
1989
2086
  // PreToolUse hook interface (stdin/stdout JSON)
1990
2087
  // ---------------------------------------------------------------------------
1991
2088
 
1992
- function buildReminderOutput(context) {
2089
+ function buildPreToolUseOutput(fields = {}) {
1993
2090
  return {
2091
+ hookEventName: 'PreToolUse',
2092
+ ...fields,
2093
+ };
2094
+ }
2095
+
2096
+ function buildReminderOutput(context) {
2097
+ return buildPreToolUseOutput({
1994
2098
  additionalContext: context,
1995
2099
  systemReminder: context,
1996
2100
  thumbgateSystemReminder: context,
1997
- };
2101
+ });
1998
2102
  }
1999
2103
 
2000
2104
  // ---------------------------------------------------------------------------
@@ -2047,6 +2151,7 @@ function formatOutput(result, behavioralContext) {
2047
2151
  const proCta = buildBlockActionProCta() || '';
2048
2152
  return JSON.stringify({
2049
2153
  hookSpecificOutput: {
2154
+ hookEventName: 'PreToolUse',
2050
2155
  ...reminder,
2051
2156
  permissionDecision: 'deny',
2052
2157
  permissionDecisionReason: `[GATE:${result.gate}] ${result.message}${reasoningSuffix}${reminderSuffix}${proCta}`,
@@ -2059,6 +2164,7 @@ function formatOutput(result, behavioralContext) {
2059
2164
  const reminderSuffix = behavioralContext ? `\n\nSystem reminder:\n${behavioralContext}` : '';
2060
2165
  return JSON.stringify({
2061
2166
  hookSpecificOutput: {
2167
+ hookEventName: 'PreToolUse',
2062
2168
  ...reminder,
2063
2169
  permissionDecision: 'deny',
2064
2170
  permissionDecisionReason: `[GATE:${result.gate}] APPROVAL REQUIRED: ${result.message} — Ask the human to confirm this action before proceeding.${reasoningSuffix}${reminderSuffix}`,
@@ -2071,6 +2177,7 @@ function formatOutput(result, behavioralContext) {
2071
2177
  const context = `[GATE:${result.gate}] WARNING: ${result.message}${reasoningSuffix}${extra}`;
2072
2178
  return JSON.stringify({
2073
2179
  hookSpecificOutput: {
2180
+ hookEventName: 'PreToolUse',
2074
2181
  additionalContext: context,
2075
2182
  ...(behavioralContext ? {
2076
2183
  systemReminder: behavioralContext,
@@ -2204,9 +2311,8 @@ function buildRelevantLessonContext(toolName, toolInput) {
2204
2311
  const lessons = retrieveRelevantLessons(toolName, actionContext, { maxResults: 3 });
2205
2312
 
2206
2313
  const entropy = calculateRetrievalEntropy(lessons);
2207
- if (entropy > 0.7) {
2208
- recordStat("retrieval_entropy_high", "block");
2209
- return { decision: "deny", gate: "knowledge-conflict-gate", message: "✗ THUMBGATE: Action blocked due to high Knowledge Entropy (conflicting past lessons).", severity: "high" };
2314
+ if (entropy > KNOWLEDGE_ENTROPY_THRESHOLD) {
2315
+ return buildKnowledgeConflictContext(toolName, toolInput, lessons, entropy);
2210
2316
  }
2211
2317
  return formatNegativeLessonContext(lessons);
2212
2318
  } catch {
@@ -2237,16 +2343,11 @@ async function buildRelevantLessonContextAsync(toolName, toolInput) {
2237
2343
  : retrieveRelevantLessons(toolName, actionContext, { maxResults: 3 });
2238
2344
 
2239
2345
  // Knowledge Conflict Detection: if retrieved lessons have high sentiment entropy,
2240
- // it indicates conflicting past evidence. Block and require human disambiguation.
2346
+ // it indicates conflicting past evidence. Warn by default; hard-block only in
2347
+ // strict mode for external/destructive side-effect commands.
2241
2348
  const entropy = calculateRetrievalEntropy(lessons);
2242
- if (entropy > 0.7) {
2243
- recordStat('retrieval_entropy_high', 'block');
2244
- return {
2245
- decision: 'deny',
2246
- gate: 'knowledge-conflict-gate',
2247
- message: '✗ THUMBGATE: Action blocked due to high Knowledge Entropy (conflicting past lessons). Please disambiguate your instructions or verify the intended behavior manually.',
2248
- severity: 'high',
2249
- };
2349
+ if (entropy > KNOWLEDGE_ENTROPY_THRESHOLD) {
2350
+ return buildKnowledgeConflictContext(toolName, toolInput, lessons, entropy);
2250
2351
  }
2251
2352
 
2252
2353
  return formatNegativeLessonContext(lessons);
@@ -2273,6 +2374,36 @@ function formatNegativeLessonContext(lessons) {
2273
2374
  return `[ThumbGate] Past mistakes relevant to this action — read before proceeding:\n${formatted.join('\n')}`;
2274
2375
  }
2275
2376
 
2377
+ function isStrictKnowledgeConflictMode() {
2378
+ return process.env.THUMBGATE_STRICT_KNOWLEDGE_CONFLICT === '1'
2379
+ || process.env.THUMBGATE_STRICT_KNOWLEDGE_CONFLICT === 'true';
2380
+ }
2381
+
2382
+ function isKnowledgeConflictHardBlockAction(toolName, toolInput = {}) {
2383
+ if (!isStrictKnowledgeConflictMode()) return false;
2384
+ if (EDIT_LIKE_TOOLS.has(toolName)) return true;
2385
+ if (toolName !== 'Bash') return false;
2386
+ return KNOWLEDGE_CONFLICT_STRICT_BASH_PATTERN.test(String(toolInput.command || ''));
2387
+ }
2388
+
2389
+ function buildKnowledgeConflictContext(toolName, toolInput, lessons, entropy) {
2390
+ const lessonContext = formatNegativeLessonContext(lessons);
2391
+ const message = `Knowledge conflict warning: retrieved lessons disagree for this action (entropy ${entropy}). Treat the reminders below as cautionary context, but do not stop unrelated work solely because memory is noisy.`;
2392
+
2393
+ if (isKnowledgeConflictHardBlockAction(toolName, toolInput)) {
2394
+ recordStat('retrieval_entropy_high', 'block');
2395
+ return {
2396
+ decision: 'deny',
2397
+ gate: 'knowledge-conflict-gate',
2398
+ message: `✗ THUMBGATE: ${message} Strict mode is enabled for destructive or external side-effect actions; verify intent or narrow the task before proceeding.`,
2399
+ severity: 'high',
2400
+ };
2401
+ }
2402
+
2403
+ recordStat('retrieval_entropy_high', 'warn');
2404
+ return mergeContextStrings(`[ThumbGate] ${message}`, lessonContext);
2405
+ }
2406
+
2276
2407
  function extractActionContext(toolName, toolInput) {
2277
2408
  if (!toolInput) return toolName;
2278
2409
  const parts = [toolName];
@@ -2659,6 +2790,7 @@ module.exports = {
2659
2790
  setTaskScope,
2660
2791
  setBranchGovernance,
2661
2792
  approveProtectedAction,
2793
+ breakGlassEmergency,
2662
2794
  getScopeState,
2663
2795
  getBranchGovernanceState,
2664
2796
  isConditionSatisfied,
@@ -2717,6 +2849,7 @@ module.exports = {
2717
2849
  getLocalOnlyScopeSources,
2718
2850
  isRemoteSideEffectCommand,
2719
2851
  evaluateLocalOnlyRemoteSideEffectGate,
2852
+ isAgentHookSettingsFile,
2720
2853
  PR_THREAD_RESOLUTION_ACTION,
2721
2854
  buildBlockActionProCta,
2722
2855
  applyDailyBlockCap,
@@ -3,13 +3,12 @@
3
3
  /**
4
4
  * install-shim.js — Install a stable shim at ~/.thumbgate/bin/thumbgate-hook
5
5
  *
6
- * The shim is a tiny shell script that always resolves thumbgate@latest,
7
- * so hook commands in settings.local.json never go stale. This is the
8
- * Volta-style pattern: a version-agnostic indirection layer that survives
9
- * across thumbgate upgrades.
6
+ * The shim is a tiny shell script that resolves the cached ThumbGate runtime
7
+ * first, so hook commands in settings.local.json stay stable across projects
8
+ * and agent restarts.
10
9
  *
11
10
  * The shim checks for a cached runtime binary first (fast path), and falls
12
- * back to `npx --yes thumbgate@latest` (slow path, self-installs).
11
+ * back to `npx --yes thumbgate@latest` (slow path, first-time self-install).
13
12
  */
14
13
 
15
14
  const fs = require('fs');
@@ -24,9 +23,10 @@ const RUNTIME_BIN = path.join(os.homedir(), '.thumbgate', 'runtime', 'node_modul
24
23
  * The shim script. Key design choices:
25
24
  * - Uses `exec` to replace the shell process (no zombie processes)
26
25
  * - Fast path: if cached runtime binary exists, exec it directly
27
- * - Slow path: npx --yes thumbgate@latest (auto-installs)
28
- * - Background upgrade: after the fast path succeeds once, spawn a
29
- * detached npm install to refresh the cache for next time
26
+ * - Slow path: npx --yes thumbgate@latest (first-time auto-installs)
27
+ * - No default self-mutation: background upgrades are opt-in via
28
+ * THUMBGATE_SHIM_AUTO_UPDATE=1 so source checkouts, enterprise pins, and
29
+ * dogfood runtimes cannot be overwritten by a hook side effect.
30
30
  */
31
31
  function shimContent() {
32
32
  const escapedRuntimeBin = JSON.stringify(RUNTIME_BIN);
@@ -35,7 +35,7 @@ function shimContent() {
35
35
  return `#!/usr/bin/env bash
36
36
  # ThumbGate hook shim — DO NOT EDIT
37
37
  # Installed by: thumbgate init
38
- # Purpose: version-agnostic hook entry point that always runs latest ThumbGate
38
+ # Purpose: stable hook entry point that runs the cached ThumbGate runtime
39
39
  # Pattern: Volta-style stable shim (see https://volta.sh)
40
40
 
41
41
  set -euo pipefail
@@ -45,8 +45,11 @@ RUNTIME_DIR=${escapedRuntimeDir}
45
45
 
46
46
  # Fast path: cached runtime binary exists and is executable
47
47
  if [ -x "$RUNTIME_BIN" ]; then
48
- # Spawn background upgrade (detached, no stdout/stderr, won't block hook)
49
- ( nohup npm install --prefix "$RUNTIME_DIR" --no-save --omit=dev thumbgate@latest >/dev/null 2>&1 & ) 2>/dev/null || true
48
+ # Optional background upgrade. Disabled by default so hooks never mutate a
49
+ # source checkout, enterprise pin, or dogfood runtime behind the operator's back.
50
+ if [ "\${THUMBGATE_SHIM_AUTO_UPDATE:-0}" = "1" ]; then
51
+ ( nohup npm install --prefix "$RUNTIME_DIR" --no-save --omit=dev thumbgate@latest >/dev/null 2>&1 & ) 2>/dev/null || true
52
+ fi
50
53
  exec "$RUNTIME_BIN" "$@"
51
54
  fi
52
55
 
package/src/api/server.js CHANGED
@@ -418,34 +418,18 @@ const TRACKED_LINK_TARGETS = Object.freeze({
418
418
  },
419
419
  allowCustomerEmail: true,
420
420
  },
421
- // 2026-05-19: Team is intake-led. Keep the tracked shortlink alive for
422
- // marketplaces and old outreach, but route it to workflow scope first
423
- // instead of blind 3-seat checkout.
421
+ // 2026-06-02: Teams/Aiventyx deprecated. Redirect legacy links to Pro.
424
422
  teams: {
425
- path: '/#workflow-sprint-intake',
426
- ctaId: 'go_teams',
423
+ path: '/go/pro?utm_source=legacy_teams&utm_medium=redirect',
424
+ ctaId: 'go_pro',
427
425
  ctaPlacement: 'link_router',
428
- eventType: 'team_intake_started',
429
- defaults: {
430
- utm_source: 'website',
431
- utm_medium: 'link_router',
432
- utm_campaign: 'team_intake',
433
- plan_id: 'team',
434
- },
426
+ eventType: 'cta_click',
435
427
  },
436
- // Aliases: /go/team → same as /go/teams, /go/checkout → same as /go/pro,
437
- // /go/trial → install guide (trial starts on init)
438
428
  team: {
439
- path: '/#workflow-sprint-intake',
440
- ctaId: 'go_team',
429
+ path: '/go/pro?utm_source=legacy_teams&utm_medium=redirect',
430
+ ctaId: 'go_pro',
441
431
  ctaPlacement: 'link_router',
442
- eventType: 'team_intake_started',
443
- defaults: {
444
- utm_source: 'website',
445
- utm_medium: 'link_router',
446
- utm_campaign: 'team_intake',
447
- plan_id: 'team',
448
- },
432
+ eventType: 'cta_click',
449
433
  },
450
434
  checkout: {
451
435
  path: '/checkout/pro',
@@ -6405,30 +6389,7 @@ ${hidden}
6405
6389
  <h1>Case Studies</h1>
6406
6390
  <p class="lede">Real integrations. No fabricated logos, no aspirational numbers — every claim below is reproducible.</p>
6407
6391
 
6408
- <article>
6409
- <h2>Aiventyx marketplace — Teams listing intake recovery</h2>
6410
- <p class="meta">Integration partner: <a href="https://www.aiventyx.com">Aiventyx</a> · Reported by: Qaiser Mehdi · Verified: 2026-05-13</p>
6411
-
6412
- <h3>The problem</h3>
6413
- <p>Aiventyx is a marketplace for AI tools. ThumbGate's Teams listing was their highest-CTR external surface — <span class="metric">62% CTR</span> (5 clicks on 8 views, May 7–9 window). When their integrator rolled out canonical tracked URLs, every Teams click started landing on:</p>
6414
- <p><code>{"error":"Tracked link not found","allowed":["gpt","pro","install","reddit","linkedin","x","github"]}</code></p>
6415
- <p>The <code>/go/teams</code> slug wasn't registered in our redirector — a 404 was eating every paid-intent click from their strongest external surface.</p>
6416
-
6417
- <h3>The fix</h3>
6418
- <p>Added <code>teams</code> to <code>TRACKED_LINK_TARGETS</code> and now routes it to <code>/?plan_id=team#workflow-sprint-intake</code>. Caller-supplied UTMs flow through to the intake path so the workflow, owner, and proof boundary are explicit before any Team checkout.</p>
6419
-
6420
- <h3>The verification</h3>
6421
- <p>Qaiser's own incognito test, May 13 6:04 AM (full email on record):</p>
6422
- <p><code>https://thumbgate.ai/go/teams?utm_source=aiventyx</code><br>
6423
- → 302 to the Team workflow intake<br>
6424
- → pricing source, campaign, and plan metadata preserved<br>
6425
- → buyer sees the scope-first path before any subscription decision</p>
6426
-
6427
- <h3>What this proves</h3>
6428
- <p>End-to-end attribution from a third-party marketplace through ThumbGate's redirector into the Team intake path, with the caller's UTM chain preserved. Regression tests pin the redirect contract so it can't silently break or regress into a blind Team checkout.</p>
6429
-
6430
- <p><a href="/go/teams?utm_source=case-study">Try the live redirect →</a></p>
6431
- </article>
6392
+ <p><em>New case studies for individual Pro operators coming soon.</em></p>
6432
6393
 
6433
6394
  <footer>
6434
6395
  <p>Want to be the next case study? The product is real, the integration is 30 seconds: <code>npx thumbgate init</code>. If you ship something with ThumbGate and want it documented here, email <a href="mailto:igor.ganapolsky@gmail.com">igor.ganapolsky@gmail.com</a>.</p>
@@ -1,160 +0,0 @@
1
- 'use strict';
2
-
3
- const { resolveAnalyticsWindow } = require('./analytics-window');
4
- const { getBillingSummaryLive } = require('./billing');
5
- const { generateDashboard } = require('./dashboard');
6
- const { getFeedbackPaths } = require('./feedback-loop');
7
- const { resolveHostedBillingConfig } = require('./hosted-config');
8
- const { loadOperatorConfig } = require('./operational-summary');
9
-
10
- function normalizeText(value) {
11
- if (value === undefined || value === null) return null;
12
- const text = String(value).trim();
13
- return text || null;
14
- }
15
-
16
- function shouldPreferHostedDashboard() {
17
- return String(process.env.THUMBGATE_METRICS_SOURCE || '').trim().toLowerCase() !== 'local';
18
- }
19
-
20
- function resolveHostedDashboardConfig() {
21
- const runtimeConfig = resolveHostedBillingConfig();
22
- const operatorConfig = loadOperatorConfig();
23
- // Match operational-summary's key priority chain so north-star and cfo
24
- // authenticate against the same hosted deployment consistently. Prior to
25
- // this change, north-star only read THUMBGATE_API_KEY, silently 401'ing
26
- // on machines configured via operator.json or THUMBGATE_OPERATOR_KEY.
27
- const apiKey = normalizeText(process.env.THUMBGATE_OPERATOR_KEY)
28
- || operatorConfig.operatorKey
29
- || normalizeText(process.env.THUMBGATE_API_KEY);
30
- const apiBaseUrl = normalizeText(process.env.THUMBGATE_BILLING_API_BASE_URL)
31
- || operatorConfig.baseUrl
32
- || runtimeConfig.billingApiBaseUrl;
33
- return {
34
- apiBaseUrl,
35
- apiKey,
36
- };
37
- }
38
-
39
- async function buildOperationalDashboard(options = {}) {
40
- const analyticsWindow = resolveAnalyticsWindow(options);
41
- const feedbackDir = options.feedbackDir || getFeedbackPaths().FEEDBACK_DIR;
42
- const billingSummary = await getBillingSummaryLive(analyticsWindow);
43
-
44
- return generateDashboard(feedbackDir, {
45
- analyticsWindow,
46
- billingSummary,
47
- billingSource: 'live',
48
- billingFallbackReason: null,
49
- });
50
- }
51
-
52
- async function fetchHostedDashboard(options = {}, config = resolveHostedDashboardConfig()) {
53
- const analyticsWindow = resolveAnalyticsWindow(options);
54
- if (!shouldPreferHostedDashboard()) {
55
- const err = new Error('Hosted operational dashboard is disabled.');
56
- err.code = 'hosted_dashboard_disabled';
57
- throw err;
58
- }
59
- if (!config.apiBaseUrl || !config.apiKey) {
60
- const err = new Error('Hosted operational dashboard is not configured.');
61
- err.code = 'hosted_dashboard_unconfigured';
62
- throw err;
63
- }
64
-
65
- const requestUrl = new URL('/v1/dashboard', config.apiBaseUrl);
66
- requestUrl.searchParams.set('window', analyticsWindow.window);
67
- requestUrl.searchParams.set('timezone', analyticsWindow.timeZone);
68
- requestUrl.searchParams.set('now', analyticsWindow.now);
69
-
70
- const response = await fetch(requestUrl, {
71
- method: 'GET',
72
- headers: {
73
- authorization: `Bearer ${config.apiKey}`,
74
- accept: 'application/json',
75
- },
76
- });
77
-
78
- if (!response.ok) {
79
- const detail = await response.text().catch(() => '');
80
- const err = new Error(`Hosted operational dashboard request failed (${response.status}): ${detail || 'unknown error'}`);
81
- err.code = 'hosted_dashboard_http_error';
82
- err.status = response.status;
83
- throw err;
84
- }
85
-
86
- return response.json();
87
- }
88
-
89
- async function getOperationalDashboard(options = {}) {
90
- const analyticsWindow = resolveAnalyticsWindow(options);
91
- try {
92
- const data = await fetchHostedDashboard(analyticsWindow);
93
- return {
94
- source: 'hosted',
95
- data,
96
- fallbackReason: null,
97
- hostedStatus: 200,
98
- };
99
- } catch (err) {
100
- const reason = err && err.message ? err.message : 'hosted_dashboard_unavailable';
101
- const status = err && typeof err.status === 'number' ? err.status : null;
102
- const code = err && err.code ? err.code : null;
103
-
104
- // Hosted deliberately disabled or never configured — local fallback is
105
- // intentional, not a degraded state. Tag as plain 'local'.
106
- if (code === 'hosted_dashboard_disabled' || code === 'hosted_dashboard_unconfigured') {
107
- return {
108
- source: 'local',
109
- data: await buildOperationalDashboard(analyticsWindow),
110
- fallbackReason: reason,
111
- hostedStatus: null,
112
- };
113
- }
114
-
115
- // Mirror operational-summary: auth failure is the dangerous case. A
116
- // dashboard that silently shows $0 revenue (from the local ledger) when
117
- // Stripe actually has paid customers is a lie the operator acts on.
118
- // Refuse to guess — surface an actionable error.
119
- if (status === 401 || status === 403) {
120
- const authErr = new Error(
121
- `Hosted operational dashboard rejected credentials (HTTP ${status}). ` +
122
- `The operator key on this machine does not match the one on the ` +
123
- `hosted deployment. Fix: set THUMBGATE_OPERATOR_KEY in this shell, ` +
124
- `or update the operatorKey field in ~/.config/thumbgate/operator.json, ` +
125
- `to match Railway's THUMBGATE_OPERATOR_KEY. ` +
126
- `Running north-star without hosted auth would report local-only ` +
127
- `data as ground truth, which may not reflect actual Stripe revenue. ` +
128
- `Original response: ${reason}`
129
- );
130
- authErr.code = 'hosted_dashboard_unauthorized';
131
- authErr.status = status;
132
- throw authErr;
133
- }
134
-
135
- // Non-auth failure — local fallback is still useful for dev workflows,
136
- // but tag the source so downstream renderers do not mistake it for
137
- // verified hosted truth.
138
- //
139
- // Log only the status code (trusted) — the full reason contains upstream
140
- // response text and is only returned structurally via fallbackReason.
141
- console.warn(
142
- `[operational-dashboard] Hosted dashboard unreachable (status=${status ?? 'network'}); ` +
143
- `falling back to LOCAL-UNVERIFIED state. Numbers below may not reflect actual Stripe revenue.`
144
- );
145
- return {
146
- source: 'local-unverified',
147
- data: await buildOperationalDashboard(analyticsWindow),
148
- fallbackReason: reason,
149
- hostedStatus: status,
150
- };
151
- }
152
- }
153
-
154
- module.exports = {
155
- buildOperationalDashboard,
156
- fetchHostedDashboard,
157
- getOperationalDashboard,
158
- resolveHostedDashboardConfig,
159
- shouldPreferHostedDashboard,
160
- };