thumbgate 1.27.11 → 1.27.12

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,23 @@
1
+ {
2
+ "version": 1,
3
+ "lessons": [
4
+ {
5
+ "id": "builtin-response-quality-shallow-positive-closeout",
6
+ "title": "MISTAKE: Assistant gave shallow acknowledgement after user said thumbs up",
7
+ "content": "What went wrong: Assistant gave shallow acknowledgement after positive feedback, such as thumbs up, perfect, good, or thank you, instead of staying quiet or giving an evidence checkpoint.\nHow to avoid: If the previous user message is positive feedback and the proposed final response is a low-value social closeout, block it and require either silence-level brevity or a compact evidence checkpoint with proof, result, residual risk, and next state.\nAction needed: Enforce this at the final-response boundary, not only as PreToolUse context.\nReasoning: Stored lessons and retrieval are not enough when the model can still ignore the lesson at generation time.",
8
+ "category": "error",
9
+ "importance": "high",
10
+ "tags": [
11
+ "feedback",
12
+ "negative",
13
+ "response-quality",
14
+ "final-response",
15
+ "positive-feedback",
16
+ "shallow-closeout",
17
+ "enforcement"
18
+ ],
19
+ "timestamp": "2026-06-20T19:01:58.000Z",
20
+ "source": "packaged-builtin"
21
+ }
22
+ ]
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thumbgate",
3
- "version": "1.27.11",
3
+ "version": "1.27.12",
4
4
  "description": "ThumbGate self-improving agent governance: thumbs-up/down turns every mistake into a prevention rule and blocks repeat patterns. 36 pre-action checks, budget enforcement, and self-protection for Claude Code, Cursor, Codex, Gemini CLI, and Amp.",
5
5
  "homepage": "https://thumbgate.ai",
6
6
  "repository": {
@@ -16,6 +16,13 @@ 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');
19
26
  const {
20
27
  recordDecisionEvaluation,
21
28
  recordDecisionOutcome,
@@ -2585,6 +2592,39 @@ function buildReminderOutput(context) {
2585
2592
  });
2586
2593
  }
2587
2594
 
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
+
2588
2628
  // ---------------------------------------------------------------------------
2589
2629
  // Upgrade nudge: surfaces Pro value at usage milestones and trial expiry.
2590
2630
  // Block-action Pro CTA: brief upgrade mention after a deny/warn decision.
@@ -2915,6 +2955,9 @@ function mergeContextStrings(...ctxs) {
2915
2955
  }
2916
2956
 
2917
2957
  async function runAsync(input) {
2958
+ const responseQualityGate = evaluateFinalResponseQualityGate(input);
2959
+ if (responseQualityGate) return responseQualityGate;
2960
+
2918
2961
  const secretGuard = evaluateSecretGuard(input);
2919
2962
  if (secretGuard) {
2920
2963
  return formatOutput(secretGuard);
@@ -2962,6 +3005,9 @@ async function runAsync(input) {
2962
3005
  }
2963
3006
 
2964
3007
  function run(input) {
3008
+ const responseQualityGate = evaluateFinalResponseQualityGate(input);
3009
+ if (responseQualityGate) return responseQualityGate;
3010
+
2965
3011
  const secretGuard = evaluateSecretGuard(input);
2966
3012
  if (secretGuard) {
2967
3013
  return formatOutput(secretGuard);
@@ -3296,6 +3342,9 @@ module.exports = {
3296
3342
  evaluateGatesAsync,
3297
3343
  computeExecutableHash,
3298
3344
  formatOutput,
3345
+ inferHookEventName,
3346
+ buildResponseQualityBlockOutput,
3347
+ evaluateFinalResponseQualityGate,
3299
3348
  isApprovalGatesEnabled,
3300
3349
  run,
3301
3350
  runAsync,
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const path = require('node:path');
4
+ const fs = require('node:fs');
4
5
  const { readJSONL, getFeedbackPaths } = require('./feedback-loop');
5
6
  const { buildMemoryLifecycleView, scoreHybridMemoryMatch } = require('./agent-memory-lifecycle');
6
7
  const { loadOptionalModule } = require('./private-core-boundary');
@@ -166,6 +167,17 @@ function resolveLessonPaths(options = {}) {
166
167
  };
167
168
  }
168
169
 
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
+
169
181
  function readPreventionRuleMatches(queryText, limit = 3, options = {}) {
170
182
  const { PREVENTION_RULES_PATH } = resolveLessonPaths(options);
171
183
  if (!PREVENTION_RULES_PATH) return [];
@@ -481,7 +493,9 @@ function searchLessons(query = '', options = {}) {
481
493
  const sqliteResults = tryFts5Search(query, options);
482
494
  if (sqliteResults) return sqliteResults;
483
495
 
484
- const memories = readJSONL(MEMORY_LOG_PATH);
496
+ const localMemories = readJSONL(MEMORY_LOG_PATH);
497
+ const builtinMemories = options.includeBuiltinLessons === false ? [] : readPackagedBuiltinLessons();
498
+ const memories = [...builtinMemories, ...localMemories];
485
499
  const feedbackEntries = readJSONL(FEEDBACK_LOG_PATH);
486
500
  const feedbackById = new Map(feedbackEntries.map((entry) => [entry.id, entry]));
487
501
  const parsedLimit = Number(options.limit || 10);