thumbgate 1.27.7 → 1.27.9

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 (106) hide show
  1. package/.well-known/llms.txt +1 -2
  2. package/README.md +0 -2
  3. package/bin/cli.js +259 -78
  4. package/package.json +12 -18
  5. package/public/blog.html +30 -0
  6. package/public/compare/adopt-ai.html +219 -0
  7. package/public/compare/agentix-labs.html +197 -0
  8. package/public/compare/ai-experience-orchestration.html +216 -0
  9. package/public/compare/anthropic-claude-for-legal.html +260 -0
  10. package/public/compare/anthropic-containment.html +280 -0
  11. package/public/compare/arcade.html +175 -0
  12. package/public/compare/arcjet.html +239 -0
  13. package/public/compare/bumblebee.html +307 -0
  14. package/public/compare/claude-code-hooks.html +294 -0
  15. package/public/compare/databricks-unity-ai-gateway.html +215 -0
  16. package/public/compare/fallow.html +351 -0
  17. package/public/compare/heidi.html +233 -0
  18. package/public/compare/mem0.html +342 -0
  19. package/public/compare/oak-and-sparrow-gatekeeper.html +289 -0
  20. package/public/compare/rein.html +236 -0
  21. package/public/compare/sigmashake.html +256 -0
  22. package/public/compare/speclock.html +342 -0
  23. package/public/compare.html +2 -0
  24. package/public/guides/agent-harness-optimization.html +342 -0
  25. package/public/guides/agentic-web-governance.html +406 -0
  26. package/public/guides/ai-agent-governance-sprint.html +415 -0
  27. package/public/guides/ai-agent-pre-action-approval-gates.html +401 -0
  28. package/public/guides/ai-agent-workflow-migration-checklist.html +392 -0
  29. package/public/guides/ai-deployment-readiness.html +415 -0
  30. package/public/guides/ai-mode-ads-agent-governance.html +401 -0
  31. package/public/guides/ai-search-topical-presence.html +342 -0
  32. package/public/guides/autoresearch-agent-safety.html +342 -0
  33. package/public/guides/background-agent-governance.html +358 -0
  34. package/public/guides/best-tools-stop-ai-agents-breaking-production.html +363 -0
  35. package/public/guides/browser-automation-safety.html +342 -0
  36. package/public/guides/chatgpt-ads-trust.html +353 -0
  37. package/public/guides/claude-code-feedback.html +339 -0
  38. package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
  39. package/public/guides/claude-code-skills-guardrails.html +343 -0
  40. package/public/guides/claude-desktop.html +356 -0
  41. package/public/guides/code-knowledge-graph-guardrails.html +365 -0
  42. package/public/guides/codex-cli-guardrails.html +339 -0
  43. package/public/guides/cursor-agent-guardrails.html +339 -0
  44. package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
  45. package/public/guides/database-agent-safety.html +406 -0
  46. package/public/guides/deepseek-v4-runtime-guardrails.html +346 -0
  47. package/public/guides/developer-machine-supply-chain-guardrails.html +358 -0
  48. package/public/guides/gcp-mcp-guardrails.html +147 -0
  49. package/public/guides/gemini-cli-feedback-memory.html +339 -0
  50. package/public/guides/gpt-5-5-model-evaluation.html +358 -0
  51. package/public/guides/internal-ai-engineering-stack-guardrails.html +348 -0
  52. package/public/guides/long-running-agent-context-management.html +346 -0
  53. package/public/guides/mcp-tool-governance.html +401 -0
  54. package/public/guides/multica-thumbgate-setup.html +134 -0
  55. package/public/guides/native-messaging-host-security.html +342 -0
  56. package/public/guides/policy-engine-pre-action-gates.html +346 -0
  57. package/public/guides/pre-action-checks.html +342 -0
  58. package/public/guides/pretooluse-hooks-vs-advisory-prompt-rules.html +342 -0
  59. package/public/guides/prompt-tricks-to-workflow-rules.html +365 -0
  60. package/public/guides/proxy-pointer-rag-guardrails.html +352 -0
  61. package/public/guides/rag-precision-tuning-guardrails.html +352 -0
  62. package/public/guides/reasoning-compression-guardrails.html +346 -0
  63. package/public/guides/relational-knowledge-ai-recommendations.html +342 -0
  64. package/public/guides/roo-code-alternative-cline.html +339 -0
  65. package/public/guides/semantic-programmatic-seo-guardrails.html +352 -0
  66. package/public/guides/seo-agent-skills-guardrails.html +344 -0
  67. package/public/guides/stop-repeated-ai-agent-mistakes.html +342 -0
  68. package/public/index.html +10 -48
  69. package/public/learn/ac-dc-runtime-enforcement.html +277 -0
  70. package/public/learn/agent-harness-pattern.html +181 -0
  71. package/public/learn/agent-swarms-shared-gates.html +173 -0
  72. package/public/learn/agentic-enterprise-context-brain.html +117 -0
  73. package/public/learn/agentic-os-team-governance.html +146 -0
  74. package/public/learn/ai-agent-governance.html +158 -0
  75. package/public/learn/ai-agent-persistent-memory.html +211 -0
  76. package/public/learn/background-agent-control-layer.html +184 -0
  77. package/public/learn/claude-code-goal-with-rubrics.html +205 -0
  78. package/public/learn/codex-role-plugins-need-governance.html +125 -0
  79. package/public/learn/cost-aware-agent-gate-routing.html +173 -0
  80. package/public/learn/databricks-unity-ai-gateway-runtime-governance.html +157 -0
  81. package/public/learn/deterministic-agent-workflows.html +185 -0
  82. package/public/learn/feedback-loop-vs-decision-layer.html +283 -0
  83. package/public/learn/from-prototype-to-production.html +223 -0
  84. package/public/learn/learn.css +51 -0
  85. package/public/learn/mcp-pre-action-checks-explained.html +172 -0
  86. package/public/learn/pretix-stripe-connect-marketplaces.html +161 -0
  87. package/public/learn/regulated-agent-execution-boundary.html +196 -0
  88. package/public/learn/spec-driven-development.html +168 -0
  89. package/public/learn/stop-ai-agent-force-push.html +134 -0
  90. package/public/learn/vibe-coding-safety-net.html +142 -0
  91. package/public/learn.html +6 -50
  92. package/public/pro.html +6 -6
  93. package/scripts/cli-schema.js +10 -22
  94. package/scripts/dashboard-chat.js +1 -2
  95. package/scripts/document-intake.js +49 -1
  96. package/scripts/gemini-embedding-policy.js +1 -2
  97. package/scripts/hosted-config.js +12 -0
  98. package/scripts/plausible-domain-config.js +1 -3
  99. package/scripts/reddit-browser-notification-watch.js +230 -0
  100. package/scripts/seo-gsd.js +0 -239
  101. package/scripts/vector-store.js +0 -44
  102. package/scripts/workspace-evolver.js +2 -62
  103. package/src/api/server.js +124 -335
  104. package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
  105. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
  106. package/scripts/hook-stop-anti-claim.js +0 -227
