thumbgate 1.28.0 → 1.28.2

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.
Files changed (38) hide show
  1. package/.claude-plugin/plugin.json +2 -1
  2. package/.well-known/mcp/server-card.json +1 -1
  3. package/README.md +96 -122
  4. package/adapters/claude/.mcp.json +2 -2
  5. package/adapters/mcp/server-stdio.js +54 -34
  6. package/adapters/opencode/opencode.json +1 -1
  7. package/bin/cli.js +100 -77
  8. package/config/gates/default.json +2 -2
  9. package/config/github-about.json +2 -5
  10. package/config/mcp-allowlists.json +5 -0
  11. package/config/post-deploy-marketing-pages.json +1 -1
  12. package/hooks/hooks.json +38 -0
  13. package/package.json +19 -8
  14. package/public/chatgpt-app.html +2 -2
  15. package/public/codex-enterprise.html +1 -1
  16. package/public/codex-plugin.html +1 -1
  17. package/public/diagnostic.html +23 -1
  18. package/public/guide.html +1 -1
  19. package/public/index.html +172 -201
  20. package/public/numbers.html +2 -2
  21. package/public/partner-intake.html +198 -0
  22. package/public/pricing.html +51 -14
  23. package/scripts/auto-wire-hooks.js +58 -2
  24. package/scripts/billing.js +815 -137
  25. package/scripts/checkout-attribution-reference.js +85 -0
  26. package/scripts/claude-feedback-sync.js +23 -6
  27. package/scripts/cli-feedback.js +20 -10
  28. package/scripts/feedback-history-distiller.js +385 -0
  29. package/scripts/feedback-loop.js +290 -1
  30. package/scripts/feedback-quality.js +25 -26
  31. package/scripts/feedback-sanitizer.js +151 -3
  32. package/scripts/gates-engine.js +128 -49
  33. package/scripts/hosted-config.js +9 -2
  34. package/scripts/mailer/resend-mailer.js +11 -5
  35. package/scripts/secret-scanner.js +363 -68
  36. package/scripts/self-protection.js +21 -36
  37. package/src/api/server.js +121 -12
  38. package/public/assets/brand/github-social-preview.png +0 -0
@@ -187,6 +187,24 @@ const PRIVATE_MCP_MODULES = Object.freeze({
187
187
  lessonSearch: path.resolve(__dirname, '../../scripts/lesson-search.js'),
188
188
  });
189
189
 
