thumbgate 1.27.12 → 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 (132) 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/gate-templates.json +0 -228
  12. package/config/gates/claim-verification.json +0 -18
  13. package/package.json +35 -25
  14. package/public/assets/brand/thumbgate-logo-transparent.svg +22 -0
  15. package/public/assets/brand/thumbgate-mark-inline-v3.svg +19 -0
  16. package/public/assets/brand/thumbgate-mark.svg +11 -5
  17. package/public/blog.html +0 -30
  18. package/public/brand/thumbgate-mark.svg +9 -5
  19. package/public/chatgpt-app.html +2 -2
  20. package/public/compare.html +2 -1
  21. package/public/dashboard.html +1 -1
  22. package/public/federal.html +1 -1
  23. package/public/index.html +95 -216
  24. package/public/learn.html +59 -35
  25. package/public/lessons.html +1 -1
  26. package/public/numbers.html +2 -2
  27. package/public/pro.html +7 -7
  28. package/scripts/aws-blocks-guardrails.js +228 -0
  29. package/scripts/cli-schema.js +22 -10
  30. package/scripts/dashboard-chat.js +2 -1
  31. package/scripts/document-intake.js +1 -49
  32. package/scripts/durability/step.js +3 -3
  33. package/scripts/gate-stats.js +5 -11
  34. package/scripts/gates-engine.js +0 -49
  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/lesson-search.js +1 -15
  39. package/scripts/llm-client.js +187 -5
  40. package/scripts/plausible-domain-config.js +3 -1
  41. package/scripts/seo-gsd.js +240 -1
  42. package/scripts/tool-registry.js +2 -2
  43. package/scripts/vector-store.js +44 -0
  44. package/scripts/workspace-evolver.js +62 -2
  45. package/src/api/server.js +340 -131
  46. package/public/assets/brand/thumbgate-mark-inline.svg +0 -15
  47. package/public/compare/adopt-ai.html +0 -219
  48. package/public/compare/agentix-labs.html +0 -197
  49. package/public/compare/ai-experience-orchestration.html +0 -216
  50. package/public/compare/anthropic-claude-for-legal.html +0 -260
  51. package/public/compare/anthropic-containment.html +0 -280
  52. package/public/compare/arcade.html +0 -175
  53. package/public/compare/arcjet.html +0 -239
  54. package/public/compare/bumblebee.html +0 -307
  55. package/public/compare/claude-code-hooks.html +0 -294
  56. package/public/compare/databricks-unity-ai-gateway.html +0 -215
  57. package/public/compare/fallow.html +0 -351
  58. package/public/compare/heidi.html +0 -233
  59. package/public/compare/mem0.html +0 -342
  60. package/public/compare/oak-and-sparrow-gatekeeper.html +0 -289
  61. package/public/compare/rein.html +0 -236
  62. package/public/compare/sigmashake.html +0 -256
  63. package/public/compare/speclock.html +0 -342
  64. package/public/guides/agent-harness-optimization.html +0 -342
  65. package/public/guides/agentic-web-governance.html +0 -406
  66. package/public/guides/ai-agent-governance-sprint.html +0 -415
  67. package/public/guides/ai-agent-pre-action-approval-gates.html +0 -401
  68. package/public/guides/ai-agent-workflow-migration-checklist.html +0 -392
  69. package/public/guides/ai-deployment-readiness.html +0 -415
  70. package/public/guides/ai-mode-ads-agent-governance.html +0 -401
  71. package/public/guides/ai-search-topical-presence.html +0 -342
  72. package/public/guides/autoresearch-agent-safety.html +0 -342
  73. package/public/guides/background-agent-governance.html +0 -358
  74. package/public/guides/best-tools-stop-ai-agents-breaking-production.html +0 -363
  75. package/public/guides/browser-automation-safety.html +0 -342
  76. package/public/guides/chatgpt-ads-trust.html +0 -353
  77. package/public/guides/claude-code-feedback.html +0 -339
  78. package/public/guides/claude-code-prevent-repeated-mistakes.html +0 -161
  79. package/public/guides/claude-code-skills-guardrails.html +0 -343
  80. package/public/guides/claude-desktop.html +0 -356
  81. package/public/guides/code-knowledge-graph-guardrails.html +0 -365
  82. package/public/guides/codex-cli-guardrails.html +0 -339
  83. package/public/guides/cursor-agent-guardrails.html +0 -339
  84. package/public/guides/cursor-prevent-repeated-mistakes.html +0 -161
  85. package/public/guides/database-agent-safety.html +0 -406
  86. package/public/guides/deepseek-v4-runtime-guardrails.html +0 -346
  87. package/public/guides/developer-machine-supply-chain-guardrails.html +0 -358
  88. package/public/guides/gcp-mcp-guardrails.html +0 -147
  89. package/public/guides/gemini-cli-feedback-memory.html +0 -339
  90. package/public/guides/gpt-5-5-model-evaluation.html +0 -358
  91. package/public/guides/internal-ai-engineering-stack-guardrails.html +0 -348
  92. package/public/guides/long-running-agent-context-management.html +0 -346
  93. package/public/guides/mcp-tool-governance.html +0 -401
  94. package/public/guides/multica-thumbgate-setup.html +0 -134
  95. package/public/guides/native-messaging-host-security.html +0 -342
  96. package/public/guides/policy-engine-pre-action-gates.html +0 -346
  97. package/public/guides/pre-action-checks.html +0 -342
  98. package/public/guides/pretooluse-hooks-vs-advisory-prompt-rules.html +0 -342
  99. package/public/guides/prompt-tricks-to-workflow-rules.html +0 -365
  100. package/public/guides/proxy-pointer-rag-guardrails.html +0 -352
  101. package/public/guides/rag-precision-tuning-guardrails.html +0 -352
  102. package/public/guides/reasoning-compression-guardrails.html +0 -346
  103. package/public/guides/relational-knowledge-ai-recommendations.html +0 -342
  104. package/public/guides/roo-code-alternative-cline.html +0 -339
  105. package/public/guides/semantic-programmatic-seo-guardrails.html +0 -352
  106. package/public/guides/seo-agent-skills-guardrails.html +0 -344
  107. package/public/guides/stop-repeated-ai-agent-mistakes.html +0 -342
  108. package/public/learn/ac-dc-runtime-enforcement.html +0 -277
  109. package/public/learn/agent-harness-pattern.html +0 -181
  110. package/public/learn/agent-identity-connector-governance.html +0 -146
  111. package/public/learn/agent-swarms-shared-gates.html +0 -173
  112. package/public/learn/agentic-enterprise-context-brain.html +0 -117
  113. package/public/learn/agentic-os-team-governance.html +0 -146
  114. package/public/learn/ai-agent-governance.html +0 -158
  115. package/public/learn/ai-agent-persistent-memory.html +0 -211
  116. package/public/learn/anthropomorphic-claim-gates.html +0 -180
  117. package/public/learn/background-agent-control-layer.html +0 -184
  118. package/public/learn/claude-code-goal-with-rubrics.html +0 -205
  119. package/public/learn/codex-role-plugins-need-governance.html +0 -125
  120. package/public/learn/cost-aware-agent-gate-routing.html +0 -173
  121. package/public/learn/databricks-unity-ai-gateway-runtime-governance.html +0 -157
  122. package/public/learn/deterministic-agent-workflows.html +0 -185
  123. package/public/learn/feedback-loop-vs-decision-layer.html +0 -283
  124. package/public/learn/from-prototype-to-production.html +0 -223
  125. package/public/learn/learn.css +0 -51
  126. package/public/learn/mcp-pre-action-checks-explained.html +0 -172
  127. package/public/learn/pretix-stripe-connect-marketplaces.html +0 -161
  128. package/public/learn/regulated-agent-execution-boundary.html +0 -196
  129. package/public/learn/spec-driven-development.html +0 -168
  130. package/public/learn/stop-ai-agent-force-push.html +0 -134
  131. package/public/learn/vibe-coding-safety-net.html +0 -142
  132. 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
  };
