thumbgate 1.27.18 → 1.27.20

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 (170) hide show
  1. package/.claude/commands/dashboard.md +15 -0
  2. package/.claude/commands/thumbgate-blocked.md +27 -0
  3. package/.claude/commands/thumbgate-dashboard.md +15 -0
  4. package/.claude/commands/thumbgate-doctor.md +30 -0
  5. package/.claude/commands/thumbgate-guard.md +36 -0
  6. package/.claude/commands/thumbgate-protect.md +30 -0
  7. package/.claude/commands/thumbgate-rules.md +30 -0
  8. package/.claude-plugin/plugin.json +4 -3
  9. package/.well-known/agentic-verify.txt +1 -0
  10. package/.well-known/llms.txt +33 -12
  11. package/.well-known/mcp/server-card.json +8 -8
  12. package/README.md +246 -30
  13. package/adapters/claude/.mcp.json +2 -2
  14. package/adapters/codex/config.toml +2 -2
  15. package/adapters/gcp/dfcx-webhook-gate.js +295 -0
  16. package/adapters/gemini/function-declarations.json +1 -0
  17. package/adapters/letta/README.md +41 -0
  18. package/adapters/letta/thumbgate-letta-adapter.js +133 -0
  19. package/adapters/mcp/server-stdio.js +263 -11
  20. package/adapters/opencode/opencode.json +1 -1
  21. package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
  22. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
  23. package/bench/observability-eval-suite.json +26 -0
  24. package/bench/thumbgate-bench.json +2 -2
  25. package/bin/cli.js +1456 -122
  26. package/bin/dashboard-cli.js +7 -0
  27. package/bin/postinstall.js +11 -27
  28. package/commands/dashboard.md +15 -0
  29. package/commands/thumbgate-dashboard.md +15 -0
  30. package/config/gate-classifier-routing.json +98 -0
  31. package/config/gate-templates.json +216 -0
  32. package/config/gates/claim-verification.json +12 -0
  33. package/config/gates/default.json +31 -2
  34. package/config/github-about.json +2 -2
  35. package/config/mcp-allowlists.json +23 -13
  36. package/config/merge-quality-checks.json +0 -1
  37. package/config/model-candidates.json +121 -6
  38. package/config/post-deploy-marketing-pages.json +80 -0
  39. package/config/tessl-tiles.json +1 -3
  40. package/openapi/openapi.yaml +12 -0
  41. package/package.json +225 -100
  42. package/public/about.html +162 -0
  43. package/public/agent-manager.html +179 -0
  44. package/public/agents-cost-savings.html +153 -0
  45. package/public/ai-malpractice-prevention.html +818 -0
  46. package/public/assets/brand/github-social-preview.png +0 -0
  47. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  48. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  49. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  50. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  51. package/public/assets/brand/thumbgate-logo-transparent.svg +28 -0
  52. package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -0
  53. package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
  54. package/public/assets/brand/thumbgate-mark-team.svg +26 -0
  55. package/public/assets/brand/thumbgate-mark.svg +21 -0
  56. package/public/assets/brand/thumbgate-wordmark.svg +20 -0
  57. package/public/assets/claude-thumbgate-statusbar.svg +8 -0
  58. package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
  59. package/public/assets/legal-intake-control-flow.svg +66 -0
  60. package/public/blog.html +4 -4
  61. package/public/brand/thumbgate-mark.svg +19 -0
  62. package/public/brand/thumbgate-og.svg +16 -0
  63. package/public/chatgpt-app.html +330 -0
  64. package/public/codex-enterprise.html +123 -0
  65. package/public/codex-plugin.html +72 -20
  66. package/public/compare.html +31 -8
  67. package/public/dashboard.html +930 -166
  68. package/public/diagnostic.html +345 -0
  69. package/public/federal.html +2 -2
  70. package/public/guide.html +33 -13
  71. package/public/index.html +469 -111
  72. package/public/install.html +193 -0
  73. package/public/js/buyer-intent.js +672 -0
  74. package/public/learn.html +183 -18
  75. package/public/lessons.html +168 -10
  76. package/public/numbers.html +7 -7
  77. package/public/pricing.html +399 -0
  78. package/public/pro.html +34 -11
  79. package/scripts/action-receipts.js +324 -0
  80. package/scripts/activation-quickstart.js +187 -0
  81. package/scripts/agent-memory-lifecycle.js +211 -0
  82. package/scripts/agent-operations-planner.js +621 -0
  83. package/scripts/agent-readiness.js +20 -3
  84. package/scripts/agent-reward-model.js +53 -1
  85. package/scripts/ai-component-inventory.js +367 -0
  86. package/scripts/async-eval-observability.js +236 -0
  87. package/scripts/audit.js +65 -0
  88. package/scripts/auto-promote-gates.js +82 -10
  89. package/scripts/auto-wire-hooks.js +14 -0
  90. package/scripts/aws-blocks-guardrails.js +272 -0
  91. package/scripts/billing.js +93 -1
  92. package/scripts/bot-detection.js +61 -3
  93. package/scripts/build-metadata.js +50 -10
  94. package/scripts/classifier-routing.js +130 -0
  95. package/scripts/cli-feedback.js +4 -2
  96. package/scripts/cli-schema.js +97 -0
  97. package/scripts/cli-telemetry.js +6 -1
  98. package/scripts/commercial-offer.js +82 -2
  99. package/scripts/context-manager.js +74 -6
  100. package/scripts/dashboard-chat.js +332 -0
  101. package/scripts/dashboard.js +68 -2
  102. package/scripts/export-databricks-bundle.js +5 -1
  103. package/scripts/export-dpo-pairs.js +7 -2
  104. package/scripts/feedback-aggregate.js +281 -0
  105. package/scripts/feedback-loop.js +123 -1
  106. package/scripts/feedback-quality.js +87 -0
  107. package/scripts/feedback-sanitizer.js +105 -0
  108. package/scripts/filesystem-search.js +35 -10
  109. package/scripts/gate-stats.js +89 -0
  110. package/scripts/gates-engine.js +1176 -85
  111. package/scripts/gemini-embedding-policy.js +2 -1
  112. package/scripts/hook-runtime.js +20 -14
  113. package/scripts/hook-stop-anti-claim.js +301 -0
  114. package/scripts/hook-thumbgate-cache-updater.js +18 -2
  115. package/scripts/hybrid-feedback-context.js +142 -7
  116. package/scripts/install-shim.js +87 -0
  117. package/scripts/lesson-inference.js +8 -3
  118. package/scripts/lesson-search.js +17 -1
  119. package/scripts/license.js +10 -10
  120. package/scripts/llm-client.js +169 -4
  121. package/scripts/local-model-profile.js +15 -8
  122. package/scripts/mcp-config.js +7 -1
  123. package/scripts/mcp-oauth.js +293 -0
  124. package/scripts/memory-scope-readiness.js +159 -0
  125. package/scripts/meta-agent-loop.js +36 -0
  126. package/scripts/noop-detect.js +285 -0
  127. package/scripts/operational-integrity.js +39 -5
  128. package/scripts/oss-pr-opportunity-scout.js +35 -5
  129. package/scripts/parallel-workflow-orchestrator.js +293 -0
  130. package/scripts/plan-gate.js +243 -0
  131. package/scripts/plausible-domain-config.js +99 -0
  132. package/scripts/plausible-server-events.js +9 -6
  133. package/scripts/pro-local-dashboard.js +4 -4
  134. package/scripts/proxy-pointer-rag-guardrails.js +42 -1
  135. package/scripts/published-cli.js +0 -8
  136. package/scripts/qa-scenario-planner.js +136 -0
  137. package/scripts/rate-limiter.js +64 -13
  138. package/scripts/repeat-metric.js +137 -0
  139. package/scripts/secret-fixture-tokens.js +61 -0
  140. package/scripts/secret-redaction.js +166 -0
  141. package/scripts/secret-scanner.js +44 -5
  142. package/scripts/security-scanner.js +260 -10
  143. package/scripts/self-distill-agent.js +3 -1
  144. package/scripts/self-harness-optimizer.js +141 -0
  145. package/scripts/self-healing-check.js +193 -0
  146. package/scripts/self-protection.js +90 -0
  147. package/scripts/seo-gsd.js +916 -7
  148. package/scripts/silent-failure-cluster.js +531 -0
  149. package/scripts/statusline-cache-path.js +17 -2
  150. package/scripts/statusline-cache-read.js +57 -0
  151. package/scripts/statusline-local-stats.js +9 -1
  152. package/scripts/statusline-meta.js +28 -2
  153. package/scripts/statusline.sh +20 -4
  154. package/scripts/sync-telemetry-from-prod.js +374 -0
  155. package/scripts/telemetry-analytics.js +357 -0
  156. package/scripts/thompson-sampling.js +31 -10
  157. package/scripts/thumbgate-bench.js +16 -1
  158. package/scripts/thumbgate-search.js +85 -19
  159. package/scripts/tool-contract-validator.js +76 -0
  160. package/scripts/tool-registry.js +169 -1
  161. package/scripts/trajectory-scorer.js +63 -0
  162. package/scripts/vector-store.js +45 -0
  163. package/scripts/verify-marketing-pages-deployed.js +212 -0
  164. package/scripts/visitor-journey.js +172 -0
  165. package/scripts/workflow-sentinel.js +286 -53
  166. package/scripts/workspace-evolver.js +62 -2
  167. package/src/api/server.js +2683 -319
  168. package/.claude-plugin/marketplace.json +0 -85
  169. package/adapters/chatgpt/openapi.yaml +0 -1695
  170. package/scripts/bot-detector.js +0 -50