@@ -1,68 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const DEFAULT_ENDPOINT = 'https://api.oraclestechnologies.com/v1/guardian/analyze';
5
-
6
- function requireApiKey(env = process.env) {
7
- const key = env.ETHICORE_API_KEY || env.GUARDIAN_API_KEY || env.ORACLES_GUARDIAN_API_KEY;
8
- if (!key) {
9
- throw new Error('ETHICORE_API_KEY env var is required');
10
- }
11
- return key;
12
- }
13
-
14
- async function analyzeText(text, options = {}) {
15
- if (!String(text || '').trim()) {
16
- throw new Error('analyzeText requires text');
17
- }
18
-
19
- const env = options.env || process.env;
20
- const endpoint = options.endpoint || env.ETHICORE_GUARDIAN_ENDPOINT || DEFAULT_ENDPOINT;
21
- const apiKey = options.apiKey || requireApiKey(env);
22
- const fetchImpl = options.fetch || fetch;
23
-
24
- const response = await fetchImpl(endpoint, {
25
- method: 'POST',
26
- headers: {
27
- Authorization: `Bearer ${apiKey}`,
28
- 'Content-Type': 'application/json',
29
- },
30
- body: JSON.stringify({ text }),
31
- });
32
-
33
- const bodyText = await response.text();
34
- let body = bodyText;
35
- try {
36
- body = bodyText ? JSON.parse(bodyText) : {};
37
- } catch {
38
- // Keep non-JSON body for diagnostics.
39
- }
40
-
41
- if (!response.ok) {
42
- throw new Error(`Ethicore Guardian API ${response.status}: ${typeof body === 'string' ? body : JSON.stringify(body)}`);
43
- }
44
-
45
- return body;
46
- }
47
-
48
- function createEthicorePolicyCheck(options = {}) {
49
- return async function ethicorePolicyCheck(action = {}) {
50
- const toolText = [
51
- action.toolName,
52
- action.actionType,
53
- action.command,
54
- action.path,
55
- action.url,
56
- action.input ? JSON.stringify(action.input) : '',
57
- ].filter(Boolean).join('\n');
58
-
59
- return analyzeText(toolText || JSON.stringify(action), options);
60
- };
61
- }
62
-
63
- module.exports = {
64
- DEFAULT_ENDPOINT,
65
- analyzeText,
66
- createEthicorePolicyCheck,
67
- requireApiKey,
68
- };
@@ -1,260 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const { normalizeProviderAction } = require('../../scripts/provider-action-normalizer');
5
-
6
- const BLOCK_DECISIONS = new Set([
7
- 'block',
8
- 'blocked',
9
- 'deny',
10
- 'denied',
11
- 'disallow',
12
- 'disallowed',
13
- 'fail',
14
- 'failed',
15
- 'forbid',
16
- 'forbidden',
17
- 'reject',
18
- 'rejected',
19
- 'unsafe',
20
- 'violation',
21
- ]);
22
-
23
- const REVIEW_DECISIONS = new Set([
24
- 'approval',
25
- 'approval-required',
26
- 'approval_required',
27
- 'approve',
28
- 'human-review',
29
- 'human_review',
30
- 'manual-review',
31
- 'manual_review',
32
- 'review',
33
- 'requires-approval',
34
- 'requires_approval',
35
- 'requires-review',
36
- 'requires_review',
37
- ]);
38
-
39
- const ALLOW_DECISIONS = new Set([
40
- 'accept',
41
- 'accepted',
42
- 'allow',
43
- 'allowed',
44
- 'ok',
45
- 'pass',
46
- 'passed',
47
- 'permit',
48
- 'permitted',
49
- 'safe',
50
- ]);
51
-
52
- function asObject(value) {
53
- return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
54
- }
55
-
56
- function asArray(value) {
57
- return Array.isArray(value) ? value : [];
58
- }
59
-
60
- function firstString(...values) {
61
- for (const value of values) {
62
- const text = String(value || '').trim();
63
- if (text) return text;
64
- }
65
- return '';
66
- }
67
-
68
- function normalizeDecisionToken(value) {
69
- return String(value || '')
70
- .trim()
71
- .toLowerCase()
72
- .replace(/\s+/g, '-');
73
- }
74
-
75
- function normalizeEvidence(value) {
76
- const direct = asArray(value.evidence);
77
- const citations = asArray(value.citations);
78
- const violations = asArray(value.violations);
79
- const reasons = asArray(value.reasons);
80
- const reasoning = asArray(value.reasoning);
81
- const threatTypes = asArray(value.threat_types || value.threatTypes)
82
- .map((threatType) => ({
83
- code: String(threatType || '').trim(),
84
- message: `Threat type: ${String(threatType || '').trim()}`,
85
- source: 'guardian',
86
- severity: firstString(value.threat_level, value.threatLevel),
87
- }));
88
- return [...direct, ...citations, ...violations, ...reasons, ...reasoning, ...threatTypes]
89
- .map((entry) => {
90
- if (typeof entry === 'string') return { text: entry };
91
- const object = asObject(entry);
92
- if (!Object.keys(object).length) return null;
93
- return {
94
- id: firstString(object.id, object.ruleId, object.rule_id, object.code),
95
- text: firstString(object.text, object.reason, object.message, object.description, object.title),
96
- source: firstString(object.source, object.provider, object.policy),
97
- severity: firstString(object.severity, object.level),
98
- raw: object,
99
- };
100
- })
101
- .filter(Boolean);
102
- }
103
-
104
- function extractPolicyDecision(input = {}) {
105
- const event = asObject(input);
106
- for (const candidate of [
107
- event.policyDecision,
108
- event.policy_decision,
109
- event.guardrailResult,
110
- event.guardrail_result,
111
- event.result,
112
- ]) {
113
- const object = asObject(candidate);
114
- if (Object.keys(object).length) return object;
115
- }
116
- return event;
117
- }
118
-
119
- function classifyPolicyDecision(input = {}) {
120
- const value = extractPolicyDecision(input);
121
- const token = normalizeDecisionToken(firstString(
122
- value.decision,
123
- value.action,
124
- value.status,
125
- value.result,
126
- value.verdict,
127
- value.outcome,
128
- value.effect,
129
- value.recommended_action,
130
- value.recommendedAction,
131
- ));
132
-
133
- if (
134
- value.allowed === false
135
- || value.is_safe === false
136
- || value.isSafe === false
137
- || value.accepted === false
138
- || value.blocked === true
139
- || value.denied === true
140
- || BLOCK_DECISIONS.has(token)
141
- ) {
142
- return 'block';
143
- }
144
- if (
145
- value.requiresApproval === true
146
- || value.requires_approval === true
147
- || value.reviewRequired === true
148
- || value.review_required === true
149
- || REVIEW_DECISIONS.has(token)
150
- ) {
151
- return 'approval_required';
152
- }
153
- if (
154
- value.allowed === true
155
- || value.is_safe === true
156
- || value.isSafe === true
157
- || value.accepted === true
158
- || ALLOW_DECISIONS.has(token)
159
- ) {
160
- return 'allow';
161
- }
162
- return 'unknown';
163
- }
164
-
165
- function normalizePolicyDecision(input = {}, options = {}) {
166
- const value = extractPolicyDecision(input);
167
- const decision = classifyPolicyDecision(value);
168
- const source = firstString(
169
- options.source,
170
- value.source,
171
- value.provider,
172
- value.engine,
173
- value.policyEngine,
174
- value.policy_engine,
175
- 'policy-engine'
176
- );
177
- const reason = firstString(
178
- value.reason,
179
- value.message,
180
- value.explanation,
181
- value.summary,
182
- asArray(value.reasoning).join('; '),
183
- asArray(value.reasons).join('; '),
184
- decision === 'unknown' ? 'Policy engine returned an unknown decision; approval required before execution.' : ''
185
- );
186
-
187
- return {
188
- allowed: decision === 'allow',
189
- blocked: decision === 'block',
190
- approvalRequired: decision === 'approval_required' || decision === 'unknown',
191
- decision,
192
- reason,
193
- source,
194
- confidence: Number.isFinite(Number(value.confidence)) ? Number(value.confidence) : null,
195
- policyId: firstString(value.policyId, value.policy_id, value.ruleId, value.rule_id, value.id),
196
- severity: firstString(value.severity, value.level, value.threat_level, value.threatLevel),
197
- score: Number.isFinite(Number(value.score))
198
- ? Number(value.score)
199
- : (Number.isFinite(Number(value.threat_score)) ? Number(value.threat_score) : null),
200
- evidence: normalizeEvidence(value),
201
- raw: value,
202
- };
203
- }
204
-
205
- function normalizePolicyAction(input = {}) {
206
- const event = asObject(input);
207
- return {
208
- ...normalizeProviderAction({
209
- ...event,
210
- provider: firstString(event.provider, event.agentRuntime, event.runtime, 'policy-engine'),
211
- toolName: firstString(event.toolName, event.tool_name, event.name),
212
- input: asObject(event.toolInput || event.input || event.arguments || event.args),
213
- }),
214
- policyContext: asObject(event.policyContext || event.policy_context),
215
- };
216
- }
217
-
218
- function createPolicyEngineGuard({
219
- policyCheck,
220
- executeTool,
221
- gateCheck,
222
- onDecision,
223
- source = 'policy-engine',
224
- } = {}) {
225
- if (typeof policyCheck !== 'function') {
226
- throw new TypeError('createPolicyEngineGuard requires a policyCheck function');
227
- }
228
- if (typeof executeTool !== 'function') {
229
- throw new TypeError('createPolicyEngineGuard requires an executeTool function');
230
- }
231
-
232
- return async function guardedPolicyTool(input = {}) {
233
- const normalizedAction = normalizePolicyAction(input);
234
- const policyDecision = normalizePolicyDecision(await policyCheck(normalizedAction), { source });
235
- const gateDecision = typeof gateCheck === 'function'
236
- ? normalizePolicyDecision(await gateCheck({ normalizedAction, policyDecision }), { source: 'thumbgate' })
237
- : null;
238
- const effectiveDecision = gateDecision && !gateDecision.allowed ? gateDecision : policyDecision;
239
-
240
- if (typeof onDecision === 'function') {
241
- await onDecision({ normalizedAction, policyDecision, gateDecision, effectiveDecision });
242
- }
243
-
244
- if (!effectiveDecision.allowed) {
245
- const error = new Error(effectiveDecision.reason || 'ThumbGate blocked this action before execution.');
246
- error.code = effectiveDecision.approvalRequired ? 'THUMBGATE_APPROVAL_REQUIRED' : 'THUMBGATE_BLOCKED';
247
- error.thumbgate = { normalizedAction, policyDecision, gateDecision, effectiveDecision };
248
- throw error;
249
- }
250
-
251
- return executeTool(input, { normalizedAction, policyDecision, gateDecision, effectiveDecision });
252
- };
253
- }
254
-
255
- module.exports = {
256
- createPolicyEngineGuard,
257
- extractPolicyDecision,
258
- normalizePolicyAction,
259
- normalizePolicyDecision,
260
- };
@@ -1,227 +0,0 @@
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
- /\b(?:money|payment|charge|checkout|revenue|price|pricing|invoice|billing|tax|sales tax|inventory|stock|permission|access|customer[-\s]facing)\b.*\b(?:correct|accurate|verified|valid|matches|working|fixed|resolved|calculated|configured)\b/i,
47
- /\b(?:correct|accurate|verified|valid|matches|working|fixed|resolved|calculated|configured)\b.*\b(?:money|payment|charge|checkout|revenue|price|pricing|invoice|billing|tax|sales tax|inventory|stock|permission|access|customer[-\s]facing)\b/i,
48
- // Added 2026-06-11 after a cross-project failure analysis: these completion
49
- // claims ("all green / stable / verified / race over / tests pass") were
50
- // asserted without proof and slipped past the original set. The proof-gate
51
- // below suppresses them whenever the SAME turn ran a verification tool, so a
52
- // "verified" claim backed by a test/curl/Read stays silent.
53
- /\ball\s+(?:the\s+)?(?:tests?\s+|checks?\s+)?(?:are\s+)?green\b/i,
54
- /\b(?:all\s+)?(?:tests?|checks?|ci)\s+(?:are\s+)?(?:now\s+)?passing\b/i,
55
- /\ball\s+(?:tests?|checks?)\s+pass(?:ed)?\b/i,
56
- /\bverified\b/i,
57
- /\bconfirmed\b/i,
58
- /\b(?:is|are|it'?s|now)\s+stable\b/i,
59
- /\ball\s+clear\b/i,
60
- /\bgood\s+to\s+go\b/i,
61
- /\brace\s+(?:is\s+)?over\b/i,
62
- /\bno\s+longer\s+racing\b/i,
63
- ];
64
-
65
- // Proof-of-verification patterns. If the SAME turn included one of these
66
- // tool calls or shell command tokens, the claim is considered backed and
67
- // the hook stays silent.
68
- const PROOF_PATTERNS = [
69
- /\bcurl\b/,
70
- /\bgh\s+pr\s+(?:view|checks|status)\b/,
71
- /\bgh\s+run\s+view\b/,
72
- /\bgh\s+api\b/,
73
- /\bnode\s+--test\b/,
74
- /\bnpm\s+(?:run\s+)?test\b/,
75
- /\bnpm\s+pack\b/,
76
- /\bjest\b/,
77
- /\bmocha\b/,
78
- /\bpytest\b/,
79
- /\bplaywright\b/,
80
- /\bgrep\b/,
81
- /\bstripe\b/,
82
- /\bplaid\b/,
83
- /\bshopify\b/,
84
- /\bsquare\b/,
85
- /\bquickbooks\b/,
86
- /Read\s*\(/, // Claude Code Read tool call
87
- /Bash\s*\(/, // Claude Code Bash tool call
88
- ];
89
-
90
- function readLastAssistantTurn(transcriptPath) {
91
- if (!transcriptPath || !fs.existsSync(transcriptPath)) return null;
92
- let content;
93
- try {
94
- content = fs.readFileSync(transcriptPath, 'utf8');
95
- } catch {
96
- return null;
97
- }
98
- const lines = content.trim().split('\n');
99
- // Walk backwards to find the last assistant message
100
- for (let i = lines.length - 1; i >= 0; i--) {
101
- const raw = lines[i].trim();
102
- if (!raw) continue;
103
- let entry;
104
- try {
105
- entry = JSON.parse(raw);
106
- } catch {
107
- continue;
108
- }
109
- if (entry.type === 'assistant' && entry.message) {
110
- return entry.message;
111
- }
112
- }
113
- return null;
114
- }
115
-
116
- function extractText(message) {
117
- if (!message || !Array.isArray(message.content)) return '';
118
- return message.content
119
- .filter((b) => b && typeof b.text === 'string')
120
- .map((b) => b.text)
121
- .join('\n');
122
- }
123
-
124
- function extractToolUseSummary(message) {
125
- if (!message || !Array.isArray(message.content)) return '';
126
- return message.content
127
- .filter((b) => b?.type === 'tool_use')
128
- .map((b) => {
129
- const name = b.name || 'tool';
130
- let summary = '';
131
- if (b.input && typeof b.input === 'object') {
132
- if (typeof b.input.command === 'string') summary = b.input.command;
133
- else if (typeof b.input.file_path === 'string') summary = b.input.file_path;
134
- else if (typeof b.input.query === 'string') summary = b.input.query;
135
- else summary = JSON.stringify(b.input).slice(0, 200);
136
- }
137
- return `${name}: ${summary}`;
138
- })
139
- .join('\n');
140
- }
141
-
142
- function findClaim(text) {
143
- for (const p of CLAIM_PATTERNS) {
144
- const m = text.match(p);
145
- if (m) return m[0];
146
- }
147
- return null;
148
- }
149
-
150
- function hasProof(combined) {
151
- return PROOF_PATTERNS.some((p) => p.test(combined));
152
- }
153
-
154
- function readStdinSync() {
155
- try {
156
- return fs.readFileSync(0, 'utf8');
157
- } catch {
158
- return '';
159
- }
160
- }
161
-
162
- function main() {
163
- const raw = readStdinSync();
164
- let payload = {};
165
- try {
166
- payload = raw ? JSON.parse(raw) : {};
167
- } catch {
168
- payload = {};
169
- }
170
-
171
- const transcriptPath = payload.transcript_path || process.env.CLAUDE_TRANSCRIPT_PATH;
172
- const message = readLastAssistantTurn(transcriptPath);
173
- if (!message) return; // no transcript visible; nothing to check
174
-
175
- const text = extractText(message);
176
- const toolUseSummary = extractToolUseSummary(message);
177
- const claim = findClaim(text);
178
- if (!claim) return; // no completion claim made; silent
179
-
180
- const proofText = `${text}\n${toolUseSummary}`;
181
- if (hasProof(proofText)) return; // claim backed by proof in same turn
182
-
183
- // Strict mode (THUMBGATE_STRICT_ENFORCEMENT=1): hard-block the stop. Emit a
184
- // Stop-hook block decision so Claude Code does NOT end the turn — the agent
185
- // must run the verification (or retract) before it can stop. Default mode
186
- // stays soft (a reminder for the next turn) so we don't break existing wiring.
187
- if (process.env.THUMBGATE_STRICT_ENFORCEMENT === '1') {
188
- 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.`;
189
- process.stdout.write(JSON.stringify({ decision: 'block', reason }) + '\n');
190
- return;
191
- }
192
-
193
- // Default (soft): surface a system reminder for the NEXT turn. Do not hard-block.
194
- const reminder = [
195
- '⚠️ ThumbGate anti-claim gate: previous turn claimed completion',
196
- ` ("${claim}") without a proof tool call in the same message.`,
197
- ' Per CLAUDE.md anti-lying: never claim "done / live / deployed / fixed /',
198
- ' verified / all green / stable" or commercial truth (money / tax / inventory /',
199
- ' permissions / customer-facing state) without curl / grep / test / source-of-truth output in the SAME turn.',
200
- ' If the work really is verified, re-state the claim with the proof.',
201
- ' If not, retract and run the verification before re-asserting.',
202
- ].join('\n');
203
- process.stdout.write(reminder + '\n');
204
- }
205
-
206
- // Path-resolve check instead of `require.main === module`. SonarCloud's
207
- // strict type inference (rule S3403) flags the === form as always-false
208
- // in CommonJS, and CLAUDE.md "Hard-Won Lessons" pins the path-based form
209
- // as the portable fix (incident 2026-04-21 / PR #1115). Resolve BOTH sides
210
- // so the comparison is between two normalized absolute paths.
211
- const path = require('node:path');
212
- if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
213
- try {
214
- main();
215
- } catch {
216
- // never crash the agent
217
- }
218
- }
219
-
220
- module.exports = {
221
- CLAIM_PATTERNS,
222
- PROOF_PATTERNS,
223
- findClaim,
224
- hasProof,
225
- extractText,
226
- extractToolUseSummary,
227
- };