thumbgate 1.26.2 β†’ 1.26.4

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.2",
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.2",
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.2",
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.2",
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.2", "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.2", "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.2' };
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.2",
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.2",
3
+ "version": "1.26.4",
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,8 +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
- "scripts/operational-summary.js",
140
138
  "scripts/operational-integrity.js",
141
139
  "scripts/oss-pr-opportunity-scout.js",
142
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.2">
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.2</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.2",
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.2</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,