thumbgate 1.27.11 → 1.27.13

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 (131) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.well-known/llms.txt +2 -1
  3. package/.well-known/mcp/server-card.json +1 -1
  4. package/README.md +2 -4
  5. package/adapters/claude/.mcp.json +2 -2
  6. package/adapters/mcp/server-stdio.js +1 -1
  7. package/adapters/opencode/opencode.json +1 -1
  8. package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
  9. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
  10. package/bin/cli.js +78 -259
  11. package/config/builtin-lessons.json +23 -0
  12. package/config/gate-templates.json +0 -228
  13. package/config/gates/claim-verification.json +0 -18
  14. package/package.json +35 -25
  15. package/public/assets/brand/thumbgate-logo-transparent.svg +22 -0
  16. package/public/assets/brand/thumbgate-mark-inline-v3.svg +19 -0
  17. package/public/assets/brand/thumbgate-mark.svg +11 -5
  18. package/public/blog.html +0 -30
  19. package/public/brand/thumbgate-mark.svg +9 -5
  20. package/public/chatgpt-app.html +2 -2
  21. package/public/compare.html +2 -1
  22. package/public/dashboard.html +1 -1
  23. package/public/federal.html +1 -1
  24. package/public/index.html +95 -216
  25. package/public/learn.html +59 -35
  26. package/public/lessons.html +1 -1
  27. package/public/numbers.html +2 -2
  28. package/public/pro.html +7 -7
  29. package/scripts/aws-blocks-guardrails.js +228 -0
  30. package/scripts/cli-schema.js +22 -10
  31. package/scripts/dashboard-chat.js +2 -1
  32. package/scripts/document-intake.js +1 -49
  33. package/scripts/durability/step.js +3 -3
  34. package/scripts/gate-stats.js +5 -11
  35. package/scripts/gemini-embedding-policy.js +2 -1
  36. package/scripts/hook-stop-anti-claim.js +116 -184
  37. package/scripts/hosted-config.js +0 -12
  38. package/scripts/llm-client.js +187 -5
  39. package/scripts/plausible-domain-config.js +3 -1
  40. package/scripts/seo-gsd.js +240 -1
  41. package/scripts/tool-registry.js +2 -2
  42. package/scripts/vector-store.js +44 -0
  43. package/scripts/workspace-evolver.js +62 -2
  44. package/src/api/server.js +340 -131
  45. package/public/assets/brand/thumbgate-mark-inline.svg +0 -15
  46. package/public/compare/adopt-ai.html +0 -219
  47. package/public/compare/agentix-labs.html +0 -197
  48. package/public/compare/ai-experience-orchestration.html +0 -216
  49. package/public/compare/anthropic-claude-for-legal.html +0 -260
  50. package/public/compare/anthropic-containment.html +0 -280
  51. package/public/compare/arcade.html +0 -175
  52. package/public/compare/arcjet.html +0 -239
  53. package/public/compare/bumblebee.html +0 -307
  54. package/public/compare/claude-code-hooks.html +0 -294
  55. package/public/compare/databricks-unity-ai-gateway.html +0 -215
  56. package/public/compare/fallow.html +0 -351
  57. package/public/compare/heidi.html +0 -233
  58. package/public/compare/mem0.html +0 -342
  59. package/public/compare/oak-and-sparrow-gatekeeper.html +0 -289
  60. package/public/compare/rein.html +0 -236
  61. package/public/compare/sigmashake.html +0 -256
  62. package/public/compare/speclock.html +0 -342
  63. package/public/guides/agent-harness-optimization.html +0 -342
  64. package/public/guides/agentic-web-governance.html +0 -406
  65. package/public/guides/ai-agent-governance-sprint.html +0 -415
  66. package/public/guides/ai-agent-pre-action-approval-gates.html +0 -401
  67. package/public/guides/ai-agent-workflow-migration-checklist.html +0 -392
  68. package/public/guides/ai-deployment-readiness.html +0 -415
  69. package/public/guides/ai-mode-ads-agent-governance.html +0 -401
  70. package/public/guides/ai-search-topical-presence.html +0 -342
  71. package/public/guides/autoresearch-agent-safety.html +0 -342
  72. package/public/guides/background-agent-governance.html +0 -358
  73. package/public/guides/best-tools-stop-ai-agents-breaking-production.html +0 -363
  74. package/public/guides/browser-automation-safety.html +0 -342
  75. package/public/guides/chatgpt-ads-trust.html +0 -353
  76. package/public/guides/claude-code-feedback.html +0 -339
  77. package/public/guides/claude-code-prevent-repeated-mistakes.html +0 -161
  78. package/public/guides/claude-code-skills-guardrails.html +0 -343
  79. package/public/guides/claude-desktop.html +0 -356
  80. package/public/guides/code-knowledge-graph-guardrails.html +0 -365
  81. package/public/guides/codex-cli-guardrails.html +0 -339
  82. package/public/guides/cursor-agent-guardrails.html +0 -339
  83. package/public/guides/cursor-prevent-repeated-mistakes.html +0 -161
  84. package/public/guides/database-agent-safety.html +0 -406
  85. package/public/guides/deepseek-v4-runtime-guardrails.html +0 -346
  86. package/public/guides/developer-machine-supply-chain-guardrails.html +0 -358
  87. package/public/guides/gcp-mcp-guardrails.html +0 -147
  88. package/public/guides/gemini-cli-feedback-memory.html +0 -339
  89. package/public/guides/gpt-5-5-model-evaluation.html +0 -358
  90. package/public/guides/internal-ai-engineering-stack-guardrails.html +0 -348
  91. package/public/guides/long-running-agent-context-management.html +0 -346
  92. package/public/guides/mcp-tool-governance.html +0 -401
  93. package/public/guides/multica-thumbgate-setup.html +0 -134
  94. package/public/guides/native-messaging-host-security.html +0 -342
  95. package/public/guides/policy-engine-pre-action-gates.html +0 -346
  96. package/public/guides/pre-action-checks.html +0 -342
  97. package/public/guides/pretooluse-hooks-vs-advisory-prompt-rules.html +0 -342
  98. package/public/guides/prompt-tricks-to-workflow-rules.html +0 -365
  99. package/public/guides/proxy-pointer-rag-guardrails.html +0 -352
  100. package/public/guides/rag-precision-tuning-guardrails.html +0 -352
  101. package/public/guides/reasoning-compression-guardrails.html +0 -346
  102. package/public/guides/relational-knowledge-ai-recommendations.html +0 -342
  103. package/public/guides/roo-code-alternative-cline.html +0 -339
  104. package/public/guides/semantic-programmatic-seo-guardrails.html +0 -352
  105. package/public/guides/seo-agent-skills-guardrails.html +0 -344
  106. package/public/guides/stop-repeated-ai-agent-mistakes.html +0 -342
  107. package/public/learn/ac-dc-runtime-enforcement.html +0 -277
  108. package/public/learn/agent-harness-pattern.html +0 -181
  109. package/public/learn/agent-identity-connector-governance.html +0 -146
  110. package/public/learn/agent-swarms-shared-gates.html +0 -173
  111. package/public/learn/agentic-enterprise-context-brain.html +0 -117
  112. package/public/learn/agentic-os-team-governance.html +0 -146
  113. package/public/learn/ai-agent-governance.html +0 -158
  114. package/public/learn/ai-agent-persistent-memory.html +0 -211
  115. package/public/learn/anthropomorphic-claim-gates.html +0 -180
  116. package/public/learn/background-agent-control-layer.html +0 -184
  117. package/public/learn/claude-code-goal-with-rubrics.html +0 -205
  118. package/public/learn/codex-role-plugins-need-governance.html +0 -125
  119. package/public/learn/cost-aware-agent-gate-routing.html +0 -173
  120. package/public/learn/databricks-unity-ai-gateway-runtime-governance.html +0 -157
  121. package/public/learn/deterministic-agent-workflows.html +0 -185
  122. package/public/learn/feedback-loop-vs-decision-layer.html +0 -283
  123. package/public/learn/from-prototype-to-production.html +0 -223
  124. package/public/learn/learn.css +0 -51
  125. package/public/learn/mcp-pre-action-checks-explained.html +0 -172
  126. package/public/learn/pretix-stripe-connect-marketplaces.html +0 -161
  127. package/public/learn/regulated-agent-execution-boundary.html +0 -196
  128. package/public/learn/spec-driven-development.html +0 -168
  129. package/public/learn/stop-ai-agent-force-push.html +0 -134
  130. package/public/learn/vibe-coding-safety-net.html +0 -142
  131. package/scripts/reddit-browser-notification-watch.js +0 -230