@@ -16,13 +16,6 @@ const {
16
16
  const {
17
17
  evaluateWorkflowSentinel,
18
18
  } = require('./workflow-sentinel');
19
- const {
20
- extractPayloadText,
21
- extractPayloadPreviousUserText,
22
- hasPositiveFeedback,
23
- isLowValueCloseout,
24
- buildResponseQualityReason,
25
- } = require('./hook-stop-anti-claim');
26
19
  const {
27
20
  recordDecisionEvaluation,
28
21
  recordDecisionOutcome,
@@ -2592,39 +2585,6 @@ function buildReminderOutput(context) {
2592
2585
  });
2593
2586
  }
2594
2587
 
2595
- function inferHookEventName(input = {}) {
2596
- const explicit = input.hook_event_name || input.hookEventName || input.event || input.lifecycle;
2597
- if (explicit) return String(explicit);
2598
- return extractPayloadText(input) ? 'Stop' : 'PreToolUse';
2599
- }
2600
-
2601
- function buildResponseQualityBlockOutput(reason, input = {}) {
2602
- return JSON.stringify({
2603
- decision: 'block',
2604
- reason,
2605
- hookSpecificOutput: {
2606
- hookEventName: inferHookEventName(input),
2607
- permissionDecision: 'deny',
2608
- permissionDecisionReason: reason,
2609
- },
2610
- });
2611
- }
2612
-
2613
- function evaluateFinalResponseQualityGate(input = {}) {
2614
- const finalText = extractPayloadText(input) || process.env.CLAUDE_RESPONSE || '';
2615
- const previousUserText = extractPayloadPreviousUserText(input)
2616
- || process.env.CLAUDE_PREVIOUS_USER_TEXT
2617
- || process.env.CLAUDE_PREVIOUS_USER
2618
- || '';
2619
-
2620
- if (!finalText || !hasPositiveFeedback(previousUserText)) return null;
2621
- if (!isLowValueCloseout(finalText, '')) return null;
2622
- recordStat('response-quality-shallow-closeout', 'block', null, {
2623
- hookEventName: inferHookEventName(input),
2624
- });
2625
- return buildResponseQualityBlockOutput(buildResponseQualityReason(), input);
2626
- }
2627
-
2628
2588
  // ---------------------------------------------------------------------------
