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
@@ -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.29.2",
4
+ "version": "1.31.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.29.2",
3
+ "version": "1.31.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",
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # ThumbGate
2
2
 
3
+ [![MCP Toplist](https://mcptoplist.com/badge/glama%2FIgorGanapolsky%2FThumbGate.svg)](https://mcptoplist.com/server/glama%2FIgorGanapolsky%2FThumbGate)
4
+
3
5
  <p align="center">
4
6
  <a href="https://thumbgate.ai">
5
7
  <img src="public/assets/brand/thumbgate-icon-512.png" alt="ThumbGate" width="128" height="128" />
@@ -28,6 +30,19 @@ Accepted feedback is stored as local lessons. Repeated concrete failures can bec
28
30
  npx thumbgate init # auto-detects the supported agent and wires its integration
29
31
  ```
30
32
 
33
+ ### MCP / Glama / registry install (stdio)
34
+
35
+ Directories and clients that install ThumbGate as an MCP server must start **stdio MCP**, not the HTTP API:
36
+
37
+ ```bash
38
+ npx -y thumbgate serve
39
+ ```
40
+
41
+ - Equivalent: `npx -y thumbgate mcp`
42
+ - Do **not** use `npm start` for MCP — that launches the hosted HTTP API (`src/api/server.js`), not the agent-facing stdio server.
43
+ - Canonical package metadata: `server.json` (`runtimeHint: npx` + `packageArguments: ["serve"]`), Smithery: `smithery.yaml`, maintainers: `glama.json`.
44
+ - Product name is **ThumbGate** only (npm: `thumbgate`). Retired legacy package aliases are not active product surfaces.
45
+
31
46
  Works with **Claude Code, Cursor, Codex, Gemini CLI, Amp, Cline, OpenCode** and MCP-compatible agents after their integration is configured. Free tier: 2 feedback captures/day (10 total) and up to 3 active auto-promoted prevention rules. [Pro: $19/mo or $149/yr](https://thumbgate.ai/checkout/pro?utm_source=github&utm_medium=readme) is the individual tier for unlimited rules, history-aware lessons, feedback sessions, a personal dashboard, and DPO export. Enterprise is custom and scoped after intake; hosted team sync and a hosted org dashboard are not in the current general-availability runtime.
32
47
 
33
48
  [![CI](https://github.com/IgorGanapolsky/ThumbGate/actions/workflows/ci.yml/badge.svg)](https://github.com/IgorGanapolsky/ThumbGate/actions/workflows/ci.yml)
@@ -256,6 +271,11 @@ evaluation/observability—is maintained in
256
271
  architecture exists, what can fail, how it is measured and secured, how it is
257
272
  deployed, and what evidence is required before claiming it works.
258
273
 
274
+ The full framework decision and one end-to-end RAG request—tenant authorization,
275
+ query transformation, parent-child retrieval, hybrid fusion, reranking,
276
+ structured generation, evaluation, and tracing—are documented in
277
+ [`docs/RAG_PRODUCTION_ARCHITECTURE.md`](./docs/RAG_PRODUCTION_ARCHITECTURE.md).
278
+
259
279
  ### Retrieval & latency: local-first, zero network hops
260
280
 
261
281
  ThumbGate's latency advantage is structural, not a tuned cloud cluster: there is no retrieval service and no model on the enforcement path, so the gate decision never leaves your machine.
@@ -302,22 +322,40 @@ Each recommendation ships with the benchmark commands to run next: feedback-deri
302
322
 
303
323
  ## Install for Your Agent
304
324
 
305
- | Agent | Command |
306
- |-------|---------|
307
- | **Claude Code** | `npx thumbgate init --agent claude-code` |
308
- | **Cursor** | `npx thumbgate init --agent cursor` |
309
- | **VS Code / Open VSX** | [plugins/vscode-extension/README.md](plugins/vscode-extension/README.md) |
310
- | **Antigravity-compatible** | [plugins/antigravity-extension/INSTALL.md](plugins/antigravity-extension/INSTALL.md) |
311
- | **JetBrains** | [plugins/jetbrains-plugin/README.md](plugins/jetbrains-plugin/README.md) |
312
- | **Codex** | `npx thumbgate init --agent codex` |
313
- | **Gemini CLI** | `npx thumbgate init --agent gemini` |
314
- | **Amp** | `npx thumbgate init --agent amp` |
315
- | **Cline** (Roo Code successor) | `npx thumbgate init --agent cline` |
316
- | **OpenCode** | `npx thumbgate init --agent opencode` |
317
- | **Claude Desktop** | [Download extension bundle](https://github.com/IgorGanapolsky/ThumbGate/releases/latest/download/thumbgate-claude-desktop.mcpb) |
318
- | **Any MCP agent** | `npx thumbgate serve` |
319
-
320
- Works with **Claude Code, Cursor, Codex, Gemini CLI, Amp, Cline, OpenCode**, and any MCP-compatible agent. Migrating from Roo Code (sunsetting 2026-05-15)? See [`adapters/cline/INSTALL.md`](./adapters/cline/INSTALL.md).
325
+ Enforcement depends on what the harness lets us intercept, so the table says which
326
+ you get. This distinction is real: with a pre-tool hook ThumbGate stops the action
327
+ before it runs; over MCP only, ThumbGate answers `gate_check` and the agent decides
328
+ whether to obey.
329
+
330
+ | Agent | Command | Enforcement |
331
+ |-------|---------|-------------|
332
+ | **Claude Code** | `npx thumbgate init --agent claude-code` | Hard — PreToolUse hook |
333
+ | **Codex** | `npx thumbgate init --agent codex` | Hard — `pre_tool_use` hook |
334
+ | **Gemini CLI** | `npx thumbgate init --agent gemini` | Hard — PreToolUse hook |
335
+ | **ForgeCode** | `npx thumbgate init --agent forge` | Hard — `pre_tool_use` trigger |
336
+ | **Cursor** | `npx thumbgate init --agent cursor` | Advisory — MCP `gate_check` |
337
+ | **Cline** (Roo Code successor) | `npx thumbgate init --agent cline` | Advisory — MCP `gate_check` + `.clinerules` |
338
+ | **OpenCode** | `npx thumbgate init --agent opencode` | Advisory — MCP `gate_check` |
339
+ | **Any MCP agent** | `npx thumbgate serve` | Advisory — MCP `gate_check` |
340
+ | **Amp** | `npx thumbgate init --agent amp` | Feedback capture only |
341
+ | **Claude Desktop** | [Download extension bundle](https://github.com/IgorGanapolsky/ThumbGate/releases/latest/download/thumbgate-claude-desktop.mcpb) | Advisory — MCP |
342
+ | **VS Code / Open VSX** | [plugins/vscode-extension/README.md](plugins/vscode-extension/README.md) | See plugin README |
343
+ | **Antigravity-compatible** | [plugins/antigravity-extension/INSTALL.md](plugins/antigravity-extension/INSTALL.md) | See plugin README |
344
+ | **JetBrains** | [plugins/jetbrains-plugin/README.md](plugins/jetbrains-plugin/README.md) | See plugin README |
345
+
346
+ **Advisory means the agent can ignore it.** Harnesses without a pre-tool hook expose no
347
+ interception point, so ThumbGate cannot stop the call itself — it returns a verdict the
348
+ agent is instructed to honor. Treat advisory coverage as a strong default, not a
349
+ guarantee, and prefer a hard-enforcement harness for anything irreversible.
350
+
351
+ The gate is **model-agnostic**: verdicts come from deterministic policy evaluation over
352
+ the proposed tool call (`scripts/gates-engine.js`), never from an LLM. Swapping the model
353
+ behind any harness does not change what is allowed.
354
+
355
+ > **Enforcement posture:** ThumbGate ships **warn-by-default** — a matched gate is logged
356
+ > and surfaced, not blocked. Set `THUMBGATE_STRICT_ENFORCEMENT=1` to hard-block. The
357
+ > `gate_check` tool reports `warn` (never `allow`) when a gate matched but posture
358
+ > downgraded it, so an agent is never told a flagged action is fine.
321
359
 
322
360
  ### Install scope: machine-wide vs per-project
323
361
 
@@ -2,13 +2,13 @@
2
2
  "mcpServers": {
3
3
  "thumbgate": {
4
4
  "command": "npx",
5
- "args": ["--yes", "--package", "thumbgate@1.29.2", "thumbgate", "serve"]
5
+ "args": ["--yes", "--package", "thumbgate@1.31.0", "thumbgate", "serve"]
6
6
  }
7
7
  },
8
8
  "hooks": {
9
9
  "preToolUse": {
10
10
  "command": "npx",
11
- "args": ["--yes", "--package", "thumbgate@1.29.2", "thumbgate", "gate-check"]
11
+ "args": ["--yes", "--package", "thumbgate@1.31.0", "thumbgate", "gate-check"]
12
12
  }
13
13
  }
14
14
  }
@@ -9,12 +9,12 @@ version: "1"
9
9
  skills:
10
10
  thumbgate-gate-check:
11
11
  description: "ThumbGate PreToolUse gate — blocks known-bad tool calls"
12
- command: "npx --yes --package thumbgate@1.29.2 thumbgate gate-check"
12
+ command: "npx --yes --package thumbgate@1.31.0 thumbgate gate-check"
13
13
  trigger: pre_tool_use
14
14
 
15
15
  thumbgate-feedback:
16
16
  description: "ThumbGate feedback capture — logs user prompt context"
17
- command: "npx --yes --package thumbgate@1.29.2 thumbgate hook-auto-capture"
17
+ command: "npx --yes --package thumbgate@1.31.0 thumbgate hook-auto-capture"
18
18
  trigger: user_prompt
19
19
 
20
20
  mcp:
@@ -23,6 +23,6 @@ mcp:
23
23
  args:
24
24
  - "--yes"
25
25
  - "--package"
26
- - "thumbgate@1.29.2"
26
+ - "thumbgate@1.31.0"
27
27
  - "thumbgate"
28
28
  - "serve"
@@ -96,6 +96,7 @@ const {
96
96
  requestEscalation,
97
97
  } = require('../../scripts/human-escalation');
98
98
  const { recordReasoningTrace } = require('../../scripts/agent-reasoning-traces');
99
+ const { recordToolCall } = require('../../scripts/tool-kpi-tracker');
99
100
  const {
100
101
  evaluateOperationalIntegrity,
101
102
  } = require('../../scripts/operational-integrity');
@@ -132,9 +133,7 @@ const {
132
133
  throw error;
133
134
  },
134
135
  }));
135
- const {
136
- searchThumbgate,
137
- } = require('../../scripts/thumbgate-search');
136
+ const { searchThumbgateAsync } = require('../../scripts/thumbgate-search');
138
137
  const {
139
138
  buildMultimodalRetrievalPlan,
140
139
  } = require('../../scripts/multimodal-retrieval-plan');
@@ -316,7 +315,7 @@ const {
316
315
  finalizeSession: finalizeFeedbackSession,
317
316
  } = require('../../scripts/feedback-session');
318
317
 
319
- const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.29.2' };
318
+ const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.31.0' };
320
319
  const COMMERCE_CATEGORIES = [
321
320
  'product_recommendation',
322
321
  'brand_compliance',
@@ -379,6 +378,9 @@ function toTextResult(payload) {
379
378
  const text = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2);
380
379
  return {
381
380
  content: [{ type: 'text', text }],
381
+ ...(payload !== null && typeof payload === 'object'
382
+ ? { structuredContent: payload }
383
+ : {}),
382
384
  };
383
385
  }
384
386
 
@@ -416,7 +418,7 @@ function toCaptureFeedbackTextResult(result) {
416
418
  if (reminder) {
417
419
  blocks.push({ type: 'text', text: reminder });
418
420
  }
419
- return { content: blocks };
421
+ return { content: blocks, structuredContent: result };
420
422
  }
421
423
 
422
424
  function formatContextPack(pack) {
@@ -758,10 +760,27 @@ function buildEstimateUncertaintyResponse(args = {}) {
758
760
  }
759
761
 
760
762
  async function callTool(name, args = {}) {
763
+ const attemptStartMs = Date.now();
761
764
  const activeProfile = getActiveMcpProfile();
762
- assertToolAllowed(name, activeProfile);
765
+ try {
766
+ assertToolAllowed(name, activeProfile);
767
+ } catch (error) {
768
+ recordMcpToolTrace(name, args, {
769
+ success: false,
770
+ category: 'profile_denied',
771
+ evidence: [error.message],
772
+ latencyMs: Date.now() - attemptStartMs,
773
+ });
774
+ throw error;
775
+ }
763
776
  const capability = getToolCapability(name);
764
777
  if (!capability.available) {
778
+ recordMcpToolTrace(name, args, {
779
+ success: false,
780
+ category: 'capability',
781
+ evidence: capability.missingModules,
782
+ latencyMs: Date.now() - attemptStartMs,
783
+ });
765
784
  if (capability.availability === 'private_core') {
766
785
  return unavailablePrivateMcpFeature(name);
767
786
  }
@@ -788,6 +807,7 @@ async function callTool(name, args = {}) {
788
807
  success: false,
789
808
  category: 'contract',
790
809
  evidence: validation.errors,
810
+ latencyMs: Date.now() - attemptStartMs,
791
811
  });
792
812
  throw err;
793
813
  }
@@ -803,6 +823,7 @@ async function callTool(name, args = {}) {
803
823
  success: false,
804
824
  category: 'permission',
805
825
  evidence: [firewallResult.message],
826
+ latencyMs: Date.now() - attemptStartMs,
806
827
  });
807
828
  throw err;
808
829
  }
@@ -816,11 +837,24 @@ async function callTool(name, args = {}) {
816
837
  success: false,
817
838
  category: err.errorCategory || 'execution',
818
839
  evidence: [err.code || err.message || 'tool execution failed'],
819
- latencyMs: Date.now() - startMs,
840
+ latencyMs: Date.now() - attemptStartMs,
820
841
  });
821
842
  throw err;
822
843
  }
823
844
  const latencyMs = Date.now() - startMs;
845
+ const outputValidation = validateMcpToolOutput(toolDef, result);
846
+ if (!outputValidation.valid) {
847
+ const err = new Error(`Structured output contract violation on '${name}': ${outputValidation.errors.join('; ')}`);
848
+ err.errorCategory = 'output_contract';
849
+ err.isRetryable = false;
850
+ recordMcpToolTrace(name, args, {
851
+ success: false,
852
+ category: 'output_contract',
853
+ evidence: outputValidation.errors,
854
+ latencyMs,
855
+ });
856
+ throw err;
857
+ }
824
858
  recordMcpToolTrace(name, args, {
825
859
  success: true,
826
860
  category: 'success',
@@ -840,7 +874,31 @@ async function callTool(name, args = {}) {
840
874
  return result;
841
875
  }
842
876
 
877
+ function validateMcpToolOutput(toolDef, result) {
878
+ if (!toolDef || !toolDef.outputSchema) return { valid: true, errors: [] };
879
+ const { validateStructuredOutput } = require('../../scripts/tool-contract-validator');
880
+ if (!result || result.structuredContent === undefined) {
881
+ return { valid: false, errors: ['Tool response is missing structuredContent'] };
882
+ }
883
+ return validateStructuredOutput(result.structuredContent, toolDef.outputSchema);
884
+ }
885
+
843
886
  function recordMcpToolTrace(name, args, outcome = {}) {
887
+ try {
888
+ recordToolCall({
889
+ toolName: name,
890
+ serverName: 'mcp',
891
+ latencyMs: Number(outcome.latencyMs || 0),
892
+ success: outcome.success === true,
893
+ agentId: args.agentId || args.processId || args.taskId || 'unknown',
894
+ metadata: {
895
+ category: outcome.category || 'unknown',
896
+ traceId: args.traceId || args.taskId || null,
897
+ },
898
+ });
899
+ } catch {
900
+ // KPI telemetry must not change the tool's functional outcome.
901
+ }
844
902
  try {
845
903
  const traceId = args.traceId || args.taskId || `mcp-${Date.now()}-${name}`;
846
904
  recordReasoningTrace({
@@ -888,10 +946,13 @@ async function callToolInner(name, args) {
888
946
  // action receipt (this action -> this outcome) before promotion. Returns
889
947
  // args unchanged when there is no matching receipt (non-breaking).
890
948
  const pairedFeedback = pairFeedbackWithReceipt(args);
891
- return toCaptureFeedbackTextResult(captureFeedback(pairedFeedback));
949
+ return toCaptureFeedbackTextResult(captureFeedback({
950
+ ...pairedFeedback,
951
+ reviewOrigin: 'automated',
952
+ }));
892
953
  }
893
954
  case 'feedback_summary':
894
- return toTextResult(feedbackSummary(Number(args.recent || 20)));
955
+ return toTextResult(feedbackSummary(Number(args.recent || 20), { humanOnly: true }));
895
956
  case 'search_lessons': {
896
957
  const module = loadPrivateMcpModule('lessonSearch');
897
958
  if (!module) return unavailablePrivateMcpFeature('search_lessons');
@@ -899,25 +960,34 @@ async function callToolInner(name, args) {
899
960
  limit: Number(args.limit || 10),
900
961
  category: args.category,
901
962
  tags: Array.isArray(args.tags) ? args.tags : [],
963
+ scope: args.scope,
964
+ requireScope: args.requireScope === true,
965
+ includeShared: args.includeShared !== false,
902
966
  }));
903
967
  }
904
968
  case 'suggest_fix':
905
969
  return buildSuggestFixResponse(args);
906
970
  case 'retrieve_lessons': {
907
971
  // Cross-encoder reranking: retrieve more candidates, then rerank for precision
908
- const { retrieveWithRerankingSync } = loadOptionalModule(path.join(__dirname, '../../scripts/cross-encoder-reranker'), () => ({
909
- retrieveWithRerankingSync: (toolName, actionContext, options = {}) => retrieveRelevantLessons(
972
+ const { retrieveWithReranking } = loadOptionalModule(path.join(__dirname, '../../scripts/cross-encoder-reranker'), () => ({
973
+ retrieveWithReranking: async (toolName, actionContext, options = {}) => retrieveRelevantLessons(
910
974
  toolName,
911
975
  actionContext,
912
976
  { maxResults: options.maxResults || 5 },
913
977
  ),
914
978
  }));
915
- return toTextResult(retrieveWithRerankingSync(
979
+ return toTextResult(await retrieveWithReranking(
916
980
  args.toolName,
917
981
  args.actionContext || '',
918
982
  {
919
983
  candidateCount: 20,
920
984
  maxResults: Number(args.maxResults || 5),
985
+ scope: args.scope,
986
+ requireScope: args.requireScope === true,
987
+ includeShared: args.includeShared !== false,
988
+ metadataFilters: args.filters,
989
+ queryRewrite: args.queryRewrite !== false,
990
+ includeRetrievalMeta: args.includeRetrievalMeta === true,
921
991
  },
922
992
  ));
923
993
  }
@@ -940,11 +1010,13 @@ async function callToolInner(name, args) {
940
1010
  }
941
1011
  case 'search_thumbgate':
942
1012
  enforceLimit('search_thumbgate');
943
- return toTextResult(searchThumbgate({
1013
+ return toTextResult(await searchThumbgateAsync({
944
1014
  query: args.query,
945
1015
  limit: args.limit,
946
1016
  source: args.source,
947
1017
  signal: args.signal,
1018
+ metadataFilters: args.filters,
1019
+ queryRewrite: args.queryRewrite !== false,
948
1020
  }));
949
1021
  case 'import_document':
950
1022
  return toTextResult(importDocument({
@@ -969,8 +1041,63 @@ async function callToolInner(name, args) {
969
1041
  }
970
1042
  return toTextResult(document);
971
1043
  }
1044
+ case 'gate_check': {
1045
+ // Same engine the PreToolUse hook uses, so an MCP client and a hook cannot
1046
+ // disagree about whether an action is allowed.
1047
+ const { runAsync } = require('../../scripts/gates-engine');
1048
+ const { canonicalizeToolCall } = require('../../scripts/harness-tool-names');
1049
+ const canonical = canonicalizeToolCall(args.tool_name, args.tool_input || {});
1050
+ const raw = await runAsync({
1051
+ tool_name: canonical.toolName,
1052
+ tool_input: canonical.toolInput,
1053
+ });
1054
+ let decision = 'allow';
1055
+ let reason = '';
1056
+ let flagged = false;
1057
+ try {
1058
+ const parsed = JSON.parse(raw);
1059
+ const hook = parsed.hookSpecificOutput || {};
1060
+ const verdict = hook.permissionDecision || parsed.decision || '';
1061
+ reason = hook.permissionDecisionReason || parsed.reason || hook.additionalContext || '';
1062
+ // The hook wire format says "deny"; the documented tool contract says "block".
1063
+ if (verdict === 'deny' || verdict === 'block') {
1064
+ decision = 'block';
1065
+ flagged = true;
1066
+ } else if (/\[GATE:/.test(reason)) {
1067
+ // A gate MATCHED but the warn-by-default posture downgraded it. Reporting
1068
+ // "allow" here is how this tool would become theater: .clinerules tells the
1069
+ // agent to abort only on "block", so a matched rm -rf / would have been run
1070
+ // with the warning text ignored. "warn" is the honest third state.
1071
+ decision = 'warn';
1072
+ flagged = true;
1073
+ }
1074
+ } catch (_) {
1075
+ // Unparseable engine output must never read as "allow" — fail closed.
1076
+ decision = 'error';
1077
+ reason = 'gate engine returned unparseable output';
1078
+ }
1079
+ const strict = process.env.THUMBGATE_STRICT_ENFORCEMENT === '1';
1080
+ return {
1081
+ content: [{
1082
+ type: 'text',
1083
+ text: JSON.stringify({
1084
+ decision,
1085
+ flagged,
1086
+ enforcement: strict ? 'strict' : 'warn-by-default',
1087
+ guidance: decision === 'block'
1088
+ ? 'Do NOT run this action. Surface the reason to the user.'
1089
+ : decision === 'warn'
1090
+ ? 'A policy gate matched but enforcement is warn-by-default. Do NOT run this action without explicit user confirmation; show them the reason.'
1091
+ : decision === 'error'
1092
+ ? 'Gate evaluation failed. Treat as unsafe and ask the user.'
1093
+ : 'No policy gate matched.',
1094
+ reason,
1095
+ }, null, 2),
1096
+ }],
1097
+ };
1098
+ }
972
1099
  case 'feedback_stats':
973
- return toTextResult(analyzeFeedback());
1100
+ return toTextResult(analyzeFeedback(undefined, { humanOnly: true }));
974
1101
  case 'diagnose_failure':
975
1102
  return buildDiagnoseFailureResponse(args);
976
1103
  case 'reflect_on_feedback':
@@ -1148,6 +1275,7 @@ async function callToolInner(name, args) {
1148
1275
  workflowContract: args.workflowContract,
1149
1276
  repoPath: args.repoPath,
1150
1277
  localOnly: args.localOnly === true,
1278
+ ttlMs: args.ttlMs,
1151
1279
  clear: args.clear === true,
1152
1280
  }),
1153
1281
  });
@@ -1744,5 +1872,6 @@ module.exports = {
1744
1872
  listAvailableTools,
1745
1873
  unavailablePrivateMcpFeature,
1746
1874
  callToolInner,
1875
+ validateMcpToolOutput,
1747
1876
  },
1748
1877
  };
@@ -7,7 +7,7 @@
7
7
  "npx",
8
8
  "--yes",
9
9
  "--package",
10
- "thumbgate@1.29.2",
10
+ "thumbgate@1.31.0",
11
11
  "thumbgate",
12
12
  "serve"
13
13
  ],
@@ -4,7 +4,7 @@
4
4
  "id": "checkout-link-grounding",
5
5
  "traceId": "trace_checkout_001",
6
6
  "question": "Is the Pro checkout link working?",
7
- "response": "The Pro checkout link is working because the checkout diagnostic confirms Stripe Pro checkout is reachable.",
7
+ "response": "The Pro checkout link is working because the checkout diagnostic confirms Stripe Pro checkout is reachable [context-1].",
8
8
  "retrievedContexts": [
9
9
  "Stripe Pro checkout diagnostic confirms the Pro checkout link is reachable and returns a valid checkout target.",
10
10
  "The workflow sprint checkout is intake-led and should not expose a stale payment link."
@@ -15,7 +15,7 @@
15
15
  "id": "letta-tool-gate",
16
16
  "traceId": "trace_letta_001",
17
17
  "question": "Should Letta execute a force push tool call?",
18
- "response": "ThumbGate blocks force push before execution for Letta tool calls.",
18
+ "response": "ThumbGate blocks force push before execution for Letta tool calls [context-1].",
19
19
  "retrievedContexts": [
20
20
  "ThumbGate's Letta adapter wraps Letta tool calls and runs gate-check before the executor is called.",
21
21
  "Force push to main is a blocked high-risk git action."