@@ -122,7 +122,7 @@ function resolveGeminiEmbeddingConfig(env = process.env) {
122
122
 
123
123
  return {
124
124
  enabled,
125
- provider: enabled ? 'gemini' : 'local',
125
+ provider: provider === 'coreai' ? 'coreai' : (enabled ? 'gemini' : 'local'),
126
126
  model: String(env.THUMBGATE_GEMINI_EMBED_MODEL || GEMINI_EMBEDDING_2_MODEL).trim() || GEMINI_EMBEDDING_2_MODEL,
127
127
  apiKey,
128
128
  apiBaseUrl: trimTrailingSlashes(env.THUMBGATE_GEMINI_API_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta'),
@@ -171,6 +171,7 @@ function buildGeminiEmbeddingRolloutPlan(args = {}) {
171
171
  },
172
172
  rolloutSteps: [
173
173
  'Keep local embeddings as the default offline path.',
174
+ 'For Apple Silicon developers, route local queries through Core AI (AOT compiled models) to bypass CPU overhead.',
174
175
  'Enable Gemini Embedding 2 only when a Gemini API key is present.',
175
176
  'Use task-specific query/document prefixes at index and retrieval time.',
176
177
  'Start at 768 dimensions, then benchmark 1536 only if recall misses show up.',
@@ -7,6 +7,7 @@ const {
7
7
  publishedCliAvailable,
8
8
  } = require('./mcp-config');
9
9
  const { publishedCliShellCommand } = require('./published-cli');
10
+ const { shimInstalled, shimPath } = require('./install-shim');
10
11
 
11
12
  const PKG_ROOT = path.join(__dirname, '..');
12
13
  const featureSupportCache = new Map();
@@ -33,34 +34,39 @@ function publishedHookCommandsAvailable(version) {
33
34
  return available;
34
35
  }
35
36
 
36
- function resolveCliBaseCommand() {
37
+ function resolveCliCommand(subcommand) {
38
+ // Source checkout: always use direct node command for development
39
+ if (isSourceCheckout(PKG_ROOT)) {
40
+ return `node ${shellQuote(path.join(PKG_ROOT, 'bin', 'cli.js'))} ${subcommand}`;
41
+ }
42
+ // Prefer stable shim — always resolves @latest, survives version bumps
43
+ if (shimInstalled()) {
44
+ return `${shellQuote(shimPath())} ${subcommand}`;
45
+ }
37
46
  const version = packageVersion();
38
47
  if (publishedHookCommandsAvailable(version)) {
39
- return publishedCliShellCommand(version);
40
- }
41
- if (isSourceCheckout(PKG_ROOT)) {
42
- return `node ${shellQuote(path.join(PKG_ROOT, 'bin', 'cli.js'))}`;
48
+ return publishedCliShellCommand(version, [subcommand]);
43
49
  }
44
- return publishedCliShellCommand(version);
50
+ return publishedCliShellCommand(version, [subcommand]);
45
51
  }
46
52
 
47
- function resolveCodexCliBaseCommand() {
53
+ function resolveCodexCliCommand(subcommand) {
48
54
  const version = packageVersion();
49
55
  if (publishedHookCommandsAvailable(version)) {
50
- return publishedCliShellCommand('latest', [], { preferInstalled: false });
56
+ return publishedCliShellCommand('latest', [subcommand]);
51
57
  }
52
58
  if (isSourceCheckout(PKG_ROOT)) {
53
- return `node ${shellQuote(path.join(PKG_ROOT, 'bin', 'cli.js'))}`;
59
+ return `node ${shellQuote(path.join(PKG_ROOT, 'bin', 'cli.js'))} ${subcommand}`;
54
60
  }
55
- return publishedCliShellCommand('latest', [], { preferInstalled: false });
61
+ return publishedCliShellCommand('latest', [subcommand]);
56
62
  }
57
63
 
58
64
  function buildPortableHookCommand(subcommand) {
59
- return `${resolveCliBaseCommand()} ${subcommand}`;
65
+ return resolveCliCommand(subcommand);
60
66
  }
61
67
 
62
68
  function buildCodexPortableHookCommand(subcommand) {
63
- return `${resolveCodexCliBaseCommand()} ${subcommand}`;
69
+ return resolveCodexCliCommand(subcommand);
64
70
  }
65
71
 
66
72
  function preToolHookCommand() {
@@ -115,8 +121,8 @@ module.exports = {
115
121
  packageVersion,
116
122
  publishedHookCommandsAvailable,
117
123
  preToolHookCommand,
118
- resolveCodexCliBaseCommand,
119
- resolveCliBaseCommand,
124
+ resolveCodexCliCommand,
125
+ resolveCliCommand,
120
126
  sessionStartHookCommand,
121
127
  statuslineCommand,
122
128
  userPromptHookCommand,
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Stop hook: anti-claim enforcement.
6
+ *
7
+ * Scans the assistant's most recent turn (assistant text + same-turn tool_use
8
+ * blocks) and blocks the "deployed / live / done / fixed / ready" claim
9
+ * unless that same turn included a proof tool call (curl / grep / test).
10
+ *
11
+ * Why: CLAUDE.md anti-lying directive ("Never claim fix done until
12
+ * committed+pushed. Never claim 'ready' without running e2e.") was
13
+ * aspirational, not enforced. Per CEO 2026-05-13 feedback after a session in
14
+ * which 5+ unverified claims slipped through, this is the harness-level
15
+ * enforcement that ends the recurring trust-burn pattern. ThumbGate-on-
16
+ * ThumbGate dogfood — we are the prevention-rule generator and a perfect
17
+ * customer for our own gate.
18
+ *
19
+ * Wires through .claude/settings.json Stop hooks list. Always exits 0
20
+ * (informational): the goal is to surface a system reminder in the next
21
+ * turn so the agent corrects mid-conversation rather than to hard-block
22
+ * the turn that already happened.
23
+ *
24
+ * Stdin: Claude Code passes the hook payload as JSON on stdin. We read
25
+ * `transcript_path` to locate the JSONL session log and scan the last
26
+ * assistant message.
27
+ *
28
+ * Stdout: any text printed is shown to the agent on the next turn.
29
+ */
30
+
31
+ const fs = require('node:fs');
32
+
33
+ // Lie-phrase patterns. These match common "claim of completion" wording
34
+ // the agent emits without verification. Word-boundary anchored to avoid
35
+ // false positives ("ready-made", "live-streaming", etc).
36
+ const CLAIM_PATTERNS = [
37
+ /\bis\s+live\b/i,
38
+ /\bnow\s+live\b/i,
39
+ /\bgoing\s+live\b/i,
40
+ /\bdeployed\b(?!\s*(yet|to\s+staging|on\s+a\s+branch))/i,
41
+ /\b(?:is|are|it'?s)\s+(?:now\s+)?(?:fully\s+)?(?:fixed|resolved|merged|shipped)\b/i,
42
+ /\bproduction[-\s]ready\b/i,
43
+ /\beverything\s+(?:is\s+)?(?:done|working|ready)\b/i,
44
+ /\b(?:github|repo|repository)\s+(?:about|metadata|description|topics?)\b.*\b(?:updated|verified|fixed|match(?:es|ed)?)\b/i,
45
+ /\b(?:about|metadata|description|topics?)\b.*\b(?:updated|verified|fixed|match(?:es|ed)?)\b.*\b(?:github|repo|repository)\b/i,
46
+ // Added 2026-06-11 after a cross-project failure analysis: these completion
47
+ // claims ("all green / stable / verified / race over / tests pass") were
48
+ // asserted without proof and slipped past the original set. The proof-gate
49
+ // below suppresses them whenever the SAME turn ran a verification tool, so a
50
+ // "verified" claim backed by a test/curl/Read stays silent.
51
+ /\b(?:all\s+)?(?:tests?|checks?|ci)\s+(?:are\s+)?(?:now\s+)?passing\b/i,
52
+ /\ball\s+(?:tests?|checks?)\s+pass(?:ed)?\b/i,
53
+ /\bverified\b/i,
54
+ /\bconfirmed\b/i,
55
+ /\b(?:is|are|it'?s|now)\s+stable\b/i,
56
+ /\ball\s+clear\b/i,
57
+ /\bgood\s+to\s+go\b/i,
58
+ /\brace\s+(?:is\s+)?over\b/i,
59
+ /\bno\s+longer\s+racing\b/i,
60
+ ];
61
+
62
+ // Proof-of-verification patterns. If the SAME turn included one of these
63
+ // tool calls or shell command tokens, the claim is considered backed and
64
+ // the hook stays silent.
65
+ const PROOF_PATTERNS = [
66
+ /\bcurl\b/,
67
+ /\bgh\s+pr\s+(?:view|checks|status)\b/,
68
+ /\bgh\s+run\s+view\b/,
69
+ /\bgh\s+api\b/,
70
+ /\bnode\s+--test\b/,
71
+ /\bnpm\s+(?:run\s+)?test\b/,
72
+ /\bnpm\s+pack\b/,
73
+ /\bjest\b/,
74
+ /\bmocha\b/,
75
+ /\bpytest\b/,
76
+ /\bplaywright\b/,
77
+ /\bgrep\b/,
78
+ /\bstripe\b/,
79
+ /\bplaid\b/,
80
+ /\bshopify\b/,
81
+ /\bsquare\b/,
82
+ /\bquickbooks\b/,
83
+ /Read\s*\(/, // Claude Code Read tool call
84
+ /Bash\s*\(/, // Claude Code Bash tool call
85
+ ];
86
+
87
+ const COMMERCIAL_CLAIM_SUBJECTS = [
88
+ 'money',
89
+ 'payment',
90
+ 'charge',
91
+ 'checkout',
92
+ 'revenue',
93
+ 'price',
94
+ 'pricing',
95
+ 'invoice',
96
+ 'billing',
97
+ 'tax',
98
+ 'sales tax',
99
+ 'inventory',
100
+ 'stock',
101
+ 'permission',
102
+ 'access',
103
+ 'customer facing',
104
+ ];
105
+
106
+ const COMMERCIAL_CLAIM_STATES = [
107
+ 'correct',
108
+ 'accurate',
109
+ 'verified',
110
+ 'valid',
111
+ 'matches',
112
+ 'working',
113
+ 'fixed',
114
+ 'resolved',
115
+ 'calculated',
116
+ 'configured',
117
+ ];
118
+
119
+ const GREEN_CLAIM_PHRASES = [
120
+ 'all green',
121
+ 'all tests green',
122
+ 'all checks green',
123
+ 'all the tests green',
124
+ 'all the checks green',
125
+ 'all tests are green',
126
+ 'all checks are green',
127
+ ];
128
+
129
+ function normalizeClaimText(text) {
130
+ return String(text || '')
131
+ .toLowerCase()
132
+ .replace(/[-_]+/g, ' ')
133
+ .replace(/[^a-z0-9]+/g, ' ')
134
+ .replace(/\s+/g, ' ')
135
+ .trim();
136
+ }
137
+
138
+ function containsPhrase(normalizedText, phrase) {
139
+ return ` ${normalizedText} `.includes(` ${phrase} `);
140
+ }
141
+
142
+ function findTokenListClaim(text, subjects, states, label) {
143
+ const normalized = normalizeClaimText(text);
144
+ if (!normalized) return null;
145
+ const subject = subjects.find((candidate) => containsPhrase(normalized, candidate));
146
+ const state = states.find((candidate) => containsPhrase(normalized, candidate));
147
+ return subject && state ? `${label}: ${subject} ${state}` : null;
148
+ }
149
+
150
+ function findGreenClaim(text) {
151
+ const normalized = normalizeClaimText(text);
152
+ return GREEN_CLAIM_PHRASES.find((phrase) => containsPhrase(normalized, phrase)) || null;
153
+ }
154
+
155
+ function readLastAssistantTurn(transcriptPath) {
156
+ if (!transcriptPath || !fs.existsSync(transcriptPath)) return null;
157
+ let content;
158
+ try {
159
+ content = fs.readFileSync(transcriptPath, 'utf8');
160
+ } catch {
161
+ return null;
162
+ }
163
+ const lines = content.trim().split('\n');
164
+ // Walk backwards to find the last assistant message
165
+ for (let i = lines.length - 1; i >= 0; i--) {
166
+ const raw = lines[i].trim();
167
+ if (!raw) continue;
168
+ let entry;
169
+ try {
170
+ entry = JSON.parse(raw);
171
+ } catch {
172
+ continue;
173
+ }
174
+ if (entry.type === 'assistant' && entry.message) {
175
+ return entry.message;
176
+ }
177
+ }
178
+ return null;
179
+ }
180
+
181
+ function extractText(message) {
182
+ if (!message || !Array.isArray(message.content)) return '';
183
+ return message.content
184
+ .filter((b) => b && typeof b.text === 'string')
185
+ .map((b) => b.text)
186
+ .join('\n');
187
+ }
188
+
189
+ function extractToolUseSummary(message) {
190
+ if (!message || !Array.isArray(message.content)) return '';
191
+ return message.content
192
+ .filter((b) => b?.type === 'tool_use')
193
+ .map((b) => {
194
+ const name = b.name || 'tool';
195
+ let summary = '';
196
+ if (b.input && typeof b.input === 'object') {
197
+ if (typeof b.input.command === 'string') summary = b.input.command;
198
+ else if (typeof b.input.file_path === 'string') summary = b.input.file_path;
199
+ else if (typeof b.input.query === 'string') summary = b.input.query;
200
+ else summary = JSON.stringify(b.input).slice(0, 200);
201
+ }
202
+ return `${name}: ${summary}`;
203
+ })
204
+ .join('\n');
205
+ }
206
+
207
+ function findClaim(text) {
208
+ const commercialClaim = findTokenListClaim(
209
+ text,
210
+ COMMERCIAL_CLAIM_SUBJECTS,
211
+ COMMERCIAL_CLAIM_STATES,
212
+ 'commercial truth',
213
+ );
214
+ if (commercialClaim) return commercialClaim;
215
+ const greenClaim = findGreenClaim(text);
216
+ if (greenClaim) return greenClaim;
217
+ for (const p of CLAIM_PATTERNS) {
218
+ const m = text.match(p);
219
+ if (m) return m[0];
220
+ }
221
+ return null;
222
+ }
223
+
224
+ function hasProof(combined) {
225
+ return PROOF_PATTERNS.some((p) => p.test(combined));
226
+ }
227
+
228
+ function readStdinSync() {
229
+ try {
230
+ return fs.readFileSync(0, 'utf8');
231
+ } catch {
232
+ return '';
233
+ }
234
+ }
235
+
236
+ function main() {
237
+ const raw = readStdinSync();
238
+ let payload = {};
239
+ try {
240
+ payload = raw ? JSON.parse(raw) : {};
241
+ } catch {
242
+ payload = {};
243
+ }
244
+
245
+ const transcriptPath = payload.transcript_path || process.env.CLAUDE_TRANSCRIPT_PATH;
246
+ const message = readLastAssistantTurn(transcriptPath);
247
+ if (!message) return; // no transcript visible; nothing to check
248
+
249
+ const text = extractText(message);
250
+ const toolUseSummary = extractToolUseSummary(message);
251
+ const claim = findClaim(text);
252
+ if (!claim) return; // no completion claim made; silent
253
+
254
+ const proofText = `${text}\n${toolUseSummary}`;
255
+ if (hasProof(proofText)) return; // claim backed by proof in same turn
256
+
257
+ // Strict mode (THUMBGATE_STRICT_ENFORCEMENT=1): hard-block the stop. Emit a
258
+ // Stop-hook block decision so Claude Code does NOT end the turn — the agent
259
+ // must run the verification (or retract) before it can stop. Default mode
260
+ // stays soft (a reminder for the next turn) so we don't break existing wiring.
261
+ if (process.env.THUMBGATE_STRICT_ENFORCEMENT === '1') {
262
+ const reason = `ThumbGate anti-claim gate (strict): you claimed completion ("${claim}") without a proof tool call in the same message. Run the verification (curl / grep / test / Read) and restate with the proof, or retract — do not end the turn on an unverified claim.`;
263
+ process.stdout.write(JSON.stringify({ decision: 'block', reason }) + '\n');
264
+ return;
265
+ }
266
+
267
+ // Default (soft): surface a system reminder for the NEXT turn. Do not hard-block.
268
+ const reminder = [
269
+ '⚠️ ThumbGate anti-claim gate: previous turn claimed completion',
270
+ ` ("${claim}") without a proof tool call in the same message.`,
271
+ ' Per CLAUDE.md anti-lying: never claim "done / live / deployed / fixed /',
272
+ ' verified / all green / stable" or commercial truth (money / tax / inventory /',
273
+ ' permissions / customer-facing state) without curl / grep / test / source-of-truth output in the SAME turn.',
274
+ ' If the work really is verified, re-state the claim with the proof.',
275
+ ' If not, retract and run the verification before re-asserting.',
276
+ ].join('\n');
277
+ process.stdout.write(reminder + '\n');
278
+ }
279
+
280
+ // Path-resolve check instead of `require.main === module`. SonarCloud's
281
+ // strict type inference (rule S3403) flags the === form as always-false
282
+ // in CommonJS, and CLAUDE.md "Hard-Won Lessons" pins the path-based form
283
+ // as the portable fix (incident 2026-04-21 / PR #1115). Resolve BOTH sides
284
+ // so the comparison is between two normalized absolute paths.
285
+ const path = require('node:path');
286
+ if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
287
+ try {
288
+ main();
289
+ } catch {
290
+ // never crash the agent
291
+ }
292
+ }
293
+
294
+ module.exports = {
295
+ CLAIM_PATTERNS,
296
+ PROOF_PATTERNS,
297
+ findClaim,
298
+ hasProof,
299
+ extractText,
300
+ extractToolUseSummary,
301
+ };
@@ -6,11 +6,27 @@
6
6
  * Also used directly by the CLI to refresh statusline counters after feedback capture.
7
7
  */
8
8
 
9
- const fs = require('fs');
10
- const path = require('path');
9
+ const fs = require('node:fs');
10
+ const os = require('node:os');
11
+ const path = require('node:path');
11
12
  const { resolveFeedbackDir } = require('./feedback-paths');
13
+ const {
14
+ getAggregateStatuslineCachePath,
15
+ shouldAggregateFeedback,
16
+ } = require('./feedback-aggregate');
12
17
 
13
18
  function getCachePath() {
19
+ const explicitFeedbackDir = process.env.THUMBGATE_FEEDBACK_DIR;
20
+ if (shouldAggregateFeedback() && explicitFeedbackDir) {
21
+ try {
22
+ if (path.resolve(explicitFeedbackDir).startsWith(path.resolve(os.tmpdir()) + path.sep)) {
23
+ return path.join(resolveFeedbackDir(), 'statusline_cache.json');
24
+ }
25
+ } catch {
26
+ return path.join(resolveFeedbackDir(), 'statusline_cache.json');
27
+ }
28
+ }
29
+ if (shouldAggregateFeedback()) return getAggregateStatuslineCachePath();
14
30
  return path.join(resolveFeedbackDir(), 'statusline_cache.json');
15
31
  }
16
32
 
@@ -18,6 +18,11 @@ const fs = require('fs');
18
18
  const path = require('path');
19
19
  const { resolveFeedbackDir } = require('./feedback-paths');
20
20
  const { readJsonl } = require('./fs-utils');
21
+ const {
22
+ TRANSPORT_WORDS,
23
+ sanitizeFeedbackText,
24
+ transportWordsOnly,
25
+ } = require('./feedback-sanitizer');
21
26
 
22
27
  // ---------------------------------------------------------------------------
23
28
  // Paths
@@ -51,6 +56,7 @@ const STOPWORDS = new Set([
51
56
  'has', 'had', 'not', 'but', 'they', 'you', 'can', 'will', 'all', 'any',
52
57
  'one', 'its', 'our', 'also', 'more', 'very', 'just', 'into', 'been',
53
58
  'bash', 'edit', 'write', 'tool', 'hook', 'clear',
59
+ ...TRANSPORT_WORDS,
54
60
  ]);
55
61
 
56
62
  const NEG = new Set([
@@ -74,7 +80,7 @@ const HYBRID_JSONL_READ_LIMIT = 400;
74
80
  */
75
81
  function normalize(text) {
76
82
  if (!text || typeof text !== 'string') return '';
77
- return text
83
+ return sanitizeFeedbackText(text)
78
84
  .replace(/\/Users\/[^\s/]+/g, '/Users/redacted')
79
85
  .replace(/:\d{4,5}\b/g, ':PORT')
80
86
  .toLowerCase()
@@ -97,7 +103,9 @@ function stripFeedbackPrefix(text) {
97
103
  * Compose normalize + stripFeedbackPrefix.
98
104
  */
99
105
  function normalizePatternText(text) {
100
- return normalize(stripFeedbackPrefix(text));
106
+ const normalized = normalize(stripFeedbackPrefix(text));
107
+ if (transportWordsOnly(normalized)) return '';
108
+ return normalized;
101
109
  }
102
110
 
103
111
  /**
@@ -125,6 +133,104 @@ function classify(entry) {
125
133
  return 'neutral';
126
134
  }
127
135
 
136
+ function isHookPromptEnvelope(context) {
137
+ if (!context || typeof context !== 'string') return false;
138
+ try {
139
+ const parsed = JSON.parse(context);
140
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
141
+ return Boolean(
142
+ parsed.prompt &&
143
+ (
144
+ parsed.hookEventName ||
145
+ parsed.hook_event_name ||
146
+ parsed.workspaceRoot ||
147
+ parsed.workspace_root ||
148
+ parsed.session_id ||
149
+ parsed.sessionId ||
150
+ parsed.transcript_path ||
151
+ parsed.transcriptPath
152
+ )
153
+ );
154
+ } catch (_) {
155
+ return false;
156
+ }
157
+ }
158
+
159
+ function patternContext(entry) {
160
+ const context = entry && entry.context ? String(entry.context) : '';
161
+ if (!context) return '';
162
+ const hasExplicitFeedback = Boolean(
163
+ entry.whatWentWrong ||
164
+ entry.what_went_wrong ||
165
+ entry.whatToChange ||
166
+ entry.what_to_change ||
167
+ entry.failureType ||
168
+ (Array.isArray(entry.tags) && entry.tags.length > 0) ||
169
+ entry.structuredRule
170
+ );
171
+ if (isHookPromptEnvelope(context) && !hasExplicitFeedback) return '';
172
+ if (isHookPromptEnvelope(context) && hasExplicitFeedback) {
173
+ return '';
174
+ }
175
+ return context;
176
+ }
177
+
178
+ /**
179
+ * Check if the feedback entry is an automated enforcement log (e.g. from gates engine)
180
+ * rather than real developer/user feedback.
181
+ */
182
+ function isAutomatedFeedback(entry) {
183
+ const tags = entry.tags || [];
184
+ if (tags.includes('auto-capture') || tags.includes('gates-engine') || tags.includes('audit-trail')) {
185
+ return true;
186
+ }
187
+ const context = String(entry.context || entry.whatWentWrong || '').toLowerCase();
188
+ return context.includes('gate "') || context.includes('blocked tool') || context.includes('warned tool');
189
+ }
190
+
191
+
192
+ function isHookPromptEnvelope(context) {
193
+ if (!context || typeof context !== 'string') return false;
194
+ try {
195
+ const parsed = JSON.parse(context);
196
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
197
+ return Boolean(
198
+ parsed.prompt &&
199
+ (
200
+ parsed.hookEventName ||
201
+ parsed.hook_event_name ||
202
+ parsed.workspaceRoot ||
203
+ parsed.workspace_root ||
204
+ parsed.session_id ||
205
+ parsed.sessionId ||
206
+ parsed.transcript_path ||
207
+ parsed.transcriptPath
208
+ )
209
+ );
210
+ } catch (_) {
211
+ return false;
212
+ }
213
+ }
214
+
215
+ function patternContext(entry) {
216
+ const context = entry && entry.context ? String(entry.context) : '';
217
+ if (!context) return '';
218
+ const hasExplicitFeedback = Boolean(
219
+ entry.whatWentWrong ||
220
+ entry.what_went_wrong ||
221
+ entry.whatToChange ||
222
+ entry.what_to_change ||
223
+ entry.failureType ||
224
+ (Array.isArray(entry.tags) && entry.tags.length > 0) ||
225
+ entry.structuredRule
226
+ );
227
+ if (isHookPromptEnvelope(context) && !hasExplicitFeedback) return '';
228
+ if (isHookPromptEnvelope(context) && hasExplicitFeedback) {
229
+ return '';
230
+ }
231
+ return context;
232
+ }
233
+
128
234
  /**
129
235
  * Extract ms from a timestamp value. Returns 0 on failure.
130
236
  */
@@ -212,13 +318,15 @@ function buildHybridState(opts) {
212
318
  if (cls === 'positive') positive++;
213
319
  if (cls === 'negative') {
214
320
  negative++;
215
- // Track tool-level negative counts
216
- const toolName = inferToolName(entry.toolName || entry.tool_name || 'unknown', entry.context || '');
217
- toolNegatives[toolName] = (toolNegatives[toolName] || 0) + 1;
321
+ // Track tool-level negative counts (exclude automated gate logs)
322
+ if (!isAutomatedFeedback(entry)) {
323
+ const toolName = inferToolName(entry.toolName || entry.tool_name || 'unknown', entry.context || '');
324
+ toolNegatives[toolName] = (toolNegatives[toolName] || 0) + 1;
325
+ }
218
326
 
219
327
  // Build pattern from context / whatWentWrong / what_went_wrong
220
328
  const rawText = [
221
- entry.context || '',
329
+ patternContext(entry),
222
330
  entry.whatWentWrong || entry.what_went_wrong || '',
223
331
  entry.whatToChange || entry.what_to_change || '',
224
332
  entry.failureType || '',
@@ -254,11 +362,13 @@ function buildHybridState(opts) {
254
362
 
255
363
  // Process attributed feedback separately to track attributed tool counts
256
364
  for (const entry of attributedEntries) {
365
+ if (classify(entry) !== 'negative') continue; // skip pruned/positive
366
+ if (isAutomatedFeedback(entry)) continue; // skip automated gate blocks
257
367
  const toolName = inferToolName(entry.toolName || entry.tool_name || entry.attributed_tool || 'unknown', entry.context || '');
258
368
  toolNegativesAttributed[toolName] = (toolNegativesAttributed[toolName] || 0) + 1;
259
369
 
260
370
  const rawText = [
261
- entry.context || '',
371
+ patternContext(entry),
262
372
  entry.whatWentWrong || entry.what_went_wrong || '',
263
373
  ...(Array.isArray(entry.tags) ? entry.tags : []),
264
374
  ...(entry.richContext && Array.isArray(entry.richContext.filePaths) ? entry.richContext.filePaths : []),
@@ -620,12 +730,36 @@ function evaluatePretool(toolName, toolInput, opts) {
620
730
 
621
731
  // Slow path: build live state (also used when compiled guards are stale)
622
732
  const state = buildHybridState({
733
+ feedbackDir: o.feedbackDir,
623
734
  feedbackLogPath: o.feedbackLogPath,
624
735
  attributedFeedbackPath: o.attributedFeedbackPath,
625
736
  });
626
737
  return evaluatePretoolFromState(state, toolName, toolInput);
627
738
  }
628
739
 
740
+ // Claw-style agent support (high-ROI for EnterpriseClaw / OpenShell agents from Automation Anywhere / Nvidia)
741
+ // Extends hybrid context for claw_action_type (file, screen, dynamic-tool, orchestration), agent_identity, hybrid_route.
742
+ // Use in evaluatePretool calls from claw-aware MCP/hooks: pass {clawContext: {actionType: 'dynamic-tool-creation', agentId: '...', route: 'local/cloud'}} in opts.
743
+ function evaluateClawPretool(toolName, toolInput, clawContext, opts) {
744
+ const o = opts || {};
745
+ const claw = clawContext || {};
746
+ // Merge claw metadata into toolInput for gate evaluation (so templates like block-dynamic-tool-creation can match)
747
+ const enrichedInput = {
748
+ ...(typeof toolInput === 'object' ? toolInput : { raw: toolInput }),
749
+ _claw: {
750
+ actionType: claw.actionType || 'unknown',
751
+ agentId: claw.agentId || 'unknown',
752
+ hybridRoute: claw.hybridRoute || 'unknown',
753
+ screenInteraction: !!claw.screenInteraction,
754
+ fileAccess: !!claw.fileAccess,
755
+ }
756
+ };
757
+ const result = evaluatePretool(toolName, JSON.stringify(enrichedInput), o);
758
+ // Tag result with claw metadata for logging/feedback
759
+ result.clawContext = claw;
760
+ return result;
761
+ }
762
+
629
763
  // ---------------------------------------------------------------------------
630
764
  // CLI main()
631
765
  // ---------------------------------------------------------------------------
@@ -674,6 +808,7 @@ function main() {
674
808
  module.exports = {
675
809
  buildHybridState,
676
810
  evaluatePretool,
811
+ evaluateClawPretool,
677
812
  compileGuardArtifact,
678
813
  writeGuardArtifact,
679
814
  readGuardArtifact,