thumbgate 1.29.2 → 1.31.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 (111) 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 +143 -14
  7. package/adapters/opencode/opencode.json +1 -1
  8. package/bench/observability-eval-suite.json +2 -2
  9. package/bin/cli.js +154 -36
  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/default.json +217 -50
  15. package/config/mcp-allowlists.json +215 -185
  16. package/config/model-tiers.json +7 -2
  17. package/config/post-deploy-marketing-pages.json +26 -1
  18. package/glama.json +6 -0
  19. package/package.json +94 -11
  20. package/public/architecture.html +130 -0
  21. package/public/assets/diagrams/agent-integration.png +0 -0
  22. package/public/assets/diagrams/before-after.svg +22 -0
  23. package/public/assets/diagrams/decision.svg +36 -0
  24. package/public/assets/diagrams/feedback-pipeline.png +0 -0
  25. package/public/assets/diagrams/hero-thumbs.svg +68 -0
  26. package/public/assets/diagrams/loop.svg +40 -0
  27. package/public/assets/diagrams/plugin-topology.png +0 -0
  28. package/public/assets/diagrams/pre-action-gate-loop.svg +59 -0
  29. package/public/assets/diagrams/self-improving-thumbs-loop.svg +105 -0
  30. package/public/assets/diagrams/stack.svg +18 -0
  31. package/public/assets/diagrams/thumbgate-architecture.png +0 -0
  32. package/public/case-studies.html +151 -0
  33. package/public/compare.html +1 -0
  34. package/public/dashboard.html +126 -28
  35. package/public/eval-scorecard.html +195 -0
  36. package/public/eval-scorecard.json +18 -0
  37. package/public/evaluations.html +168 -0
  38. package/public/index.html +143 -13
  39. package/public/numbers.html +3 -2
  40. package/public/pricing.html +143 -30
  41. package/public/whitepaper.html +189 -0
  42. package/scripts/a-plus-evidence-scorecard.js +303 -0
  43. package/scripts/activation-quickstart.js +1 -0
  44. package/scripts/agent-outcome-monitor.js +71 -1
  45. package/scripts/async-eval-observability.js +36 -11
  46. package/scripts/audit-trail.js +37 -1
  47. package/scripts/auto-promote-gates.js +149 -34
  48. package/scripts/billing.js +3 -1
  49. package/scripts/claude-feedback-sync.js +3 -2
  50. package/scripts/cli-feedback.js +13 -7
  51. package/scripts/colbert-style-maxsim.js +236 -0
  52. package/scripts/cross-encoder-reranker.js +359 -126
  53. package/scripts/dashboard-chat.js +350 -17
  54. package/scripts/document-intake.js +283 -7
  55. package/scripts/eval-quality-suite.js +204 -0
  56. package/scripts/feedback-aggregate.js +5 -2
  57. package/scripts/feedback-loop.js +359 -189
  58. package/scripts/feedback-paths.js +32 -13
  59. package/scripts/feedback-quality.js +53 -0
  60. package/scripts/filesystem-search.js +17 -7
  61. package/scripts/gates-engine.js +98 -4
  62. package/scripts/generate-case-study-outreach.js +253 -0
  63. package/scripts/generate-eval-scorecard.js +276 -0
  64. package/scripts/growth-campaigns.js +183 -0
  65. package/scripts/harness-tool-names.js +70 -0
  66. package/scripts/hook-runtime.js +10 -3
  67. package/scripts/jsonl-watcher.js +1 -0
  68. package/scripts/lesson-db.js +16 -5
  69. package/scripts/lesson-embedding-index.js +67 -20
  70. package/scripts/lesson-embedding-maintenance.js +177 -0
  71. package/scripts/lesson-inference.js +23 -4
  72. package/scripts/lesson-reranker.js +55 -9
  73. package/scripts/lesson-retrieval.js +375 -32
  74. package/scripts/lesson-search.js +48 -11
  75. package/scripts/llm-client.js +304 -15
  76. package/scripts/mcp-config.js +26 -5
  77. package/scripts/mcp-oauth.js +37 -2
  78. package/scripts/model-eval.js +308 -0
  79. package/scripts/model-tier-router.js +593 -0
  80. package/scripts/parallel-workflow-orchestrator.js +86 -22
  81. package/scripts/pragmatic-hybrid-search.js +379 -0
  82. package/scripts/published-cli.js +11 -1
  83. package/scripts/rag-document-pipeline.js +461 -0
  84. package/scripts/rag-structured-output.js +441 -0
  85. package/scripts/ragas-style-metrics.js +351 -0
  86. package/scripts/refresh-proof-pack.js +261 -0
  87. package/scripts/request-envelope.js +178 -0
  88. package/scripts/rerank-pipeline.js +370 -0
  89. package/scripts/rerank-quality-eval.js +155 -0
  90. package/scripts/retrieval-hybrid-ablation.js +120 -0
  91. package/scripts/retrieval-quality-tier.js +118 -0
  92. package/scripts/risk-scorer.js +144 -15
  93. package/scripts/secret-scanner.js +395 -4
  94. package/scripts/self-distill-agent.js +7 -1
  95. package/scripts/self-healing-check.js +25 -0
  96. package/scripts/skill-packs.js +183 -0
  97. package/scripts/slow-loop.js +72 -0
  98. package/scripts/statusline-links.js +1 -1
  99. package/scripts/statusline-local-stats.js +1 -1
  100. package/scripts/statusline.sh +8 -1
  101. package/scripts/telemetry-analytics.js +13 -1
  102. package/scripts/thumbgate-bench.js +13 -0
  103. package/scripts/thumbgate-search.js +98 -6
  104. package/scripts/tier-budget-guard.js +186 -0
  105. package/scripts/tool-kpi-tracker.js +124 -0
  106. package/scripts/tool-registry.js +95 -1
  107. package/scripts/vector-store.js +108 -4
  108. package/scripts/verify-marketing-pages-deployed.js +85 -3
  109. package/server.json +44 -0
  110. package/smithery.yaml +17 -0
  111. package/src/api/server.js +424 -99
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 {
@@ -1185,7 +1301,7 @@ function capture() {
1185
1301
  }
1186
1302
 
1187
1303
  if (args.summary) {
1188
- console.log(feedbackSummary(Number(args.recent || 20)));
1304
+ console.log(feedbackSummary(Number(args.recent || 20), { humanOnly: true }));
1189
1305
  return;
1190
1306
  }
1191
1307
 
@@ -1271,6 +1387,7 @@ function capture() {
1271
1387
  whatWorked: args['what-worked'],
1272
1388
  tags: args.tags,
1273
1389
  gateAction: gateAction || undefined,
1390
+ reviewOrigin: 'human',
1274
1391
  });