190
+ const PRIVATE_MCP_TOOL_REQUIREMENTS = Object.freeze({
191
+ search_lessons: ['lessonSearch'],
192
+ reflect_on_feedback: ['reflectorAgent'],
193
+ list_intents: ['intentRouter'],
194
+ plan_intent: ['intentRouter'],
195
+ start_handoff: ['intentRouter', 'delegationRuntime'],
196
+ complete_handoff: ['delegationRuntime'],
197
+ distribute_context_to_agents: ['swarmCoordinator'],
198
+ session_report: ['sessionReport'],
199
+ generate_operator_artifact: ['operatorArtifacts'],
200
+ org_dashboard: ['orgDashboard'],
201
+ get_business_metrics: ['semanticLayer'],
202
+ describe_semantic_entity: ['semanticLayer'],
203
+ run_managed_lesson_agent: ['managedLessonAgent'],
204
+ managed_agent_status: ['managedLessonAgent'],
205
+ context_stuff_lessons: ['lessonInference'],
206
+ });
207
+
190
208
  function loadPrivateMcpModule(key) {
191
209
  const modulePath = PRIVATE_MCP_MODULES[key];
192
210
  if (!modulePath) {
@@ -204,6 +222,20 @@ function loadPrivateMcpModule(key) {
204
222
  }
205
223
  }
206
224
 
225
+ function isToolAvailable(toolName) {
226
+ const requirements = PRIVATE_MCP_TOOL_REQUIREMENTS[toolName] || [];
227
+ try {
228
+ return requirements.every((key) => Boolean(loadPrivateMcpModule(key)));
229
+ } catch {
230
+ return false;
231
+ }
232
+ }
233
+
234
+ function listAvailableTools(profileName = getActiveMcpProfile()) {
235
+ const allowedTools = new Set(getAllowedTools(profileName));
236
+ return TOOLS.filter((tool) => allowedTools.has(tool.name) && isToolAvailable(tool.name));
237
+ }
238
+
207
239
  function unavailablePrivateMcpFeature(toolName) {
208
240
  return toTextResult({
209
241
  ok: false,
@@ -231,7 +263,7 @@ const {
231
263
  finalizeSession: finalizeFeedbackSession,
232
264
  } = require('../../scripts/feedback-session');
233
265
 
234
- const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.28.0' };
266
+ const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.28.2' };
235
267
  const COMMERCE_CATEGORIES = [
236
268
  'product_recommendation',
237
269
  'brand_compliance',
@@ -1357,7 +1389,7 @@ async function handleRequest(message) {
1357
1389
  };
1358
1390
  }
1359
1391
  if (message.method === 'ping') return {};
1360
- if (message.method === 'tools/list') return { tools: TOOLS };
1392
+ if (message.method === 'tools/list') return { tools: listAvailableTools() };
1361
1393
  if (message.method === 'tools/call') return callTool(message.params.name, message.params.arguments);
1362
1394
  throw new Error(`Unsupported method: ${message.method}`);
1363
1395
  }
@@ -1430,22 +1462,14 @@ function writeNdjsonResponse(id, payload, error = null) {
1430
1462
  process.stdout.write(`${body}\n`);
1431
1463
  }
1432
1464
 
1433
- /**
1434
- * Default staleness threshold: if a lock is older than this (ms), the holder
1435
- * is considered orphaned even if its PID is still alive — it likely belongs
1436
- * to a defunct Claude Code session whose process was never reaped.
1437
- */
1438
- const LOCK_STALE_MS = Number(process.env.THUMBGATE_LOCK_STALE_MS) || 2 * 60 * 60 * 1000; // 2 hours
1439
-
1440
1465
  /**
1441
1466
  * Acquire a file-system lock to prevent duplicate MCP server instances.
1442
- * Returns { lockFile, cleanupLock } on success, or calls process.exit(1)
1443
- * if another live server holds the lock.
1467
+ * Returns { lockFile, cleanupLock } on success.
1444
1468
  *
1445
- * Staleness reaping: if the lock-holding process is alive but the lock is
1446
- * older than LOCK_STALE_MS, the holder is killed (SIGTERM) and the lock is
1447
- * reclaimed. This prevents orphaned `thumbgate serve` processes from permanently
1448
- * blocking new sessions.
1469
+ * Every live stdio client needs its own MCP process, and those sessions can
1470
+ * legitimately run for many hours. A lock's age is therefore not evidence that
1471
+ * its owner is orphaned. Live owners coexist through per-session locks; only a
1472
+ * lock whose PID is no longer running is reclaimed.
1449
1473
  */
1450
1474
  function acquireLock() {
1451
1475
  const feedbackDir = getFeedbackPaths().FEEDBACK_DIR;
@@ -1458,26 +1482,19 @@ function acquireLock() {
1458
1482
  try { process.kill(lockData.pid, 0); isRunning = true; } catch { /* process is dead */ }
1459
1483
 
1460
1484
  if (isRunning) {
1461
- const lockAge = Date.now() - new Date(lockData.startedAt).getTime();
1462
- if (lockAge > LOCK_STALE_MS) {
1463
- // Orphaned process kill it and take over
1464
- process.stderr.write(`[thumbgate] Lock held by PID ${lockData.pid} is ${Math.round(lockAge / 60000)}m old (threshold: ${Math.round(LOCK_STALE_MS / 60000)}m). Reaping orphaned process.\n`);
1465
- try { process.kill(lockData.pid, 'SIGTERM'); } catch { /* already gone */ }
1466
- } else {
1467
- // Another session's MCP server is running coexist via per-session lock.
1468
- // Each Claude session communicates via its own stdio pipe and needs its own server.
1469
- // SQLite WAL mode handles concurrent access safely.
1470
- process.stderr.write(`[thumbgate] Another MCP server (PID ${lockData.pid}) is running for ${feedbackDir}. Starting concurrent session.\n`);
1471
- const sessionLockFile = path.join(feedbackDir, `.mcp-server-${process.pid}.lock`);
1472
- fs.writeFileSync(sessionLockFile, JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
1473
- const cleanupSessionLock = () => { try { fs.unlinkSync(sessionLockFile); } catch { /* already removed */ } };
1474
- process.on('exit', cleanupSessionLock);
1475
- process.on('SIGTERM', () => { cleanupSessionLock(); process.exit(0); });
1476
- process.on('SIGINT', () => { cleanupSessionLock(); process.exit(0); });
1477
- return { lockFile: sessionLockFile, cleanupLock: cleanupSessionLock };
1478
- }
1485
+ // Another session's MCP server is running — coexist via per-session lock.
1486
+ // Each client communicates via its own stdio pipe and needs its own server.
1487
+ // SQLite WAL mode handles concurrent access safely.
1488
+ process.stderr.write(`[thumbgate] Another MCP server (PID ${lockData.pid}) is running for ${feedbackDir}. Starting concurrent session.\n`);
1489
+ const sessionLockFile = path.join(feedbackDir, `.mcp-server-${process.pid}.lock`);
1490
+ fs.writeFileSync(sessionLockFile, JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
1491
+ const cleanupSessionLock = () => { try { fs.unlinkSync(sessionLockFile); } catch { /* already removed */ } };
1492
+ process.on('exit', cleanupSessionLock);
1493
+ process.on('SIGTERM', () => { cleanupSessionLock(); process.exit(0); });
1494
+ process.on('SIGINT', () => { cleanupSessionLock(); process.exit(0); });
1495
+ return { lockFile: sessionLockFile, cleanupLock: cleanupSessionLock };
1479
1496
  }
1480
- // Stale lock from a dead or reaped process — remove it
1497
+ // Stale lock from a dead process — remove it.
1481
1498
  try { fs.unlinkSync(lockFile); } catch { /* already gone */ }
1482
1499
  process.stderr.write(`[thumbgate] Removed stale lock (PID ${lockData.pid} is no longer running).\n`);
1483
1500
  }
@@ -1574,7 +1591,10 @@ module.exports = {
1574
1591
  buildSuggestFixResponse,
1575
1592
  __test__: {
1576
1593
  PRIVATE_MCP_MODULES,
1594
+ PRIVATE_MCP_TOOL_REQUIREMENTS,
1577
1595
  loadPrivateMcpModule,
1596
+ isToolAvailable,
1597
+ listAvailableTools,
1578
1598
  unavailablePrivateMcpFeature,
1579
1599
  callToolInner,
1580
1600
  },
@@ -7,7 +7,7 @@
7
7
  "npx",
8
8
  "--yes",
9
9
  "--package",
10
- "thumbgate@1.28.0",
10
+ "thumbgate@1.28.2",
11
11
  "thumbgate",
12
12
  "serve"
13
13
  ],
package/bin/cli.js CHANGED
@@ -444,6 +444,11 @@ function codexPreToolHookSectionBlock() {
444
444
  return `[hooks.pre_tool_use]\ncommand = "${entry.command}"\nargs = ${formatTomlStringArray(entry.args)}\n`;
445
445
  }
446
446
 
447
+ function codexUserPromptHookSectionBlock() {
448
+ const entry = canonicalCodexCliEntry(['hook-auto-capture']);
449
+ return `[hooks.user_prompt_submit]\ncommand = "${entry.command}"\nargs = ${formatTomlStringArray(entry.args)}\n`;
450
+ }
451
+
447
452
  function mcpSectionRegex(name) {
448
453
  return new RegExp(
449
454
  `^\\[mcp_servers\\.${escapeRegExp(name)}\\]\\n(?:^(?!\\[).*(?:\\n|$))*`,
@@ -461,6 +466,7 @@ function tomlSectionRegex(name) {
461
466
  function upsertCodexServerConfig(content) {
462
467
  const canonicalBlock = codexMcpSectionBlock(MCP_SERVER_NAME);
463
468
  const canonicalHookBlock = codexPreToolHookSectionBlock();
469
+ const canonicalUserPromptHookBlock = codexUserPromptHookSectionBlock();
464
470
  const sections = MCP_SERVER_NAMES.map((name) => ({
465
471
  name,
466
472
  regex: mcpSectionRegex(name),
@@ -473,7 +479,7 @@ function upsertCodexServerConfig(content) {
473
479
  const prefix = content.trimEnd();
474
480
  return {
475
481
  changed: true,
476
- content: `${prefix}${prefix ? '\n\n' : ''}${canonicalBlock}\n${canonicalHookBlock}`,
482
+ content: `${prefix}${prefix ? '\n\n' : ''}${canonicalBlock}\n${canonicalHookBlock}\n${canonicalUserPromptHookBlock}`,
477
483
  };
478
484
  }
479
485
 
@@ -517,6 +523,19 @@ function upsertCodexServerConfig(content) {
517
523
  changed = true;
518
524
  }
519
525
 
526
+ const userPromptHookRegex = tomlSectionRegex('hooks.user_prompt_submit');
527
+ if (userPromptHookRegex.test(nextContent)) {
528
+ const current = nextContent.match(userPromptHookRegex)[0];
529
+ if (current !== canonicalUserPromptHookBlock) {
530
+ nextContent = nextContent.replace(userPromptHookRegex, canonicalUserPromptHookBlock);
531
+ changed = true;
532
+ }
533
+ } else {
534
+ const prefix = nextContent.trimEnd();
535
+ nextContent = `${prefix}${prefix ? '\n\n' : ''}${canonicalUserPromptHookBlock}`;
536
+ changed = true;
537
+ }
538
+
520
539
  return {
521
540
  changed,
522
541
  content: nextContent.endsWith('\n') ? nextContent : `${nextContent}\n`,
@@ -2695,23 +2714,27 @@ function install() {
2695
2714
  }
2696
2715
 
2697
2716
  async function gateCheck() {
2698
- // Explicit emergency escape hatch ONLY. The 2026-06-03 hotfix made this
2699
- // bypass-by-default, which silently disabled ThumbGate's enforcement entirely
2700
- // (the firewall approved everything). Restored 2026-06-04: enforcement runs by
2701
- // default in warn-by-default posture (see gates-engine applyEnforcementPosture);
2702
- // set THUMBGATE_HOTFIX_BYPASS=1 to disable all checks if a gate ever misfires.
2703
- if (process.env.THUMBGATE_HOTFIX_BYPASS === '1') {
2704
- process.stdout.write(JSON.stringify({
2705
- decision: 'approve',
2706
- reason: 'hotfix-bypass-opt-in',
2707
- hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: '' }
2708
- }) + '\n');
2709
- return;
2710
- }
2711
2717
  try {
2712
2718
  const payload = readStdinText();
2713
2719
  const input = payload ? JSON.parse(payload) : {};
2714
2720
  const gatesEngine = require(path.join(PKG_ROOT, 'scripts', 'gates-engine'));
2721
+
2722
+ // The operator bypass only skips advisory and strict-mode gates. Security and
2723
+ // self-protection floors are evaluated before the early approval path.
2724
+ if (process.env.THUMBGATE_HOTFIX_BYPASS === '1') {
2725
+ const hardFloorOutput = gatesEngine.runHardFloor(input);
2726
+ if (hardFloorOutput) {
2727
+ process.stdout.write(hardFloorOutput + '\n');
2728
+ return;
2729
+ }
2730
+ process.stdout.write(JSON.stringify({
2731
+ decision: 'approve',
2732
+ reason: 'operator-bypass-opt-in',
2733
+ hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: '' }
2734
+ }) + '\n');
2735
+ return;
2736
+ }
2737
+
2715
2738
  const output = await gatesEngine.runAsync(input);
2716
2739
  process.stdout.write(output + '\n');
2717
2740
  } catch (err) {
@@ -2749,14 +2772,21 @@ function statuslineRender() {
2749
2772
 
2750
2773
  function hookAutoCapture() {
2751
2774
  syncActiveProjectContext();
2775
+ const { extractPromptEnvelope } = require(path.join(PKG_ROOT, 'scripts', 'feedback-sanitizer'));
2776
+ const rawStdin = readStdinText();
2777
+ const promptEnvelope = extractPromptEnvelope(rawStdin);
2778
+ // stdin on a UserPromptSubmit hook is the JSON payload
2779
+ // {session_id, transcript_path, cwd, prompt, ...}. Persist ONLY the human
2780
+ // `.prompt` field — never the whole stdin object — so session metadata blobs
2781
+ // can't be promoted as lessons.
2752
2782
  const prompt = process.env.CLAUDE_USER_PROMPT
2753
2783
  || process.env.THUMBGATE_USER_PROMPT
2754
2784
  || process.env.CODEX_USER_PROMPT
2755
2785
  || process.env.USER_PROMPT
2756
- || readStdinText().trim();
2786
+ || promptEnvelope.prompt;
2757
2787
  const { evaluatePromptGuard } = require(path.join(PKG_ROOT, 'scripts', 'prompt-guard'));
2758
2788
  const { processInlineFeedback, formatCliOutput } = require(path.join(PKG_ROOT, 'scripts', 'cli-feedback'));
2759
- const { detectFeedbackSignal } = require(path.join(PKG_ROOT, 'scripts', 'feedback-quality'));
2789
+ const { detectFeedbackSignal, isGenericFeedbackText } = require(path.join(PKG_ROOT, 'scripts', 'feedback-quality'));
2760
2790
  const { loadOptionalModule } = require(path.join(PKG_ROOT, 'scripts', 'private-core-boundary'));
2761
2791
  const { recordConversationEntry, readRecentConversationWindow } = loadOptionalModule(
2762
2792
  path.join(PKG_ROOT, 'scripts', 'feedback-history-distiller'),
@@ -2766,34 +2796,60 @@ function hookAutoCapture() {
2766
2796
  })
2767
2797
  );
2768
2798
 
2769
- recordConversationEntry({
2770
- author: 'user',
2771
- text: prompt,
2772
- source: 'claude_user_prompt',
2773
- });
2774
-
2775
2799
  const guardResult = evaluatePromptGuard(prompt);
2776
2800
  if (guardResult) {
2801
+ recordConversationEntry({
2802
+ author: 'user',
2803
+ text: prompt,
2804
+ source: 'claude_user_prompt',
2805
+ });
2777
2806
  process.stdout.write(`${JSON.stringify(guardResult)}\n`);
2778
2807
  return;
2779
2808
  }
2780
2809
 
2781
2810
  const detected = detectFeedbackSignal(prompt);
2782
2811
  if (!detected) {
2812
+ recordConversationEntry({
2813
+ author: 'user',
2814
+ text: prompt,
2815
+ source: 'claude_user_prompt',
2816
+ });
2783
2817
  return;
2784
2818
  }
2785
2819
 
2786
2820
  const signal = detected.signal;
2787
2821
  const conversationWindow = readRecentConversationWindow({ limit: 8 });
2822
+ const chatHistory = conversationWindow.map((entry) => ({
2823
+ role: entry.author === 'assistant' ? 'assistant' : 'user',
2824
+ content: entry.text || '',
2825
+ }));
2826
+ const normalizedSignal = signal === 'down' ? 'negative' : 'positive';
2827
+ const genericFeedback = isGenericFeedbackText(prompt, normalizedSignal);
2828
+ const { buildFeedbackSourceIdentity } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
2829
+ const sourceEvent = buildFeedbackSourceIdentity({
2830
+ signal,
2831
+ promptText: prompt,
2832
+ sessionId: promptEnvelope.sessionId || process.env.CLAUDE_SESSION_ID,
2833
+ promptId: promptEnvelope.promptId,
2834
+ projectDir: promptEnvelope.projectDir || CWD,
2835
+ timestamp: promptEnvelope.timestamp,
2836
+ source: 'claude-user-prompt',
2837
+ });
2788
2838
  const result = processInlineFeedback({
2789
2839
  signal,
2790
2840
  context: prompt,
2791
- chatHistory: signal === 'down'
2792
- ? conversationWindow.map((entry) => ({ role: entry.author === 'assistant' ? 'assistant' : 'user', content: entry.text || '' }))
2793
- : undefined,
2794
- whatWentWrong: signal === 'down' ? prompt : undefined,
2795
- whatWorked: signal === 'up' ? prompt : undefined,
2841
+ chatHistory,
2842
+ whatWentWrong: signal === 'down' && !genericFeedback ? prompt : undefined,
2843
+ whatWorked: signal === 'up' && !genericFeedback ? prompt : undefined,
2844
+ sourceEvent,
2796
2845
  });
2846
+ if (!result.feedbackResult || !result.feedbackResult.duplicate) {
2847
+ recordConversationEntry({
2848
+ author: 'user',
2849
+ text: prompt,
2850
+ source: 'claude_user_prompt',
2851
+ });
2852
+ }
2797
2853
  process.stdout.write(formatCliOutput(result) + '\n');
2798
2854
  }
2799
2855
 
@@ -3947,53 +4003,6 @@ switch (COMMAND) {
3947
4003
  case 'dispatch-brief':
3948
4004
  dispatchBrief();
3949
4005
  break;
3950
- case 'gate-check': {
3951
- // PreToolUse hook interface: reads tool call JSON from stdin, outputs gate verdict
3952
- // Used by: generate-pretool-hook.sh → npx thumbgate gate-check
3953
- const { run: gateRun, runAsync: gateRunAsync } = require(path.join(PKG_ROOT, 'scripts', 'gates-engine'));
3954
- const { evaluateSelfProtection } = require(path.join(PKG_ROOT, 'scripts', 'self-protection'));
3955
- let stdinData = '';
3956
- process.stdin.setEncoding('utf8');
3957
- process.stdin.on('data', (chunk) => { stdinData += chunk; });
3958
- process.stdin.on('end', async () => {
3959
- try {
3960
- const input = JSON.parse(stdinData);
3961
- const output = await gateRunAsync(input);
3962
- // Self-protection overlay (2026-07-08): the gate engine only denies edits
3963
- // to ThumbGate's own governance files under strict enforcement; by default
3964
- // such edits pass as a clean ALLOW. Surface them (or block in strict) so an
3965
- // agent can't silently disable the firewall. Only overlays when the engine
3966
- // did not already produce a hard deny.
3967
- let verdict = output;
3968
- try {
3969
- const parsed = JSON.parse(output || '{}');
3970
- const alreadyDeny = (parsed.hookSpecificOutput && parsed.hookSpecificOutput.permissionDecision === 'deny')
3971
- || parsed.decision === 'block';
3972
- if (!alreadyDeny) {
3973
- const sp = evaluateSelfProtection(input.tool_name, input.tool_input);
3974
- if (sp && sp.action === 'block') {
3975
- verdict = JSON.stringify({
3976
- decision: 'block',
3977
- reason: sp.message,
3978
- hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: sp.message },
3979
- });
3980
- } else if (sp && sp.action === 'warn') {
3981
- verdict = JSON.stringify({
3982
- hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: sp.message },
3983
- });
3984
- }
3985
- }
3986
- } catch (_e) { /* non-JSON engine output: pass through unchanged */ }
3987
- process.stdout.write(verdict + '\n');
3988
- process.exit(0);
3989
- } catch (err) {
3990
- process.stderr.write(`gate-check error: ${err.message}\n`);
3991
- process.stdout.write(JSON.stringify({}) + '\n');
3992
- process.exit(0);
3993
- }
3994
- });
3995
- break;
3996
- }
3997
4006
  case 'hermes-gate': {
3998
4007
  // Nous Research Hermes Agent `pre_tool_call` shell hook.
3999
4008
  // Hermes pipes each pending tool call as JSON to stdin and reads a decision from stdout;
@@ -4004,7 +4013,7 @@ switch (COMMAND) {
4004
4013
  // Hermes `pre_tool_call` is binary (block or allow) with no warn channel, and the whole point
4005
4014
  // of wiring it is to gate, so we run STRICT enforcement by default — otherwise ThumbGate's
4006
4015
  // warn-by-default posture would pass every deny through and the hook would block nothing.
4007
- // Opt out with THUMBGATE_HERMES_WARN_ONLY=1; THUMBGATE_HOTFIX_BYPASS=1 still disables checks.
4016
+ // Opt out with THUMBGATE_HERMES_WARN_ONLY=1; the operator bypass also leaves ordinary gates advisory.
4008
4017
  // Wire it in ~/.hermes/config.yaml — see adapters/hermes/config.yaml.
4009
4018
  if (process.env.THUMBGATE_HERMES_WARN_ONLY !== '1' && process.env.THUMBGATE_HOTFIX_BYPASS !== '1') {
4010
4019
  process.env.THUMBGATE_STRICT_ENFORCEMENT = '1';
@@ -4016,8 +4025,22 @@ switch (COMMAND) {
4016
4025
  process.stdin.on('end', async () => {
4017
4026
  try {
4018
4027
  const payload = JSON.parse(hermesStdin);
4019
- // Hermes sends snake_case tool_name/tool_input gates-engine reads these directly.
4020
- const verdict = await hermesGateRun({ tool_name: payload.tool_name, tool_input: payload.tool_input });
4028
+ const hermesToolName = String(payload.tool_name || '');
4029
+ const canonicalToolNames = {
4030
+ terminal: 'Bash',
4031
+ process: 'Bash',
4032
+ execute_code: 'Bash',
4033
+ patch: 'Edit',
4034
+ write_file: 'Write',
4035
+ read_file: 'Read',
4036
+ };
4037
+ const verdict = await hermesGateRun({
4038
+ tool_name: canonicalToolNames[hermesToolName] || hermesToolName,
4039
+ tool_input: {
4040
+ ...(payload.tool_input || {}),
4041
+ hermes_tool_name: hermesToolName,
4042
+ },
4043
+ });
4021
4044
  let parsed = {};
4022
4045
  try { parsed = JSON.parse(verdict); } catch (_e) { parsed = {}; }
4023
4046
  const hso = parsed.hookSpecificOutput || {};
@@ -342,7 +342,7 @@
342
342
  "id": "self-protect-config",
343
343
  "layer": "Execution",
344
344
  "toolNames": ["Edit", "Write", "MultiEdit"],
345
- "pattern": "(?:config/gates/|config/budget\\.json|\\.thumbgate/|thumbgate\\.json)",
345
+ "pattern": "(?:config/gates/|config/(?:budget|enforcement|mcp-allowlists)\\.json|\\.thumbgate/config\\.json|thumbgate\\.json)",
346
346
  "action": "block",
347
347
  "message": "Self-protection: agent cannot modify ThumbGate configuration, gate rules, or budget settings.",
348
348
  "severity": "critical",
@@ -372,7 +372,7 @@
372
372
  "id": "self-protect-hooks-disable",
373
373
  "layer": "Execution",
374
374
  "toolNames": ["Edit", "Write", "Bash"],
375
- "pattern": "(?:settings\\.json|settings\\.local\\.json).*(?:hooks|PreToolUse|PostToolUse)",
375
+ "pattern": "(?:\\.claude/settings(?:\\.local)?\\.json|\\.codex/config\\.toml|scripts/hook-[^/]+\\.(?:js|sh))",
376
376
  "action": "block",
377
377
  "message": "Self-protection: agent cannot modify hook registrations.",
378
378
  "severity": "critical",
@@ -2,14 +2,11 @@
2
2
  "repo": "IgorGanapolsky/ThumbGate",
3
3
  "repositoryUrl": "https://github.com/IgorGanapolsky/ThumbGate",
4
4
  "homepageUrl": "https://thumbgate.ai",
5
- "githubDescription": "Agent governance: thumbs become Pre-Action Checks that flag repeat mistakes and hard-block catastrophic classes before code or systems change.",
6
- "metaDescription": "ThumbGate is the pre-action governance runtime for AI coding agents (Claude Code, Cursor, Codex, Gemini CLI, Amp, Cline, OpenCode): 👍 thumbs up and 👎 thumbs down feedback become history-aware lessons stored locally; Enterprise adds shared lessons and org visibility. PreToolUse checks flag and log risky tool calls hard-blocking catastrophic classes by default and warning on the rest, with strict mode to hard-block every rule.",
5
+ "githubDescription": "ThumbGate Pre-Action Checks derive rules from repeated failures, flag risky tool calls, hard-block detected secret leaks, and block matches in strict mode.",
6
+ "metaDescription": "ThumbGate is the local pre-action governance runtime for AI coding agents (Claude Code, Cursor, Codex, Gemini CLI, Amp, Cline, OpenCode): accepted 👍 thumbs-up and 👎 thumbs-down feedback becomes history-aware local lessons; repeated failures can become prevention rules. PreToolUse checks flag and log risky tool calls, hard-block detected secret leaks, and block matching actions in strict mode. Pro is for individual operators; Enterprise rollout is scoped after intake.",
7
7
  "topics": [
8
8
  "thumbgate",
9
9
  "pre-action-checks",
10
- "save-llm-tokens",
11
- "reduce-llm-cost",
12
- "ai-cost-optimization",
13
10
  "mcp",
14
11
  "mcp-server",
15
12
  "ai-agents",
@@ -68,6 +68,11 @@
68
68
  "distribute_context_to_agents",
69
69
  "session_report",
70
70
  "generate_operator_artifact",
71
+ "run_managed_lesson_agent",
72
+ "managed_agent_status",
73
+ "run_self_distill",
74
+ "self_distill_status",
75
+ "context_stuff_lessons",
71
76
  "perplexity_search",
72
77
  "perplexity_ask",
73
78
  "perplexity_research",
@@ -4,7 +4,7 @@
4
4
  "pages": [
5
5
  {
6
6
  "route": "/",
7
- "sentinel": "Stop paying for the same AI mistake twice",
7
+ "sentinel": "Catch AI agents before they spend money",
8
8
  "description": "Home page hero (lifeblood — never regress)"
9
9
  },
10
10
  {
@@ -0,0 +1,38 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Bash|Edit|Write",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node",
10
+ "args": ["${CLAUDE_PLUGIN_ROOT}/scripts/hook-pre-tool-use.js"]
11
+ }
12
+ ]
13
+ }
14
+ ],
15
+ "UserPromptSubmit": [
16
+ {
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "node",
21
+ "args": ["${CLAUDE_PLUGIN_ROOT}/bin/cli.js", "hook-auto-capture"]
22
+ }
23
+ ]
24
+ }
25
+ ],
26
+ "SessionStart": [
27
+ {
28
+ "hooks": [
29
+ {
30
+ "type": "command",
31
+ "command": "node",
32
+ "args": ["${CLAUDE_PLUGIN_ROOT}/bin/cli.js", "session-start"]
33
+ }
34
+ ]
35
+ }
36
+ ]
37
+ }
38
+ }