thumbgate 1.27.13 → 1.27.14
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/agent-readiness.js +142 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thumbgate",
|
|
3
|
-
"version": "1.27.
|
|
3
|
+
"version": "1.27.14",
|
|
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": {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
5
6
|
const path = require('path');
|
|
6
7
|
const {
|
|
7
8
|
getActiveMcpProfile,
|
|
@@ -165,6 +166,136 @@ function summarizePermissionTier(profileName = getActiveMcpProfile()) {
|
|
|
165
166
|
};
|
|
166
167
|
}
|
|
167
168
|
|
|
169
|
+
function flattenHookCommands(value, commands = []) {
|
|
170
|
+
if (!value) return commands;
|
|
171
|
+
if (typeof value === 'string') {
|
|
172
|
+
commands.push(value);
|
|
173
|
+
return commands;
|
|
174
|
+
}
|
|
175
|
+
if (Array.isArray(value)) {
|
|
176
|
+
value.forEach((item) => flattenHookCommands(item, commands));
|
|
177
|
+
return commands;
|
|
178
|
+
}
|
|
179
|
+
if (typeof value === 'object') {
|
|
180
|
+
if (typeof value.command === 'string') commands.push(value.command);
|
|
181
|
+
Object.entries(value)
|
|
182
|
+
.filter(([key]) => key !== 'command')
|
|
183
|
+
.forEach(([, item]) => flattenHookCommands(item, commands));
|
|
184
|
+
}
|
|
185
|
+
return commands;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function readJsonIfExists(filePath) {
|
|
189
|
+
try {
|
|
190
|
+
if (!fs.existsSync(filePath)) return null;
|
|
191
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
192
|
+
} catch (_) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function detectHookAutoCapture(projectRoot) {
|
|
198
|
+
const effectiveRoot = projectRoot || findProjectRoot();
|
|
199
|
+
const settingsFiles = ['.claude/settings.json', '.claude/settings.local.json']
|
|
200
|
+
.map((file) => path.join(effectiveRoot, file))
|
|
201
|
+
.filter((file) => fs.existsSync(file));
|
|
202
|
+
const commands = settingsFiles.flatMap((file) => {
|
|
203
|
+
const parsed = readJsonIfExists(file);
|
|
204
|
+
return flattenHookCommands(parsed && parsed.hooks && parsed.hooks.UserPromptSubmit);
|
|
205
|
+
});
|
|
206
|
+
const wiredCommands = commands.filter((command) => /hook-auto-capture(\.sh)?\b/.test(command));
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
ready: wiredCommands.length > 0,
|
|
210
|
+
settingsFiles: settingsFiles.map((file) => path.relative(effectiveRoot, file)),
|
|
211
|
+
wiredCommands,
|
|
212
|
+
recommendation: wiredCommands.length > 0
|
|
213
|
+
? 'UserPromptSubmit auto-capture hook is wired for typed thumbs feedback.'
|
|
214
|
+
: 'Run `npx thumbgate init --wire-hooks` so typed thumbs up/down feedback is captured automatically.',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function probeFeedbackCapture(projectRoot) {
|
|
219
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'thumbgate-doctor-feedback-'));
|
|
220
|
+
const previousFeedbackDir = process.env.THUMBGATE_FEEDBACK_DIR;
|
|
221
|
+
const previousProjectDir = process.env.THUMBGATE_PROJECT_DIR;
|
|
222
|
+
const previousNoNudge = process.env.THUMBGATE_NO_NUDGE;
|
|
223
|
+
|
|
224
|
+
process.env.THUMBGATE_FEEDBACK_DIR = tempDir;
|
|
225
|
+
process.env.THUMBGATE_PROJECT_DIR = projectRoot || findProjectRoot();
|
|
226
|
+
process.env.THUMBGATE_NO_NUDGE = '1';
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const {
|
|
230
|
+
captureFeedback,
|
|
231
|
+
getFeedbackPaths,
|
|
232
|
+
readJSONL,
|
|
233
|
+
} = require('./feedback-loop');
|
|
234
|
+
const result = captureFeedback({
|
|
235
|
+
signal: 'down',
|
|
236
|
+
context: 'doctor feedback loop probe: prove thumbs feedback persists to durable logs',
|
|
237
|
+
whatWentWrong: 'A runtime claimed thumbs feedback worked without verifying durable capture',
|
|
238
|
+
whatToChange: 'Run thumbgate doctor or feedback-self-test before claiming the loop is wired',
|
|
239
|
+
tags: 'doctor,feedback-loop-probe',
|
|
240
|
+
});
|
|
241
|
+
const paths = getFeedbackPaths();
|
|
242
|
+
const feedbackId = result.feedbackEvent && result.feedbackEvent.id;
|
|
243
|
+
const memoryId = result.memoryRecord && result.memoryRecord.id;
|
|
244
|
+
const feedbackStored = Boolean(feedbackId && readJSONL(paths.FEEDBACK_LOG_PATH).some((row) => row.id === feedbackId));
|
|
245
|
+
const memoryStored = Boolean(memoryId && readJSONL(paths.MEMORY_LOG_PATH).some((row) => row.id === memoryId));
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
ready: Boolean(result.accepted && feedbackStored && memoryStored),
|
|
249
|
+
accepted: Boolean(result.accepted),
|
|
250
|
+
feedbackStored,
|
|
251
|
+
memoryStored,
|
|
252
|
+
feedbackId: feedbackId || null,
|
|
253
|
+
memoryId: memoryId || null,
|
|
254
|
+
mode: 'isolated-temp-store',
|
|
255
|
+
recommendation: (result.accepted && feedbackStored && memoryStored)
|
|
256
|
+
? 'Feedback capture writes durable feedback and memory records.'
|
|
257
|
+
: 'Feedback capture did not persist both feedback and memory records; run `npx thumbgate feedback-self-test --json`.',
|
|
258
|
+
};
|
|
259
|
+
} catch (error) {
|
|
260
|
+
return {
|
|
261
|
+
ready: false,
|
|
262
|
+
accepted: false,
|
|
263
|
+
feedbackStored: false,
|
|
264
|
+
memoryStored: false,
|
|
265
|
+
feedbackId: null,
|
|
266
|
+
memoryId: null,
|
|
267
|
+
mode: 'isolated-temp-store',
|
|
268
|
+
error: error && error.message ? error.message : String(error),
|
|
269
|
+
recommendation: 'Feedback capture probe failed; run `npx thumbgate feedback-self-test --json` for details.',
|
|
270
|
+
};
|
|
271
|
+
} finally {
|
|
272
|
+
if (previousFeedbackDir === undefined) delete process.env.THUMBGATE_FEEDBACK_DIR;
|
|
273
|
+
else process.env.THUMBGATE_FEEDBACK_DIR = previousFeedbackDir;
|
|
274
|
+
if (previousProjectDir === undefined) delete process.env.THUMBGATE_PROJECT_DIR;
|
|
275
|
+
else process.env.THUMBGATE_PROJECT_DIR = previousProjectDir;
|
|
276
|
+
if (previousNoNudge === undefined) delete process.env.THUMBGATE_NO_NUDGE;
|
|
277
|
+
else process.env.THUMBGATE_NO_NUDGE = previousNoNudge;
|
|
278
|
+
try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch (_) { /* ignore cleanup failure */ }
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function assessFeedbackLoop(projectRoot) {
|
|
283
|
+
const hookAutoCapture = detectHookAutoCapture(projectRoot);
|
|
284
|
+
const captureProbe = probeFeedbackCapture(projectRoot);
|
|
285
|
+
const ready = hookAutoCapture.ready && captureProbe.ready;
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
ready,
|
|
289
|
+
automatic: hookAutoCapture.ready,
|
|
290
|
+
durableCapture: captureProbe.ready,
|
|
291
|
+
hookAutoCapture,
|
|
292
|
+
captureProbe,
|
|
293
|
+
recommendation: ready
|
|
294
|
+
? 'Typed thumbs feedback is automatically detected and durable capture is verified.'
|
|
295
|
+
: 'Install hooks and verify capture before claiming thumbs feedback improves future runs.',
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
168
299
|
function generateAgentReadinessReport({
|
|
169
300
|
projectRoot = PROJECT_ROOT,
|
|
170
301
|
mcpProfile = null,
|
|
@@ -172,11 +303,13 @@ function generateAgentReadinessReport({
|
|
|
172
303
|
const runtime = detectRuntimeIsolation();
|
|
173
304
|
const bootstrap = collectBootstrapFiles(projectRoot);
|
|
174
305
|
const permissions = summarizePermissionTier(mcpProfile || getActiveMcpProfile());
|
|
306
|
+
const feedbackLoop = assessFeedbackLoop(projectRoot);
|
|
175
307
|
|
|
176
308
|
const warnings = [];
|
|
177
309
|
if (!runtime.isolated) warnings.push(runtime.recommendation);
|
|
178
310
|
if (!bootstrap.ready) warnings.push(bootstrap.recommendation);
|
|
179
311
|
if (!permissions.ready) warnings.push(permissions.recommendation);
|
|
312
|
+
if (!feedbackLoop.ready) warnings.push(feedbackLoop.recommendation);
|
|
180
313
|
|
|
181
314
|
return {
|
|
182
315
|
generatedAt: new Date().toISOString(),
|
|
@@ -185,10 +318,12 @@ function generateAgentReadinessReport({
|
|
|
185
318
|
runtime,
|
|
186
319
|
bootstrap,
|
|
187
320
|
permissions,
|
|
321
|
+
feedbackLoop,
|
|
188
322
|
articleAlignment: {
|
|
189
323
|
runtimeIsolation: runtime.isolated,
|
|
190
324
|
contextConditioning: bootstrap.ready,
|
|
191
325
|
permissionEnvelope: permissions.ready,
|
|
326
|
+
feedbackLoop: feedbackLoop.ready,
|
|
192
327
|
},
|
|
193
328
|
warnings,
|
|
194
329
|
};
|
|
@@ -208,6 +343,10 @@ function reportToText(report) {
|
|
|
208
343
|
lines.push(`Permissions: ${report.permissions.profile} (${report.permissions.tier})`);
|
|
209
344
|
lines.push(` Write-capable tools: ${report.permissions.writeCapableTools.length}`);
|
|
210
345
|
lines.push(` Recommendation: ${report.permissions.recommendation}`);
|
|
346
|
+
lines.push(`Feedback loop: ${report.feedbackLoop.ready ? 'ready' : 'needs_attention'}`);
|
|
347
|
+
lines.push(` Auto-capture hook: ${report.feedbackLoop.automatic ? 'wired' : 'missing'}`);
|
|
348
|
+
lines.push(` Durable capture: ${report.feedbackLoop.durableCapture ? 'verified' : 'failed'}`);
|
|
349
|
+
lines.push(` Recommendation: ${report.feedbackLoop.recommendation}`);
|
|
211
350
|
|
|
212
351
|
if (report.warnings.length > 0) {
|
|
213
352
|
lines.push('');
|
|
@@ -226,6 +365,9 @@ module.exports = {
|
|
|
226
365
|
detectRuntimeIsolation,
|
|
227
366
|
collectBootstrapFiles,
|
|
228
367
|
summarizePermissionTier,
|
|
368
|
+
detectHookAutoCapture,
|
|
369
|
+
probeFeedbackCapture,
|
|
370
|
+
assessFeedbackLoop,
|
|
229
371
|
generateAgentReadinessReport,
|
|
230
372
|
reportToText,
|
|
231
373
|
};
|