thumbgate 1.27.10 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thumbgate",
3
- "version": "1.27.10",
3
+ "version": "1.27.11",
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": {
@@ -11,6 +11,11 @@ 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
+
14
19
  function loadGatesFile(filePath) {
15
20
  if (!fs.existsSync(filePath)) return [];
16
21
  try {
@@ -39,16 +44,16 @@ function calculateStats() {
39
44
  // Count total blocks/warns from occurrences in auto-promoted gates
40
45
  const totalBlocked = autoGates
41
46
  .filter((g) => g.action === 'block')
42
- .reduce((sum, g) => sum + (g.occurrences || 0), 0);
47
+ .reduce((sum, g) => sum + safeOccurrenceCount(g.occurrences), 0);
43
48
  const totalWarned = autoGates
44
49
  .filter((g) => g.action === 'warn')
45
- .reduce((sum, g) => sum + (g.occurrences || 0), 0);
50
+ .reduce((sum, g) => sum + safeOccurrenceCount(g.occurrences), 0);
46
51
 
47
52
  // Top blocked gate. A configured block rule with zero occurrences is not a
48
53
  // "top blocker"; only recorded block events should appear here.
49
54
  const topBlocked = [...allGates]
50
- .filter((g) => g.action === 'block' && Number(g.occurrences || 0) > 0)
51
- .sort((a, b) => (b.occurrences || 0) - (a.occurrences || 0))
55
+ .filter((g) => g.action === 'block' && safeOccurrenceCount(g.occurrences) > 0)
56
+ .sort((a, b) => safeOccurrenceCount(b.occurrences) - safeOccurrenceCount(a.occurrences))
52
57
  .at(0) || null;
53
58
 
54
59
  // Last promotion event
@@ -105,7 +110,7 @@ function computeCalibration(gates) {
105
110
  const calibration = [];
106
111
  for (const gate of gates || []) {
107
112
  if (!gate || !gate.id) continue;
108
- const occurrences = Number(gate.occurrences || 0);
113
+ const occurrences = safeOccurrenceCount(gate.occurrences);
109
114
  const action = gate.action || 'unknown';
110
115
  // Only annotate gates with recorded occurrence data
111
116
  if (occurrences === 0) continue;
@@ -258,6 +263,7 @@ module.exports = {
258
263
  loadGatesFile,
259
264
  tryComputeBayesErrorRate,
260
265
  computeCalibration,
266
+ safeOccurrenceCount,
261
267
  MANUAL_GATES_PATH,
262
268
  STATS_PATH,
263
269
  };
@@ -16,10 +16,11 @@
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
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.
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.
23
24
  *
24
25
  * Stdin: Claude Code passes the hook payload as JSON on stdin. We read
25
26
  * `transcript_path` to locate the JSONL session log and scan the last
@@ -203,6 +204,79 @@ function hasProof(combined) {
203
204
  return PROOF_PATTERNS.some((p) => p.test(combined));
204
205
  }
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
+
206
280
  function readStdinSync() {
207
281
  try {
208
282
  return fs.readFileSync(0, 'utf8');
@@ -221,6 +295,17 @@ function main() {
221
295
  }
222
296
 
223
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
+
224
309
  const message = readLastAssistantTurn(transcriptPath);
225
310
  if (!message) return; // no transcript visible; nothing to check
226
311
 
@@ -229,14 +314,7 @@ function main() {
229
314
  const previousUserText = readPreviousUserText(transcriptPath);
230
315
 
231
316
  if (hasPositiveFeedback(previousUserText) && isLowValueCloseout(text, toolUseSummary)) {
232
- const reminder = [
233
- '⚠️ ThumbGate response-quality gate: previous turn answered positive feedback',
234
- ' with a low-value social closeout instead of silence-level brevity or an evidence checkpoint.',
235
- ' Positive feedback after operational work should trigger either no extra noise,',
236
- ' a compact evidence checkpoint, or a concrete next-state update.',
237
- ' Do not reply with generic "Good / Great / Use X/Y" filler.',
238
- ].join('\n');
239
- process.stdout.write(reminder + '\n');
317
+ process.stdout.write(buildResponseQualityReminder() + '\n');
240
318
  return;
241
319
  }
242
320
 
@@ -282,6 +360,9 @@ module.exports = {
282
360
  hasProof,
283
361
  hasPositiveFeedback,
284
362
  isLowValueCloseout,
363
+ extractPayloadText,
364
+ extractPayloadPreviousUserText,
365
+ buildResponseQualityReason,
285
366
  extractText,
286
367
  extractToolUseSummary,
287
368
  readPreviousUserText,