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 +1 -1
- package/scripts/gate-stats.js +11 -5
- package/scripts/hook-stop-anti-claim.js +93 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thumbgate",
|
|
3
|
-
"version": "1.27.
|
|
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": {
|
package/scripts/gate-stats.js
CHANGED
|
@@ -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
|
|
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
|
|
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' &&
|
|
51
|
-
.sort((a, b) => (b.occurrences
|
|
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 =
|
|
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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* the turn
|
|
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
|
-
|
|
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,
|