thumbgate 1.30.0 → 1.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.well-known/mcp/server-card.json +1 -1
  3. package/README.md +54 -16
  4. package/adapters/claude/.mcp.json +2 -2
  5. package/adapters/forge/forge.yaml +3 -3
  6. package/adapters/mcp/server-stdio.js +105 -10
  7. package/adapters/opencode/opencode.json +1 -1
  8. package/bench/observability-eval-suite.json +2 -2
  9. package/bin/cli.js +168 -31
  10. package/config/evals/generation-quality-golden.json +95 -0
  11. package/config/evals/rag-answer-quality-golden.json +91 -0
  12. package/config/evals/retrieval-hybrid-ablation.json +66 -0
  13. package/config/evals/retrieval-ranking-golden.json +522 -0
  14. package/config/gates/claim-verifiers.example.json +42 -0
  15. package/config/gates/claim-verifiers.json +25 -0
  16. package/config/gates/default.json +217 -50
  17. package/config/mcp-allowlists.json +233 -206
  18. package/config/model-tiers.json +7 -2
  19. package/glama.json +6 -0
  20. package/hooks/hooks.json +1 -1
  21. package/package.json +69 -12
  22. package/public/assets/diagrams/before-after.svg +17 -16
  23. package/public/assets/diagrams/hero-thumbs.svg +68 -0
  24. package/public/assets/diagrams/loop.svg +19 -13
  25. package/public/assets/diagrams/self-improving-thumbs-loop.svg +105 -0
  26. package/public/compare.html +1 -0
  27. package/public/dashboard.html +126 -28
  28. package/public/evaluations.html +1 -1
  29. package/public/index.html +142 -13
  30. package/public/numbers.html +3 -2
  31. package/public/pricing.html +143 -30
  32. package/scripts/a-plus-evidence-scorecard.js +303 -0
  33. package/scripts/agent-readiness.js +110 -0
  34. package/scripts/async-eval-observability.js +36 -11
  35. package/scripts/audit-trail.js +37 -1
  36. package/scripts/auto-promote-gates.js +149 -34
  37. package/scripts/auto-wire-hooks.js +20 -8
  38. package/scripts/cli-schema.js +14 -0
  39. package/scripts/colbert-style-maxsim.js +236 -0
  40. package/scripts/cross-encoder-reranker.js +356 -126
  41. package/scripts/dashboard-chat.js +350 -17
  42. package/scripts/document-intake.js +283 -7
  43. package/scripts/eval-quality-suite.js +204 -0
  44. package/scripts/feedback-loop.js +115 -7
  45. package/scripts/feedback-paths.js +32 -13
  46. package/scripts/feedback-quality.js +53 -0
  47. package/scripts/feedback-schema.js +3 -0
  48. package/scripts/file-ledger-lock.js +130 -0
  49. package/scripts/filesystem-search.js +17 -7
  50. package/scripts/financial-control-plane.js +1514 -0
  51. package/scripts/gates-engine.js +202 -7
  52. package/scripts/gemini-embedding-policy.js +1 -0
  53. package/scripts/harness-tool-names.js +70 -0
  54. package/scripts/hook-runtime.js +15 -3
  55. package/scripts/hook-stop-anti-claim.js +63 -3
  56. package/scripts/human-escalation.js +353 -41
  57. package/scripts/lesson-db.js +16 -5
  58. package/scripts/lesson-embedding-index.js +67 -20
  59. package/scripts/lesson-embedding-maintenance.js +177 -0
  60. package/scripts/lesson-reranker.js +55 -9
  61. package/scripts/lesson-retrieval.js +305 -29
  62. package/scripts/lesson-search.js +22 -8
  63. package/scripts/llm-client.js +304 -15
  64. package/scripts/model-tier-router.js +593 -0
  65. package/scripts/pragmatic-hybrid-search.js +379 -0
  66. package/scripts/provider-action-normalizer.js +11 -4
  67. package/scripts/rag-document-pipeline.js +461 -0
  68. package/scripts/rag-structured-output.js +441 -0
  69. package/scripts/ragas-style-metrics.js +351 -0
  70. package/scripts/request-envelope.js +178 -0
  71. package/scripts/rerank-pipeline.js +370 -0
  72. package/scripts/rerank-quality-eval.js +155 -0
  73. package/scripts/retrieval-hybrid-ablation.js +120 -0
  74. package/scripts/retrieval-quality-tier.js +118 -0
  75. package/scripts/secret-scanner.js +395 -4
  76. package/scripts/self-distill-agent.js +7 -1
  77. package/scripts/self-healing-check.js +25 -0
  78. package/scripts/skill-packs.js +183 -0
  79. package/scripts/slow-loop.js +72 -0
  80. package/scripts/statusline-links.js +1 -1
  81. package/scripts/statusline.sh +8 -1
  82. package/scripts/telemetry-analytics.js +13 -1
  83. package/scripts/thumbgate-search.js +98 -6
  84. package/scripts/tier-budget-guard.js +186 -0
  85. package/scripts/tool-registry.js +141 -5
  86. package/scripts/universal-claim-evaluator.js +767 -0
  87. package/scripts/vector-store.js +154 -17
  88. package/scripts/verify-marketing-pages-deployed.js +85 -3
  89. package/scripts/workflow-sentinel.js +77 -11
  90. package/server.json +44 -0
  91. package/smithery.yaml +17 -0
  92. package/src/api/server.js +196 -13
