thumbgate 1.27.9 → 1.27.11

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.
@@ -0,0 +1,369 @@
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 and
20
+ * emits a Claude-hook `decision:"block"` JSON payload when a final-response
21
+ * violation can be evaluated before the response is accepted. Transcript-only
22
+ * runs still surface a reminder for the next turn when the host has already
23
+ * written the assistant response.
24
+ *
25
+ * Stdin: Claude Code passes the hook payload as JSON on stdin. We read
26
+ * `transcript_path` to locate the JSONL session log and scan the last
27
+ * assistant message.
28
+ *
29
+ * Stdout: any text printed is shown to the agent on the next turn.
30
+ */
31
+
32
+ const fs = require('node:fs');
33
+
34
+ // Lie-phrase patterns. These match common "claim of completion" wording
35
+ // the agent emits without verification. Word-boundary anchored to avoid
36
+ // false positives ("ready-made", "live-streaming", etc).
37
+ const CLAIM_PATTERNS = [
38
+ /\bis\s+live\b/i,
39
+ /\bnow\s+live\b/i,
40
+ /\bgoing\s+live\b/i,
41
+ /\bdeployed\b(?!\s*(yet|to\s+staging|on\s+a\s+branch))/i,
42
+ /\b(?:is|are|it'?s)\s+(?:now\s+)?(?:fully\s+)?(?:fixed|resolved|merged|shipped)\b/i,
43
+ /\bproduction[-\s]ready\b/i,
44
+ /\beverything\s+(?:is\s+)?(?:done|working|ready)\b/i,
45
+ /\b(?:github|repo|repository)\s+(?:about|metadata|description|topics?)\b.*\b(?:updated|verified|fixed|match(?:es|ed)?)\b/i,
46
+ /\b(?:about|metadata|description|topics?)\b.*\b(?:updated|verified|fixed|match(?:es|ed)?)\b.*\b(?:github|repo|repository)\b/i,
47
+ /\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,
48
+ /\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,
49
+ ];
50
+
51
+ // Proof-of-verification patterns. If the SAME turn included one of these
52
+ // tool calls or shell command tokens, the claim is considered backed and
53
+ // the hook stays silent.
54
+ const PROOF_PATTERNS = [
55
+ /\bcurl\b/,
56
+ /\bgh\s+pr\s+(?:view|checks|status)\b/,
57
+ /\bgh\s+run\s+view\b/,
58
+ /\bgh\s+api\b/,
59
+ /\bnode\s+--test\b/,
60
+ /\bnpm\s+(?:run\s+)?test\b/,
61
+ /\bnpm\s+pack\b/,
62
+ /\bjest\b/,
63
+ /\bmocha\b/,
64
+ /\bpytest\b/,
65
+ /\bplaywright\b/,
66
+ /\bgrep\b/,
67
+ /\bstripe\b/,
68
+ /\bplaid\b/,
69
+ /\bshopify\b/,
70
+ /\bsquare\b/,
71
+ /\bquickbooks\b/,
72
+ /\bgh\s+api\b/,
73
+ /\bls\b/,
74
+ /\bcat\b/,
75
+ /Read\s*\(/, // Claude Code Read tool call
76
+ /Bash\s*\(/, // Claude Code Bash tool call
77
+ ];
78
+
79
+ const POSITIVE_FEEDBACK_PATTERNS = [
80
+ /\bthumbs?\s*up\b/i,
81
+ /👍/,
82
+ /\bthank(s| you)\b/i,
83
+ /\bgood\b/i,
84
+ /\bgreat\b/i,
85
+ /\bperfect\b/i,
86
+ /\bok(?:ay)?\b/i,
87
+ ];
88
+
89
+ const LOW_VALUE_CLOSEOUT_PATTERNS = [
90
+ /^(?:good|great|perfect|ok(?:ay)?|thanks?|thank you)[.!,\s-]*(?:$|\b)/i,
91
+ /\buse\s+\w+\s*\/\s*\w+\b/i,
92
+ /\bsounds good\b/i,
93
+ ];
94
+
95
+ const SUBSTANTIVE_CLOSEOUT_PATTERNS = [
96
+ /\b(?:evidence|verified|tested|proof|result|residual risk|next state|next action|timestamp|source|url|file|line|passed|failed|blocked|unknown)\b/i,
97
+ /\b(?:node --test|npm run|curl|gh |git |pytest|playwright|screenshot|diff --check)\b/i,
98
+ /https?:\/\//i,
99
+ /\/[\w.-]+\/[\w./-]+/,
100
+ /`[^`]+`/,
101
+ ];
102
+
103
+ function readTranscriptEntries(transcriptPath) {
104
+ if (!transcriptPath || !fs.existsSync(transcriptPath)) return [];
105
+ let content;
106
+ try {
107
+ content = fs.readFileSync(transcriptPath, 'utf8');
108
+ } catch {
109
+ return [];
110
+ }
111
+ return content
112
+ .trim()
113
+ .split('\n')
114
+ .map((raw) => {
115
+ try {
116
+ return JSON.parse(raw);
117
+ } catch {
118
+ return null;
119
+ }
120
+ })
121
+ .filter(Boolean);
122
+ }
123
+
124
+ function readLastAssistantTurn(transcriptPath) {
125
+ const entries = readTranscriptEntries(transcriptPath);
126
+ // Walk backwards to find the last assistant message
127
+ for (let i = entries.length - 1; i >= 0; i--) {
128
+ const entry = entries[i];
129
+ if (entry.type === 'assistant' && entry.message) {
130
+ return entry.message;
131
+ }
132
+ }
133
+ return null;
134
+ }
135
+
136
+ function readPreviousUserText(transcriptPath) {
137
+ const entries = readTranscriptEntries(transcriptPath);
138
+ let seenAssistant = false;
139
+ for (let i = entries.length - 1; i >= 0; i--) {
140
+ const entry = entries[i];
141
+ if (!seenAssistant && entry.type === 'assistant') {
142
+ seenAssistant = true;
143
+ continue;
144
+ }
145
+ if (seenAssistant && entry.type === 'user' && entry.message) {
146
+ return extractText(entry.message);
147
+ }
148
+ }
149
+ return '';
150
+ }
151
+
152
+ function extractText(message) {
153
+ if (!message || !Array.isArray(message.content)) return '';
154
+ return message.content
155
+ .filter((b) => b && typeof b.text === 'string')
156
+ .map((b) => b.text)
157
+ .join('\n');
158
+ }
159
+
160
+ function extractToolUseSummary(message) {
161
+ if (!message || !Array.isArray(message.content)) return '';
162
+ return message.content
163
+ .filter((b) => b?.type === 'tool_use')
164
+ .map((b) => {
165
+ const name = b.name || 'tool';
166
+ let summary = '';
167
+ if (b.input && typeof b.input === 'object') {
168
+ if (typeof b.input.command === 'string') summary = b.input.command;
169
+ else if (typeof b.input.file_path === 'string') summary = b.input.file_path;
170
+ else if (typeof b.input.query === 'string') summary = b.input.query;
171
+ else summary = JSON.stringify(b.input).slice(0, 200);
172
+ }
173
+ return `${name}: ${summary}`;
174
+ })
175
+ .join('\n');
176
+ }
177
+
178
+ function wordCount(text) {
179
+ return String(text || '').trim().split(/\s+/).filter(Boolean).length;
180
+ }
181
+
182
+ function hasPositiveFeedback(text) {
183
+ return POSITIVE_FEEDBACK_PATTERNS.some((p) => p.test(text || ''));
184
+ }
185
+
186
+ function isLowValueCloseout(text, toolUseSummary = '') {
187
+ const normalized = String(text || '').trim();
188
+ if (!normalized) return false;
189
+ if (toolUseSummary.trim()) return false;
190
+ if (wordCount(normalized) > 45) return false;
191
+ if (SUBSTANTIVE_CLOSEOUT_PATTERNS.some((p) => p.test(normalized))) return false;
192
+ return LOW_VALUE_CLOSEOUT_PATTERNS.some((p) => p.test(normalized));
193
+ }
194
+
195
+ function findClaim(text) {
196
+ for (const p of CLAIM_PATTERNS) {
197
+ const m = text.match(p);
198
+ if (m) return m[0];
199
+ }
200
+ return null;
201
+ }
202
+
203
+ function hasProof(combined) {
204
+ return PROOF_PATTERNS.some((p) => p.test(combined));
205
+ }
206
+
207
+ function extractPayloadText(payload) {
208
+ if (!payload || typeof payload !== 'object') return '';
209
+ const candidates = [
210
+ payload.response,
211
+ payload.assistant_response,
212
+ payload.assistantResponse,
213
+ payload.final_response,
214
+ payload.finalResponse,
215
+ payload.text,
216
+ payload.output,
217
+ payload.message,
218
+ ];
219
+ for (const candidate of candidates) {
220
+ if (typeof candidate === 'string' && candidate.trim()) return candidate;
221
+ if (candidate && typeof candidate === 'object') {
222
+ const extracted = extractText(candidate);
223
+ if (extracted.trim()) return extracted;
224
+ }
225
+ }
226
+ return '';
227
+ }
228
+
229
+ function extractPayloadPreviousUserText(payload) {
230
+ if (!payload || typeof payload !== 'object') return '';
231
+ const candidates = [
232
+ payload.previous_user_text,
233
+ payload.previousUserText,
234
+ payload.user_prompt,
235
+ payload.userPrompt,
236
+ payload.prompt,
237
+ ];
238
+ for (const candidate of candidates) {
239
+ if (typeof candidate === 'string' && candidate.trim()) return candidate;
240
+ if (candidate && typeof candidate === 'object') {
241
+ const extracted = extractText(candidate);
242
+ if (extracted.trim()) return extracted;
243
+ }
244
+ }
245
+ return '';
246
+ }
247
+
248
+ function buildResponseQualityReason() {
249
+ return [
250
+ 'ThumbGate response-quality gate: final response answered positive feedback',
251
+ 'with a low-value social closeout instead of silence-level brevity or an evidence checkpoint.',
252
+ 'Positive feedback after operational work should trigger either no extra noise,',
253
+ 'a compact evidence checkpoint, or a concrete next-state update.',
254
+ 'Do not reply with generic "Good / Great / Use X/Y" filler.',
255
+ ].join(' ');
256
+ }
257
+
258
+ function buildResponseQualityReminder() {
259
+ return [
260
+ '⚠️ ThumbGate response-quality gate: previous turn answered positive feedback',
261
+ ' with a low-value social closeout instead of silence-level brevity or an evidence checkpoint.',
262
+ ' Positive feedback after operational work should trigger either no extra noise,',
263
+ ' a compact evidence checkpoint, or a concrete next-state update.',
264
+ ' Do not reply with generic "Good / Great / Use X/Y" filler.',
265
+ ].join('\n');
266
+ }
267
+
268
+ function writeBlock(reason) {
269
+ process.stdout.write(JSON.stringify({
270
+ decision: 'block',
271
+ reason,
272
+ hookSpecificOutput: {
273
+ hookEventName: 'Stop',
274
+ permissionDecision: 'deny',
275
+ permissionDecisionReason: reason,
276
+ },
277
+ }) + '\n');
278
+ }
279
+
280
+ function readStdinSync() {
281
+ try {
282
+ return fs.readFileSync(0, 'utf8');
283
+ } catch {
284
+ return '';
285
+ }
286
+ }
287
+
288
+ function main() {
289
+ const raw = readStdinSync();
290
+ let payload = {};
291
+ try {
292
+ payload = raw ? JSON.parse(raw) : {};
293
+ } catch {
294
+ payload = {};
295
+ }
296
+
297
+ const transcriptPath = payload.transcript_path || process.env.CLAUDE_TRANSCRIPT_PATH;
298
+ const directText = extractPayloadText(payload) || process.env.CLAUDE_RESPONSE || '';
299
+ const directPreviousUserText = extractPayloadPreviousUserText(payload)
300
+ || process.env.CLAUDE_PREVIOUS_USER_TEXT
301
+ || process.env.CLAUDE_PREVIOUS_USER
302
+ || '';
303
+
304
+ if (directText && hasPositiveFeedback(directPreviousUserText) && isLowValueCloseout(directText, '')) {
305
+ writeBlock(buildResponseQualityReason());
306
+ return;
307
+ }
308
+
309
+ const message = readLastAssistantTurn(transcriptPath);
310
+ if (!message) return; // no transcript visible; nothing to check
311
+
312
+ const text = extractText(message);
313
+ const toolUseSummary = extractToolUseSummary(message);
314
+ const previousUserText = readPreviousUserText(transcriptPath);
315
+
316
+ if (hasPositiveFeedback(previousUserText) && isLowValueCloseout(text, toolUseSummary)) {
317
+ process.stdout.write(buildResponseQualityReminder() + '\n');
318
+ return;
319
+ }
320
+
321
+ const claim = findClaim(text);
322
+ if (!claim) return; // no completion claim made; silent
323
+
324
+ const proofText = `${text}\n${toolUseSummary}`;
325
+ if (hasProof(proofText)) return; // claim backed by proof in same turn
326
+
327
+ // Surface a system reminder for the NEXT turn. Do not hard-block.
328
+ const reminder = [
329
+ '⚠️ ThumbGate anti-claim gate: previous turn claimed completion',
330
+ ` ("${claim}") without a proof tool call in the same message.`,
331
+ ' Per CLAUDE.md anti-lying: never claim "done / live / deployed / fixed"',
332
+ ' or commercial truth (money / tax / inventory / permissions / customer-facing state)',
333
+ ' without curl / grep / test / source-of-truth output in the SAME turn.',
334
+ ' If the work really is verified, re-state the claim with the proof.',
335
+ ' If not, retract and run the verification before re-asserting.',
336
+ ].join('\n');
337
+ process.stdout.write(reminder + '\n');
338
+ }
339
+
340
+ // Path-resolve check instead of `require.main === module`. SonarCloud's
341
+ // strict type inference (rule S3403) flags the === form as always-false
342
+ // in CommonJS, and CLAUDE.md "Hard-Won Lessons" pins the path-based form
343
+ // as the portable fix (incident 2026-04-21 / PR #1115). Resolve BOTH sides
344
+ // so the comparison is between two normalized absolute paths.
345
+ const path = require('node:path');
346
+ if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
347
+ try {
348
+ main();
349
+ } catch {
350
+ // never crash the agent
351
+ }
352
+ }
353
+
354
+ module.exports = {
355
+ CLAIM_PATTERNS,
356
+ PROOF_PATTERNS,
357
+ POSITIVE_FEEDBACK_PATTERNS,
358
+ LOW_VALUE_CLOSEOUT_PATTERNS,
359
+ findClaim,
360
+ hasProof,
361
+ hasPositiveFeedback,
362
+ isLowValueCloseout,
363
+ extractPayloadText,
364
+ extractPayloadPreviousUserText,
365
+ buildResponseQualityReason,
366
+ extractText,
367
+ extractToolUseSummary,
368
+ readPreviousUserText,
369
+ };
@@ -971,12 +971,12 @@ const TOOLS = [
971
971
  }),
972
972
  readOnlyTool({
973
973
  name: 'verify_claim',
974
- description: 'Check whether a claim has enough tracked evidence before the agent asserts it.',
974
+ description: 'Check whether a claim has enough tracked evidence before the agent asserts it, including tests-pass, commercial truth, GitHub metadata, and anthropomorphic AI claims such as "the model understands" or "the agent decided".',
975
975
  inputSchema: {
976
976
  type: 'object',
977
977
  required: ['claim'],
978
978
  properties: {
979
- claim: { type: 'string', description: 'The claim text to verify' },
979
+ claim: { type: 'string', description: 'The claim text to verify before assertion; human-like AI claims require anthropomorphic_claim_verified evidence.' },
980
980
  goalContract: GOAL_CONTRACT_SCHEMA,
981
981
  },
982
982
  },
package/src/api/server.js CHANGED
@@ -3334,6 +3334,8 @@ function renderSitemapXml(runtimeConfig) {
3334
3334
  { path: '/learn/agentic-os-team-governance', changefreq: 'weekly', priority: '0.85' },
3335
3335
  { path: '/learn/cost-aware-agent-gate-routing', changefreq: 'weekly', priority: '0.85' },
3336
3336
  { path: '/learn/databricks-unity-ai-gateway-runtime-governance', changefreq: 'weekly', priority: '0.85' },
3337
+ { path: '/learn/anthropomorphic-claim-gates', changefreq: 'weekly', priority: '0.85' },
3338
+ { path: '/learn/agent-identity-connector-governance', changefreq: 'weekly', priority: '0.9' },
3337
3339
  { path: '/learn/pretix-stripe-connect-marketplaces', changefreq: 'weekly', priority: '0.9' },
3338
3340
  { path: '/compare/claude-code-hooks', changefreq: 'weekly', priority: '0.85' },
3339
3341
  { path: '/compare/bumblebee', changefreq: 'weekly', priority: '0.85' },