@@ -11,11 +11,6 @@ const PROJECT_ROOT = path.join(__dirname, '..');
11
11
  const MANUAL_GATES_PATH = path.join(PROJECT_ROOT, 'config', 'gates', 'default.json');
12
12
  const STATS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'gate-stats.json');
13
13
 
14
- function safeOccurrenceCount(value) {
15
- const n = Number(value);
16
- return Number.isFinite(n) && n > 0 ? n : 0;
17
- }
18
-
19
14
  function loadGatesFile(filePath) {
20
15
  if (!fs.existsSync(filePath)) return [];
21
16
  try {
@@ -44,16 +39,16 @@ function calculateStats() {
44
39
  // Count total blocks/warns from occurrences in auto-promoted gates
45
40
  const totalBlocked = autoGates
46
41
  .filter((g) => g.action === 'block')
47
- .reduce((sum, g) => sum + safeOccurrenceCount(g.occurrences), 0);
42
+ .reduce((sum, g) => sum + (g.occurrences || 0), 0);
48
43
  const totalWarned = autoGates
49
44
  .filter((g) => g.action === 'warn')
50
- .reduce((sum, g) => sum + safeOccurrenceCount(g.occurrences), 0);
45
+ .reduce((sum, g) => sum + (g.occurrences || 0), 0);
51
46
 
52
47
  // Top blocked gate. A configured block rule with zero occurrences is not a
53
48
  // "top blocker"; only recorded block events should appear here.
54
49
  const topBlocked = [...allGates]
55
- .filter((g) => g.action === 'block' && safeOccurrenceCount(g.occurrences) > 0)
56
- .sort((a, b) => safeOccurrenceCount(b.occurrences) - safeOccurrenceCount(a.occurrences))
50
+ .filter((g) => g.action === 'block' && Number(g.occurrences || 0) > 0)
51
+ .sort((a, b) => (b.occurrences || 0) - (a.occurrences || 0))
57
52
  .at(0) || null;
58
53
 
59
54
  // Last promotion event
@@ -110,7 +105,7 @@ function computeCalibration(gates) {
110
105
  const calibration = [];
111
106
  for (const gate of gates || []) {
112
107
  if (!gate || !gate.id) continue;
113
- const occurrences = safeOccurrenceCount(gate.occurrences);
108
+ const occurrences = Number(gate.occurrences || 0);
114
109
  const action = gate.action || 'unknown';
115
110
  // Only annotate gates with recorded occurrence data
116
111
  if (occurrences === 0) continue;
@@ -263,7 +258,6 @@ module.exports = {
263
258
  loadGatesFile,
264
259
  tryComputeBayesErrorRate,
265
260
  computeCalibration,
266
- safeOccurrenceCount,
267
261
  MANUAL_GATES_PATH,
268
262
  STATS_PATH,
269
263
  };
@@ -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.',
@@ -16,11 +16,10 @@
16
16
  * ThumbGate dogfood — we are the prevention-rule generator and a perfect
17
17
  * customer for our own gate.
18
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.
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.
24
23
  *
25
24
  * Stdin: Claude Code passes the hook payload as JSON on stdin. We read
26
25
  * `transcript_path` to locate the JSONL session log and scan the last
@@ -44,8 +43,20 @@ const CLAIM_PATTERNS = [
44
43
  /\beverything\s+(?:is\s+)?(?:done|working|ready)\b/i,
45
44
  /\b(?:github|repo|repository)\s+(?:about|metadata|description|topics?)\b.*\b(?:updated|verified|fixed|match(?:es|ed)?)\b/i,
46
45
  /\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,
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,
49
60
  ];
50
61
 
51
62
  // Proof-of-verification patterns. If the SAME turn included one of these
@@ -69,63 +80,97 @@ const PROOF_PATTERNS = [
69
80
  /\bshopify\b/,
70
81
  /\bsquare\b/,
71
82
  /\bquickbooks\b/,
72
- /\bgh\s+api\b/,
73
- /\bls\b/,
74
- /\bcat\b/,
75
83
  /Read\s*\(/, // Claude Code Read tool call
76
84
  /Bash\s*\(/, // Claude Code Bash tool call
77
85
  ];
78
86
 
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
+ 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',
87
104
  ];
88
105
 
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,
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',
93
117
  ];
94
118
 
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
- /`[^`]+`/,
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',
101
127
  ];
102
128
 
103
- function readTranscriptEntries(transcriptPath) {
104
- if (!transcriptPath || !fs.existsSync(transcriptPath)) return [];
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;
105
157
  let content;
106
158
  try {
107
159
  content = fs.readFileSync(transcriptPath, 'utf8');
108
160
  } catch {
109
- return [];
161
+ return null;
110
162
  }
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);
163
+ const lines = content.trim().split('\n');
126
164
  // Walk backwards to find the last assistant message
127
- for (let i = entries.length - 1; i >= 0; i--) {
128
- const entry = entries[i];
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
+ }
129
174
  if (entry.type === 'assistant' && entry.message) {
130
175
  return entry.message;
131
176
  }
@@ -133,22 +178,6 @@ function readLastAssistantTurn(transcriptPath) {
133
178
  return null;
134
179
  }
135
180
 
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
181
  function extractText(message) {
153
182
  if (!message || !Array.isArray(message.content)) return '';
154
183
  return message.content
@@ -175,24 +204,16 @@ function extractToolUseSummary(message) {
175
204
  .join('\n');
176
205
  }
177
206
 
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
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;
196
217
  for (const p of CLAIM_PATTERNS) {
197
218
  const m = text.match(p);
198
219
  if (m) return m[0];
@@ -204,79 +225,6 @@ function hasProof(combined) {
204
225
  return PROOF_PATTERNS.some((p) => p.test(combined));
205
226
  }
206
227
 
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
228
  function readStdinSync() {
281
229
  try {
282
230
  return fs.readFileSync(0, 'utf8');
@@ -295,42 +243,34 @@ function main() {
295
243
  }
296
244
 
297
245
  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
246
  const message = readLastAssistantTurn(transcriptPath);
310
247
  if (!message) return; // no transcript visible; nothing to check
311
248
 
312
249
  const text = extractText(message);
313
250
  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
251
  const claim = findClaim(text);
322
252
  if (!claim) return; // no completion claim made; silent
323
253
 
324
254
  const proofText = `${text}\n${toolUseSummary}`;
325
255
  if (hasProof(proofText)) return; // claim backed by proof in same turn
326
256
 
327
- // Surface a system reminder for the NEXT turn. Do not hard-block.
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.
328
268
  const reminder = [
329
269
  '⚠️ ThumbGate anti-claim gate: previous turn claimed completion',
330
270
  ` ("${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.',
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.',
334
274
  ' If the work really is verified, re-state the claim with the proof.',
335
275
  ' If not, retract and run the verification before re-asserting.',
336
276
  ].join('\n');
@@ -354,16 +294,8 @@ if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
354
294
  module.exports = {
355
295
  CLAIM_PATTERNS,
356
296
  PROOF_PATTERNS,
357
- POSITIVE_FEEDBACK_PATTERNS,
358
- LOW_VALUE_CLOSEOUT_PATTERNS,
359
297
  findClaim,
360
298
  hasProof,
361
- hasPositiveFeedback,
362
- isLowValueCloseout,
363
- extractPayloadText,
364
- extractPayloadPreviousUserText,
365
- buildResponseQualityReason,
366
299
  extractText,
367
300
  extractToolUseSummary,
368
- readPreviousUserText,
369
301
  };
@@ -13,7 +13,6 @@ const DEFAULT_PRO_PRICE_DOLLARS = PRO_MONTHLY_PRICE_DOLLARS;
13
13
  const DEFAULT_PRO_PRICE_LABEL = PRO_PRICE_LABEL;
14
14
  const DEFAULT_SPRINT_DIAGNOSTIC_PRICE_DOLLARS = 499;
15
15
  const DEFAULT_WORKFLOW_SPRINT_PRICE_DOLLARS = 1500;
16
- const DEFAULT_SNAPSHOT_PRICE_DOLLARS = 97;
17
16
  const GA_MEASUREMENT_ID_PATTERN = /^G-[A-Z0-9]+$/i;
18
17
 
19
18
  function normalizeOrigin(value) {
@@ -130,10 +129,6 @@ function resolveHostedBillingConfig({ requestOrigin } = {}, env = process.env) {
130
129
  const googleSiteVerification = normalizeTrackingId(env.THUMBGATE_GOOGLE_SITE_VERIFICATION);
131
130
  const sprintDiagnosticCheckoutUrl = normalizeAbsoluteUrl(env.THUMBGATE_SPRINT_DIAGNOSTIC_CHECKOUT_URL);
132
131
  const workflowSprintCheckoutUrl = normalizeAbsoluteUrl(env.THUMBGATE_WORKFLOW_SPRINT_CHECKOUT_URL);
133
- const paypalDiagnosticCheckoutUrl = normalizeAbsoluteUrl(env.THUMBGATE_PAYPAL_DIAGNOSTIC_CHECKOUT_URL);
134
- const paypalWorkflowSprintCheckoutUrl = normalizeAbsoluteUrl(env.THUMBGATE_PAYPAL_WORKFLOW_SPRINT_CHECKOUT_URL);
135
- const morSnapshotCheckoutUrl = normalizeAbsoluteUrl(env.THUMBGATE_MOR_SNAPSHOT_CHECKOUT_URL);
136
- const morProvider = String(env.THUMBGATE_MOR_PROVIDER || '').trim();
137
132
 
138
133
  return {
139
134
  appOrigin,
@@ -147,16 +142,10 @@ function resolveHostedBillingConfig({ requestOrigin } = {}, env = process.env) {
147
142
  proPriceLabel,
148
143
  sprintDiagnosticCheckoutUrl,
149
144
  workflowSprintCheckoutUrl,
150
- paypalDiagnosticCheckoutUrl,
151
- paypalWorkflowSprintCheckoutUrl,
152
- morSnapshotCheckoutUrl,
153
- morProvider,
154
145
  sprintDiagnosticPriceDollars: normalizePriceDollars(env.THUMBGATE_SPRINT_DIAGNOSTIC_PRICE_DOLLARS)
155
146
  || DEFAULT_SPRINT_DIAGNOSTIC_PRICE_DOLLARS,
156
147
  workflowSprintPriceDollars: normalizePriceDollars(env.THUMBGATE_WORKFLOW_SPRINT_PRICE_DOLLARS)
157
148
  || DEFAULT_WORKFLOW_SPRINT_PRICE_DOLLARS,
158
- snapshotPriceDollars: normalizePriceDollars(env.THUMBGATE_SNAPSHOT_PRICE_DOLLARS)
159
- || DEFAULT_SNAPSHOT_PRICE_DOLLARS,
160
149
  gaMeasurementId,
161
150
  googleSiteVerification,
162
151
  posthogApiKey,
@@ -170,7 +159,6 @@ module.exports = {
170
159
  DEFAULT_PRO_PRICE_LABEL,
171
160
  DEFAULT_SPRINT_DIAGNOSTIC_PRICE_DOLLARS,
172
161
  DEFAULT_WORKFLOW_SPRINT_PRICE_DOLLARS,
173
- DEFAULT_SNAPSHOT_PRICE_DOLLARS,
174
162
  GA_MEASUREMENT_ID_PATTERN,
175
163
  normalizeAbsoluteUrl,
176
164
  normalizeOrigin,