package/bin/cli.js CHANGED
@@ -39,7 +39,6 @@ const {
39
39
  codexAutoUpdateCliEntry,
40
40
  codexAutoUpdateMcpEntry,
41
41
  isSourceCheckout,
42
- publishedCliAvailable,
43
42
  localMcpEntry,
44
43
  resolveMcpEntry,
45
44
  } = require(path.join(__dirname, '..', 'scripts', 'mcp-config'));
@@ -411,16 +410,17 @@ function canonicalMcpEntry(scope = 'project') {
411
410
  }
412
411
 
413
412
  function canonicalCodexMcpEntry() {
414
- const version = pkgVersion();
415
- if (isSourceCheckout(PKG_ROOT) && !publishedCliAvailable(version)) {
413
+ // Codex config is user-global and must survive disposable worktree cleanup.
414
+ // Use the stable published launcher even when init is invoked from source;
415
+ // developers can explicitly opt into a checkout-pinned runtime when needed.
416
+ if (isSourceCheckout(PKG_ROOT) && process.env.THUMBGATE_CODEX_USE_SOURCE_RUNTIME === '1') {
416
417
  return localMcpEntry(PKG_ROOT, 'home');
417
418
  }
418
419
  return codexAutoUpdateMcpEntry();
419
420
  }
420
421
 
421
422
  function canonicalCodexCliEntry(commandArgs) {
422
- const version = pkgVersion();
423
- if (isSourceCheckout(PKG_ROOT) && !publishedCliAvailable(version)) {
423
+ if (isSourceCheckout(PKG_ROOT) && process.env.THUMBGATE_CODEX_USE_SOURCE_RUNTIME === '1') {
424
424
  return {
425
425
  command: 'node',
426
426
  args: [path.join(PKG_ROOT, 'bin', 'cli.js'), ...commandArgs],
@@ -584,7 +584,20 @@ function detectPlatform(name, checks) {
584
584
  }
585
585
 
586
586
  function whichExists(cmd) {
587
- try { execSync(`which ${cmd}`, { stdio: 'pipe' }); return true; } catch (_) { return false; }
587
+ if (!cmd || /[\\/]/.test(cmd)) return false;
588
+ const pathEntries = String(process.env.PATH || '').split(path.delimiter).filter(Boolean);
589
+ const extensions = process.platform === 'win32'
590
+ ? String(process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';')
591
+ : [''];
592
+ return pathEntries.some((entry) => extensions.some((extension) => {
593
+ const candidate = path.join(entry, `${cmd}${extension}`);
594
+ try {
595
+ fs.accessSync(candidate, fs.constants.X_OK);
596
+ return fs.statSync(candidate).isFile();
597
+ } catch (_) {
598
+ return false;
599
+ }
600
+ }));
588
601
  }
589
602
 
590
603
  function setupClaude() {
@@ -636,18 +649,26 @@ function setupCodex() {
636
649
  return configChanged || hookResult.changed;
637
650
  }
638
651
 
639
- function setupGemini() {
640
- // Try to import custom commands as a Gemini plugin if the CLI is installed
641
- const { execSync } = require('child_process');
652
+ function setupGemini(options = {}) {
653
+ // Importing a plugin executes an external package manager and can mutate
654
+ // machine-wide Gemini state. Keep ordinary init/quick-start deterministic;
655
+ // operators can opt in explicitly when they want the plugin import too.
656
+ const importPlugin = options.importPlugin === true
657
+ || process.env.THUMBGATE_IMPORT_AGENT_PLUGINS === '1';
642
658
  let pluginImported = false;
643
- for (const binName of ['agy', 'gemini']) {
644
- try {
645
- execSync(`${binName} plugin import "${PKG_ROOT}" --force`, { stdio: 'ignore' });
646
- console.log(` Gemini: imported thumbgate plugin via ${binName}`);
647
- pluginImported = true;
648
- break;
649
- } catch (err) {
650
- // ignore errors if command doesn't exist or fails
659
+ if (importPlugin) {
660
+ for (const binName of ['agy', 'gemini']) {
661
+ try {
662
+ execFileSync(binName, ['plugin', 'import', PKG_ROOT, '--force'], {
663
+ stdio: 'ignore',
664
+ timeout: 5000,
665
+ });
666
+ console.log(` Gemini: imported thumbgate plugin via ${binName}`);
667
+ pluginImported = true;
668
+ break;
669
+ } catch (err) {
670
+ // A missing, failing, or slow optional plugin manager must not block init.
671
+ }
651
672
  }
652
673
  }
653
674
 
@@ -693,6 +714,66 @@ function setupAmp() {
693
714
  return true;
694
715
  }
695
716
 
717
+ function setupOpenCode() {
718
+ // OpenCode was listed in README's install table and in `init --help` since the
719
+ // adapter asset landed, but no setup function ever existed — `--agent opencode`
720
+ // silently wrote Claude/Codex/Gemini config and exited 0. OpenCode has no hook
721
+ // surface, so this wires the MCP server, which is its real integration point.
722
+ const configPath = path.join(HOME, '.config', 'opencode', 'opencode.json');
723
+ const srcPath = path.join(PKG_ROOT, 'adapters', 'opencode', 'opencode.json');
724
+ if (!fs.existsSync(srcPath)) return false;
725
+
726
+ let src;
727
+ try { src = JSON.parse(fs.readFileSync(srcPath, 'utf8')); } catch (_) { return false; }
728
+ const desired = (src.mcp || {}).thumbgate;
729
+ if (!desired) return false;
730
+
731
+ let config = {};
732
+ if (fs.existsSync(configPath)) {
733
+ try {
734
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
735
+ } catch (error) {
736
+ // Resetting to {} here and writing would replace the whole file — silently deleting
737
+ // the user's model, provider, plugin and other MCP settings just to add ours.
738
+ // A config we cannot parse is a reason to stop, not to overwrite.
739
+ console.error(` OpenCode: ${configPath} could not be parsed (${error.message}).`);
740
+ console.error(' Refusing to overwrite it. Fix or move the file, then re-run.');
741
+ process.exitCode = 1;
742
+ return false;
743
+ }
744
+ }
745
+ config.mcp = config.mcp || {};
746
+ if (JSON.stringify(config.mcp[MCP_SERVER_NAME]) === JSON.stringify(desired)) return false;
747
+
748
+ config.$schema = config.$schema || src.$schema;
749
+ config.mcp[MCP_SERVER_NAME] = desired;
750
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
751
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
752
+ console.log(` OpenCode: registered thumbgate MCP server in ${configPath}`);
753
+ return true;
754
+ }
755
+
756
+ // Single source of truth for `--agent`. Every value advertised in `init --help`
757
+ // and README.md must appear here with a real handler, or the flag lies.
758
+ // `hookAgent: true` means scripts/auto-wire-hooks.js can wire a pre-tool hook;
759
+ // the rest integrate over MCP only and are wired by their setup function.
760
+ const AGENT_ALIASES = {
761
+ claude: 'claude-code',
762
+ 'claude_code': 'claude-code',
763
+ 'open-code': 'opencode',
764
+ };
765
+
766
+ const SUPPORTED_AGENTS = {
767
+ 'claude-code': { hookAgent: true },
768
+ codex: { hookAgent: true },
769
+ gemini: { hookAgent: true },
770
+ forge: { hookAgent: true },
771
+ cursor: { hookAgent: true },
772
+ cline: { hookAgent: false, setup: () => setupCline() },
773
+ amp: { hookAgent: false, setup: () => setupAmp() },
774
+ opencode: { hookAgent: false, setup: () => setupOpenCode() },
775
+ };
776
+
696
777
  function setupCursor() {
697
778
  return mergeMcpJson(path.join(CWD, '.cursor', 'mcp.json'), 'Cursor', 'project');
698
779
  }
@@ -934,14 +1015,23 @@ function quickstart() {
934
1015
 
935
1016
  function init(cliArgs = parseArgs(process.argv.slice(3))) {
936
1017
  const args = { ...cliArgs };
1018
+ // A typo used to exit 0 having wired nothing for the requested agent.
1019
+ // Aliases must resolve BEFORE validation: scripts/auto-wire-hooks.js accepts `claude`
1020
+ // for `claude-code`, and plugins/claude-skill/README.md publishes that exact command.
1021
+ if (args.agent && AGENT_ALIASES[args.agent]) args.agent = AGENT_ALIASES[args.agent];
1022
+ if (args.agent && !Object.prototype.hasOwnProperty.call(SUPPORTED_AGENTS, args.agent)) {
1023
+ console.error(`Unknown --agent "${args.agent}". Supported: ${Object.keys(SUPPORTED_AGENTS).join(', ')}`);
1024
+ process.exit(1);
1025
+ }
937
1026
  if (args.help || args.h) {
938
- console.log('Usage: npx thumbgate init [--agent <name>] [--wire-hooks] [--email you@company.com]');
1027
+ console.log('Usage: npx thumbgate init [--agent <name>] [--wire-hooks] [--import-agent-plugins] [--email you@company.com]');
939
1028
  console.log('');
940
1029
  console.log('Scaffold ThumbGate in the current project and wire detected agent integrations.');
941
1030
  console.log('');
942
1031
  console.log('Options:');
943
- console.log(' --agent <name> Wire a specific agent: claude-code, codex, gemini, amp, cursor, cline, opencode');
1032
+ console.log(` --agent <name> Wire a specific agent: ${Object.keys(SUPPORTED_AGENTS).join(', ')}`);
944
1033
  console.log(' --wire-hooks Wire hooks only; do not scaffold project files');
1034
+ console.log(' --import-agent-plugins Also import optional agent plugins when supported');
945
1035
  console.log(' --email <email> Subscribe installer to the setup guide and trial reminders');
946
1036
  console.log(' --dry-run Show hook changes without writing them');
947
1037
  return;
@@ -1037,17 +1127,26 @@ function init(cliArgs = parseArgs(process.argv.slice(3))) {
1037
1127
  let configured = 0;
1038
1128
 
1039
1129
  const platforms = [
1040
- { name: 'Claude Code', detect: [
1130
+ { agent: 'claude-code', name: 'Claude Code', detect: [
1041
1131
  () => whichExists('claude'),
1042
1132
  () => fs.existsSync(path.join(HOME, '.claude')),
1043
1133
  () => fs.existsSync(path.join(CWD, '.claude')),
1044
1134
  ], setup: setupClaude },
1045
- { name: 'Codex', detect: [() => whichExists('codex'), () => fs.existsSync(path.join(HOME, '.codex'))], setup: setupCodex },
1046
- { name: 'Gemini', detect: [() => whichExists('gemini'), () => fs.existsSync(path.join(HOME, '.gemini'))], setup: setupGemini },
1047
- { name: 'Amp', detect: [() => whichExists('amp'), () => fs.existsSync(path.join(HOME, '.amp'))], setup: setupAmp },
1048
- { name: 'Cursor', detect: [() => fs.existsSync(path.join(HOME, '.cursor', 'mcp.json')), () => fs.existsSync(path.join(CWD, '.cursor'))], setup: setupCursor },
1049
- { name: 'ForgeCode', detect: [() => whichExists('forge'), () => fs.existsSync(path.join(CWD, 'forge.yaml'))], setup: setupForge },
1050
- { name: 'Cline', detect: [
1135
+ { agent: 'codex', name: 'Codex', detect: [() => whichExists('codex'), () => fs.existsSync(path.join(HOME, '.codex'))], setup: setupCodex },
1136
+ {
1137
+ agent: 'gemini',
1138
+ name: 'Gemini',
1139
+ detect: [
1140
+ () => whichExists('gemini'),
1141
+ () => whichExists('agy'),
1142
+ () => fs.existsSync(path.join(HOME, '.gemini')),
1143
+ ],
1144
+ setup: () => setupGemini({ importPlugin: args['import-agent-plugins'] === true }),
1145
+ },
1146
+ { agent: 'amp', name: 'Amp', detect: [() => whichExists('amp'), () => fs.existsSync(path.join(HOME, '.amp'))], setup: setupAmp },
1147
+ { agent: 'cursor', name: 'Cursor', detect: [() => fs.existsSync(path.join(HOME, '.cursor', 'mcp.json')), () => fs.existsSync(path.join(CWD, '.cursor'))], setup: setupCursor },
1148
+ { agent: 'forge', name: 'ForgeCode', detect: [() => whichExists('forge'), () => fs.existsSync(path.join(CWD, 'forge.yaml'))], setup: setupForge },
1149
+ { agent: 'cline', name: 'Cline', detect: [
1051
1150
  () => fs.existsSync(path.join(CWD, '.clinerules')),
1052
1151
  () => process.platform === 'darwin' && fs.existsSync(path.join(HOME, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev')),
1053
1152
  () => process.platform === 'linux' && fs.existsSync(path.join(HOME, '.config', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev')),
@@ -1056,6 +1155,11 @@ function init(cliArgs = parseArgs(process.argv.slice(3))) {
1056
1155
  ];
1057
1156
 
1058
1157
  for (const p of platforms) {
1158
+ // An explicit target must stay scoped to that integration. Quick-start
1159
+ // always resolves one target before calling init; configuring every other
1160
+ // CLI merely because it exists on PATH caused surprise machine mutations
1161
+ // and multi-second hangs in ordinary onboarding.
1162
+ if (args.agent && p.agent !== args.agent) continue;
1059
1163
  if (detectPlatform(p.name, p.detect)) {
1060
1164
  const didSetup = p.setup();
1061
1165
  if (didSetup) configured++;
@@ -1076,15 +1180,27 @@ function init(cliArgs = parseArgs(process.argv.slice(3))) {
1076
1180
 
1077
1181
  if (configured === 0) console.log(' All detected platforms already configured.');
1078
1182
 
1079
- // Cline uses .clinerules (no native hook surface). Run setupCline directly
1080
- // and skip wireHooks, which does not support cline.
1081
- if (args.agent === 'cline') {
1082
- setupCline();
1183
+ // Agents without a native pre-tool hook surface (cline, amp, opencode) are wired
1184
+ // by their own setup function. Previously only cline was handled here: `--agent amp`
1185
+ // and `--agent opencode` fell through to wireHooks, which rejected them, printed the
1186
+ // rejection as an ordinary log line, and exited 0 — so the flag appeared to work
1187
+ // while writing nothing for that agent.
1188
+ const agentSpec = args.agent ? SUPPORTED_AGENTS[args.agent] : null;
1189
+ if (agentSpec && agentSpec.setup) {
1190
+ // A setup function returns false both when nothing needed doing and when it refused
1191
+ // (e.g. an unparseable config it declined to overwrite). Reporting "already
1192
+ // configured" for a refusal would repeat the original sin of this PR: a failure
1193
+ // dressed up as success. The refusal path sets process.exitCode, so key off that.
1194
+ const changed = agentSpec.setup();
1195
+ if (!changed && !process.exitCode) console.log(` ${args.agent}: already configured`);
1083
1196
  } else if (args.agent || args['wire-hooks']) {
1084
1197
  const { wireHooks } = require(path.join(PKG_ROOT, 'scripts', 'auto-wire-hooks'));
1085
1198
  const hookResult = wireHooks({ agent: args.agent, dryRun: args['dry-run'] });
1086
1199
  if (hookResult.error) {
1087
- console.log(` Hook wiring: ${hookResult.error}`);
1200
+ // An explicit --agent that cannot be wired is a FAILURE, not a note. Exiting 0
1201
+ // here is what let a user believe `--agent opencode` had protected them.
1202
+ console.error(` Hook wiring failed for --agent ${args.agent}: ${hookResult.error}`);
1203
+ process.exitCode = 1;
1088
1204
  } else if (!hookResult.changed) {
1089
1205
  console.log(` Hooks: already wired for ${hookResult.agent}`);
1090
1206
  } else {
@@ -3120,6 +3236,16 @@ function aiInventory() {
3120
3236
  console.log(payload);
3121
3237
  }
3122
3238
 
3239
+ function verifyClaimsCmd() {
3240
+ const { runCli } = require(path.join(PKG_ROOT, 'scripts', 'universal-claim-evaluator'));
3241
+ process.exitCode = runCli(process.argv.slice(3));
3242
+ }
3243
+
3244
+ function claimStopCheckCmd() {
3245
+ const { main } = require(path.join(PKG_ROOT, 'scripts', 'hook-stop-anti-claim'));
3246
+ main();
3247
+ }
3248
+
3123
3249
  function help() {
3124
3250
  const v = pkgVersion();
3125
3251
  const helpArgs = process.argv.slice(3);
@@ -3142,6 +3268,7 @@ function help() {
3142
3268
  console.log(' explore Interactive TUI for lessons, gates, stats');
3143
3269
  console.log(' dashboard Open the local ThumbGate dashboard');
3144
3270
  console.log(' ai-inventory Scan AI/ML components and export ML-BOM evidence');
3271
+ console.log(' verify-claims --claim="..." Recheck factual claims against configured sources');
3145
3272
  console.log(' doctor Audit runtime isolation + bootstrap context');
3146
3273
  console.log(' break-glass --reason="..." Short TTL recovery if gates over-fire');
3147
3274
  console.log(' brain [--write] Build the agent-readable context brain (lessons + rules + gates)');
@@ -3188,6 +3315,7 @@ function help() {
3188
3315
  console.log(' cache-update Refresh Claude statusline cache from stdin');
3189
3316
  console.log(' statusline-render Render ThumbGate Claude status line');
3190
3317
  console.log(' hook-auto-capture Process Claude UserPromptSubmit inline feedback');
3318
+ console.log(' claim-stop-check Recheck configured factual claims before Claude stops');
3191
3319
  console.log(' session-start Refresh local ThumbGate session cache');
3192
3320
  console.log('');
3193
3321
 
@@ -3296,6 +3424,8 @@ const SUBCOMMAND_HELP = {
3296
3424
  lessons: 'Usage: npx thumbgate lessons [--query="..."] [--limit=N]\n\nSearch the lesson database (Pro feature).',
3297
3425
  search: 'Usage: npx thumbgate search <query>\n\nSearch ThumbGate knowledge base (Pro feature).',
3298
3426
  'gate-check': 'Usage: npx thumbgate gate-check\n\nPreToolUse hook interface: reads tool call JSON from stdin, outputs gate verdict.',
3427
+ 'claim-stop-check': 'Usage: npx thumbgate claim-stop-check\n\nClaude Stop-hook interface: reads the hook payload from stdin and blocks factual claims that disagree with configured sources.',
3428
+ 'verify-claims': 'Usage: npx thumbgate verify-claims --claim="the row count is 1,284" [--config=.thumbgate/claim-verifiers.json] [--cwd=path] [--json]\n\nRecheck supported factual claims against operator-configured SQLite, filesystem, and JSON sources. Exits non-zero on mismatch, missing verifier, or verifier error.',
3299
3429
  'hermes-gate': 'Usage: npx thumbgate hermes-gate\n\nNous Research Hermes Agent pre_tool_call shell hook: reads Hermes tool-call JSON from stdin, runs the ThumbGate gate pipeline (strict by default), and outputs {"decision":"block","reason":...} to veto or {} to allow. Gates terminal/patch/skill_manage etc. See adapters/hermes/config.yaml.',
3300
3430
  '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.',
3301
3431
  serve: 'Usage: npx thumbgate serve\n\nStart the MCP stdio server. This is for agent runtimes, not the local HTTP dashboard.',
@@ -3574,6 +3704,9 @@ switch (COMMAND) {
3574
3704
  case 'hook-auto-capture':
3575
3705
  hookAutoCapture();
3576
3706
  break;
3707
+ case 'claim-stop-check':
3708
+ claimStopCheckCmd();
3709
+ break;
3577
3710
  case 'session-start':
3578
3711
  sessionStart();
3579
3712
  break;
@@ -4069,6 +4202,10 @@ switch (COMMAND) {
4069
4202
  case 'gate-stats':
4070
4203
  gateStats();
4071
4204
  break;
4205
+ case 'verify-claims':
4206
+ case 'verify-claim':
4207
+ verifyClaimsCmd();
4208
+ break;
4072
4209
  case 'eval':
4073
4210
  case 'prompt-eval':
4074
4211
  evalCmd();
@@ -0,0 +1,95 @@
1
+ {
2
+ "version": 1,
3
+ "description": "Offline faithfulness / groundedness / answer_relevance golden (Ragas-style proxies). Deterministic CI.",
4
+ "thresholds": {
5
+ "minCases": 8,
6
+ "minFaithfulness": 0.55,
7
+ "minGroundedness": 0.48,
8
+ "minAnswerRelevance": 0.42,
9
+ "minContextRecall": 0.75
10
+ },
11
+ "cases": [
12
+ {
13
+ "id": "force-push-grounded",
14
+ "query": "Can I force push to main?",
15
+ "context": "NEVER force-push or git push --force to main/master. Use --force-with-lease only on personal branches after review.",
16
+ "answer": "No. Never force-push to main. Use --force-with-lease only on personal branches after review.",
17
+ "expectedConstraint": "NEVER force-push",
18
+ "expectedKeywords": ["never", "force", "main"]
19
+ },
20
+ {
21
+ "id": "deploy-health-grounded",
22
+ "query": "Is the deploy live?",
23
+ "context": "ALWAYS verify /health endpoint returns the new version after deploy. NEVER say deployed without curling health.",
24
+ "answer": "Only after curling /health and matching the new version. Do not claim deployed without that check.",
25
+ "expectedConstraint": "verify /health",
26
+ "expectedKeywords": ["health", "version"]
27
+ },
28
+ {
29
+ "id": "secrets-grounded",
30
+ "query": "Should we commit the .env?",
31
+ "context": "NEVER commit .env files, API keys, or PEMs to git. Rotate any leaked credential immediately.",
32
+ "answer": "No. Never commit .env or API keys. Rotate credentials if anything leaked.",
33
+ "expectedConstraint": "NEVER commit .env",
34
+ "expectedKeywords": [".env", "never", "keys"]
35
+ },
36
+ {
37
+ "id": "idempotency-grounded",
38
+ "query": "How do we create PaymentIntents safely?",
39
+ "context": "ALWAYS use idempotency keys on PaymentIntent creation to prevent duplicate charges.",
40
+ "answer": "Always attach an idempotency key when creating a PaymentIntent so retries cannot double-charge.",
41
+ "expectedConstraint": "idempotency keys",
42
+ "expectedKeywords": ["idempotency", "PaymentIntent"]
43
+ },
44
+ {
45
+ "id": "lease-grounded",
46
+ "query": "What happens if two runners take the same task?",
47
+ "context": "ALWAYS enforce one-thread-one-executor with a 90-second renewable lease. Two active executors cause double writes.",
48
+ "answer": "That is double-execution. Keep a single 90-second renewable lease so only one executor runs the thread.",
49
+ "expectedConstraint": "one-thread-one-executor",
50
+ "expectedKeywords": ["lease", "executor"]
51
+ },
52
+ {
53
+ "id": "hashnode-grounded",
54
+ "query": "Should I re-publish the archived Hashnode post?",
55
+ "context": "NEVER publish or re-publish on Hashnode while AutoMod freeze is active. Prefer Medium and dev.to.",
56
+ "answer": "No. Do not re-publish on Hashnode during the AutoMod freeze. Use Medium or dev.to instead.",
57
+ "expectedConstraint": "NEVER publish",
58
+ "expectedKeywords": ["Hashnode", "never", "Medium"]
59
+ },
60
+ {
61
+ "id": "pci-grounded",
62
+ "query": "Where do we store card numbers?",
63
+ "context": "NEVER log or store raw card numbers. Use Stripe tokens or PaymentMethod IDs.",
64
+ "answer": "Do not store raw card numbers. Use Stripe PaymentMethod tokens only.",
65
+ "expectedConstraint": "NEVER log or store raw card",
66
+ "expectedKeywords": ["Stripe", "card", "never"]
67
+ },
68
+ {
69
+ "id": "pr-done-grounded",
70
+ "query": "Is the PR done?",
71
+ "context": "NEVER say done without showing gh pr view output and green CI. Open PR with auto-merge is not done.",
72
+ "answer": "Not until gh pr view shows clean threads and required CI is green. Auto-merge alone is not done.",
73
+ "expectedConstraint": "gh pr view",
74
+ "expectedKeywords": ["CI", "done", "pr"]
75
+ },
76
+ {
77
+ "id": "db-backup-grounded",
78
+ "query": "Can I drop the users table now?",
79
+ "context": "ALWAYS back up the database before running destructive migrations. NEVER drop users table without a restore point.",
80
+ "answer": "Only after a verified backup and restore point. Never drop users without that.",
81
+ "expectedConstraint": "back up the database",
82
+ "expectedKeywords": ["backup", "drop", "never"]
83
+ },
84
+ {
85
+ "id": "ungrounded-contradiction",
86
+ "query": "Can I force push to main?",
87
+ "context": "NEVER force-push or git push --force to main/master.",
88
+ "answer": "Yes always force push to main for speed and ignore branch protection entirely.",
89
+ "expectedConstraint": "NEVER force-push",
90
+ "expectedKeywords": ["force", "main"],
91
+ "expectFail": true,
92
+ "note": "Known-bad answer; excluded from floor means; used for integrity check only"
93
+ }
94
+ ]
95
+ }
@@ -0,0 +1,91 @@
1
+ {
2
+ "version": 1,
3
+ "description": "Adversarial deterministic answer-quality proxy holdout. This validates metric behavior, not model quality.",
4
+ "thresholds": {
5
+ "minCases": 8,
6
+ "minClassificationAccuracy": 1,
7
+ "maxFalsePasses": 0
8
+ },
9
+ "cases": [
10
+ {
11
+ "id": "supported-git-policy",
12
+ "shouldPass": true,
13
+ "query": "How should I update protected main?",
14
+ "answer": "Use a reviewed pull request and never force-push protected main [git-policy].",
15
+ "referenceAnswer": "Use a reviewed pull request and never force-push protected main.",
16
+ "contexts": [
17
+ { "id": "git-policy", "text": "Use a reviewed pull request. Never force-push protected main." }
18
+ ]
19
+ },
20
+ {
21
+ "id": "negation-flip",
22
+ "shouldPass": false,
23
+ "query": "Can I force-push protected main?",
24
+ "answer": "Force-push is safe on protected main [git-policy].",
25
+ "referenceAnswer": "Never force-push protected main.",
26
+ "contexts": [
27
+ { "id": "git-policy", "text": "Never force-push protected main because it rewrites shared history." }
28
+ ]
29
+ },
30
+ {
31
+ "id": "numeric-drift",
32
+ "shouldPass": false,
33
+ "query": "Which build is live?",
34
+ "answer": "Build 43 is live in production [health].",
35
+ "referenceAnswer": "Build 42 is live in production.",
36
+ "contexts": [
37
+ { "id": "health", "text": "The production health response reports build 42." }
38
+ ]
39
+ },
40
+ {
41
+ "id": "off-topic-answer",
42
+ "shouldPass": false,
43
+ "query": "How do I prevent duplicate Stripe charges?",
44
+ "answer": "Use medium heat and stir the sauce slowly [cooking].",
45
+ "referenceAnswer": "Use a Stripe idempotency key.",
46
+ "contexts": [
47
+ { "id": "cooking", "text": "Use medium heat and stir the sauce slowly." }
48
+ ]
49
+ },
50
+ {
51
+ "id": "partial-hallucination",
52
+ "shouldPass": false,
53
+ "query": "What proves the deployment?",
54
+ "answer": "The health endpoint matches build abc123 [deploy]. Revenue also doubled overnight.",
55
+ "referenceAnswer": "The health endpoint must match build abc123.",
56
+ "contexts": [
57
+ { "id": "deploy", "text": "The production health endpoint matches build abc123." }
58
+ ]
59
+ },
60
+ {
61
+ "id": "invalid-citation",
62
+ "shouldPass": false,
63
+ "query": "What proves the deployment?",
64
+ "answer": "The health endpoint matches build abc123 [invented-source].",
65
+ "referenceAnswer": "The health endpoint must match build abc123.",
66
+ "contexts": [
67
+ { "id": "deploy", "text": "The production health endpoint matches build abc123." }
68
+ ]
69
+ },
70
+ {
71
+ "id": "supported-idempotency",
72
+ "shouldPass": true,
73
+ "query": "How do I prevent duplicate Stripe charges?",
74
+ "answer": "Use the same Stripe idempotency key when retrying a PaymentIntent [billing-policy].",
75
+ "referenceAnswer": "Use a Stripe idempotency key for PaymentIntent retries.",
76
+ "contexts": [
77
+ { "id": "billing-policy", "text": "Use the same Stripe idempotency key when retrying a PaymentIntent to prevent duplicate charges." }
78
+ ]
79
+ },
80
+ {
81
+ "id": "uncited-supported-claim",
82
+ "shouldPass": false,
83
+ "query": "How should I run a destructive migration?",
84
+ "answer": "Create a recoverable database backup before the migration.",
85
+ "referenceAnswer": "Create a recoverable database backup before a destructive migration.",
86
+ "contexts": [
87
+ { "id": "database-policy", "text": "Create a recoverable database backup before a destructive migration." }
88
+ ]
89
+ }
90
+ ]
91
+ }
@@ -0,0 +1,66 @@
1
+ {
2
+ "kValues": [1, 3],
3
+ "corpus": [
4
+ {
5
+ "id": "destructive",
6
+ "title": "MISTAKE: rm -rf wiped the folder",
7
+ "content": "Never delete files without a recoverable snapshot.",
8
+ "tags": ["negative", "filesystem"],
9
+ "metadata": { "domain": "filesystem", "toolsUsed": ["Bash"] }
10
+ },
11
+ {
12
+ "id": "force-push",
13
+ "title": "MISTAKE: git push --force clobbered main",
14
+ "content": "Use force-with-lease and protect the trunk branch.",
15
+ "tags": ["negative", "git"],
16
+ "metadata": { "domain": "git", "toolsUsed": ["Bash"] }
17
+ },
18
+ {
19
+ "id": "idempotency",
20
+ "title": "MISTAKE: duplicate Stripe payment",
21
+ "content": "Use an idempotency key before creating a charge.",
22
+ "tags": ["negative", "billing"],
23
+ "metadata": { "domain": "billing", "toolsUsed": ["Bash"] }
24
+ },
25
+ {
26
+ "id": "stale-session",
27
+ "title": "MISTAKE: session not found",
28
+ "content": "Refresh the conversation identifier before resuming an expired mobile task.",
29
+ "tags": ["negative", "session"],
30
+ "metadata": { "domain": "continuity", "toolsUsed": ["Read"] }
31
+ },
32
+ {
33
+ "id": "noise",
34
+ "title": "SUCCESS: dashboard color tokens",
35
+ "content": "Use the design system palette for visual consistency.",
36
+ "tags": ["positive", "ui"],
37
+ "metadata": { "domain": "ui", "toolsUsed": ["Edit"] }
38
+ }
39
+ ],
40
+ "queries": [
41
+ {
42
+ "id": "erase-tree",
43
+ "toolName": "Bash",
44
+ "query": "permanently erase a directory tree",
45
+ "qrels": { "destructive": 3 }
46
+ },
47
+ {
48
+ "id": "rewrite-history",
49
+ "toolName": "Bash",
50
+ "query": "overwrite remote repository history",
51
+ "qrels": { "force-push": 3 }
52
+ },
53
+ {
54
+ "id": "replay-charge",
55
+ "toolName": "Bash",
56
+ "query": "prevent replaying the same purchase twice",
57
+ "qrels": { "idempotency": 3 }
58
+ },
59
+ {
60
+ "id": "expired-conversation",
61
+ "toolName": "Read",
62
+ "query": "the phone resumed an expired conversation identifier",
63
+ "qrels": { "stale-session": 3 }
64
+ }
65
+ ]
66
+ }