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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thumbgate",
3
- "version": "1.27.13",
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
  };