1275
1392
 
1276
1393
  if (result.accepted) {
@@ -1386,6 +1503,7 @@ function feedbackSelfTest() {
1386
1503
  ? (args['what-worked'] || 'Feedback capture persisted and was verified by a self-test')
1387
1504
  : undefined,
1388
1505
  tags: args.tags || 'self-test,dogfood,feedback-capture',
1506
+ reviewOrigin: 'automated',
1389
1507
  });
1390
1508
 
1391
1509
  const paths = getFeedbackPaths();
@@ -1444,7 +1562,7 @@ function stats() {
1444
1562
  trackEvent('cli_stats', { command: 'stats' });
1445
1563
  const args = parseArgs(process.argv.slice(3));
1446
1564
  const { analyzeFeedback } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
1447
- const data = analyzeFeedback();
1565
+ const data = analyzeFeedback(undefined, { humanOnly: true });
1448
1566
 
1449
1567
  // Gate enforcement stats — runtime intercepts + configured gates
1450
1568
  let gateData = { blocked: 0, warned: 0, passed: 0, byGate: {} };
@@ -1761,7 +1879,7 @@ function summary() {
1761
1879
  const args = parseArgs(process.argv.slice(3));
1762
1880
  const { feedbackSummary, analyzeFeedback } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
1763
1881
  if (args.json) {
1764
- const data = analyzeFeedback();
1882
+ const data = analyzeFeedback(undefined, { humanOnly: true });
1765
1883
  console.log(JSON.stringify({
1766
1884
  total: data.total,
1767
1885
  positives: data.totalPositive,
@@ -1771,7 +1889,7 @@ function summary() {
1771
1889
  }, null, 2));
1772
1890
  return;
1773
1891
  }
1774
- console.log(feedbackSummary(Number(args.recent || 20)));
1892
+ console.log(feedbackSummary(Number(args.recent || 20), { humanOnly: true }));
1775
1893
  }
1776
1894
 
1777
1895
  function lessons() {
@@ -2861,7 +2979,7 @@ function sessionStart() {
2861
2979
  } catch (_) { /* best-effort fallback sync */ }
2862
2980
  const { analyzeFeedback } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
2863
2981
  const { refreshStatuslineCache } = require(path.join(PKG_ROOT, 'scripts', 'hook-thumbgate-cache-updater'));
2864
- refreshStatuslineCache(analyzeFeedback());
2982
+ refreshStatuslineCache(analyzeFeedback(undefined, { humanOnly: true }));
2865
2983
 
2866
2984
  // Build a top-level <system-reminder> block that Claude Code's SessionStart
2867
2985
  // hook surfaces to the agent as first-class context — not buried stderr.
@@ -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
+ }