2629
2589
  // Upgrade nudge: surfaces Pro value at usage milestones and trial expiry.
2630
2590
  // Block-action Pro CTA: brief upgrade mention after a deny/warn decision.
@@ -2955,9 +2915,6 @@ function mergeContextStrings(...ctxs) {
2955
2915
  }
2956
2916
 
2957
2917
  async function runAsync(input) {
2958
- const responseQualityGate = evaluateFinalResponseQualityGate(input);
2959
- if (responseQualityGate) return responseQualityGate;
2960
-
2961
2918
  const secretGuard = evaluateSecretGuard(input);
2962
2919
  if (secretGuard) {
2963
2920
  return formatOutput(secretGuard);
@@ -3005,9 +2962,6 @@ async function runAsync(input) {
3005
2962
  }
3006
2963
 
3007
2964
  function run(input) {
3008
- const responseQualityGate = evaluateFinalResponseQualityGate(input);
3009
- if (responseQualityGate) return responseQualityGate;
3010
-
3011
2965
  const secretGuard = evaluateSecretGuard(input);
3012
2966
  if (secretGuard) {
3013
2967
  return formatOutput(secretGuard);
@@ -3342,9 +3296,6 @@ module.exports = {
3342
3296
  evaluateGatesAsync,
3343
3297
  computeExecutableHash,
3344
3298
  formatOutput,
3345
- inferHookEventName,
3346
- buildResponseQualityBlockOutput,
3347
- evaluateFinalResponseQualityGate,
3348
3299
  isApprovalGatesEnabled,
3349
3300
  run,
3350
3301
  runAsync,
@@ -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,
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  const path = require('node:path');
4
- const fs = require('node:fs');
5
4
  const { readJSONL, getFeedbackPaths } = require('./feedback-loop');
6
5
  const { buildMemoryLifecycleView, scoreHybridMemoryMatch } = require('./agent-memory-lifecycle');
7
6
  const { loadOptionalModule } = require('./private-core-boundary');
@@ -167,17 +166,6 @@ function resolveLessonPaths(options = {}) {
167
166
  };
168
167
  }
169
168
 
170
- function readPackagedBuiltinLessons() {
171
- if (process.env.THUMBGATE_DISABLE_BUILTIN_LESSONS === '1') return [];
172
- const builtinPath = path.resolve(__dirname, '..', 'config', 'builtin-lessons.json');
173
- try {
174
- const parsed = JSON.parse(fs.readFileSync(builtinPath, 'utf8'));
175
- return Array.isArray(parsed.lessons) ? parsed.lessons : [];
176
- } catch {
177
- return [];
178
- }
179
- }
180
-
181
169
  function readPreventionRuleMatches(queryText, limit = 3, options = {}) {
182
170
  const { PREVENTION_RULES_PATH } = resolveLessonPaths(options);
183
171
  if (!PREVENTION_RULES_PATH) return [];
@@ -493,9 +481,7 @@ function searchLessons(query = '', options = {}) {
493
481
  const sqliteResults = tryFts5Search(query, options);
494
482
  if (sqliteResults) return sqliteResults;
495
483
 
496
- const localMemories = readJSONL(MEMORY_LOG_PATH);
497
- const builtinMemories = options.includeBuiltinLessons === false ? [] : readPackagedBuiltinLessons();
498
- const memories = [...builtinMemories, ...localMemories];
484
+ const memories = readJSONL(MEMORY_LOG_PATH);
499
485
  const feedbackEntries = readJSONL(FEEDBACK_LOG_PATH);
500
486
  const feedbackById = new Map(feedbackEntries.map((entry) => [entry.id, entry]));
501
487
  const parsedLimit = Number(options.limit || 10);