thumbgate 1.27.13 → 1.27.15

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.15",
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,141 @@ 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
+ // Keep this probe isolated. Direct project scope intentionally overrides
226
+ // THUMBGATE_FEEDBACK_DIR in the runtime resolver, which is correct for real
227
+ // project writes but wrong for a doctor self-test.
228
+ delete process.env.THUMBGATE_PROJECT_DIR;
229
+ process.env.THUMBGATE_NO_NUDGE = '1';
230
+
231
+ try {
232
+ const {
233
+ captureFeedback,
234
+ getFeedbackPaths,
235
+ readJSONL,
236
+ } = require('./feedback-loop');
237
+ const result = captureFeedback({
238
+ signal: 'down',
239
+ context: 'doctor feedback loop probe: prove thumbs feedback persists to durable logs',
240
+ whatWentWrong: 'A runtime claimed thumbs feedback worked without verifying durable capture',
241
+ whatToChange: 'Run thumbgate doctor or feedback-self-test before claiming the loop is wired',
242
+ tags: 'doctor,feedback-loop-probe',
243
+ });
244
+ const paths = getFeedbackPaths();
245
+ const feedbackId = result.feedbackEvent && result.feedbackEvent.id;
246
+ const memoryId = result.memoryRecord && result.memoryRecord.id;
247
+ const feedbackStored = Boolean(feedbackId && readJSONL(paths.FEEDBACK_LOG_PATH).some((row) => row.id === feedbackId));
248
+ const memoryStored = Boolean(memoryId && readJSONL(paths.MEMORY_LOG_PATH).some((row) => row.id === memoryId));
249
+
250
+ return {
251
+ ready: Boolean(result.accepted && feedbackStored && memoryStored),
252
+ accepted: Boolean(result.accepted),
253
+ feedbackStored,
254
+ memoryStored,
255
+ feedbackId: feedbackId || null,
256
+ memoryId: memoryId || null,
257
+ mode: 'isolated-temp-store',
258
+ feedbackDir: paths.FEEDBACK_DIR,
259
+ recommendation: (result.accepted && feedbackStored && memoryStored)
260
+ ? 'Feedback capture writes durable feedback and memory records.'
261
+ : 'Feedback capture did not persist both feedback and memory records; run `npx thumbgate feedback-self-test --json`.',
262
+ };
263
+ } catch (error) {
264
+ return {
265
+ ready: false,
266
+ accepted: false,
267
+ feedbackStored: false,
268
+ memoryStored: false,
269
+ feedbackId: null,
270
+ memoryId: null,
271
+ mode: 'isolated-temp-store',
272
+ feedbackDir: tempDir,
273
+ error: error && error.message ? error.message : String(error),
274
+ recommendation: 'Feedback capture probe failed; run `npx thumbgate feedback-self-test --json` for details.',
275
+ };
276
+ } finally {
277
+ if (previousFeedbackDir === undefined) delete process.env.THUMBGATE_FEEDBACK_DIR;
278
+ else process.env.THUMBGATE_FEEDBACK_DIR = previousFeedbackDir;
279
+ if (previousProjectDir === undefined) delete process.env.THUMBGATE_PROJECT_DIR;
280
+ else process.env.THUMBGATE_PROJECT_DIR = previousProjectDir;
281
+ if (previousNoNudge === undefined) delete process.env.THUMBGATE_NO_NUDGE;
282
+ else process.env.THUMBGATE_NO_NUDGE = previousNoNudge;
283
+ try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch (_) { /* ignore cleanup failure */ }
284
+ }
285
+ }
286
+
287
+ function assessFeedbackLoop(projectRoot) {
288
+ const hookAutoCapture = detectHookAutoCapture(projectRoot);
289
+ const captureProbe = probeFeedbackCapture(projectRoot);
290
+ const ready = hookAutoCapture.ready && captureProbe.ready;
291
+
292
+ return {
293
+ ready,
294
+ automatic: hookAutoCapture.ready,
295
+ durableCapture: captureProbe.ready,
296
+ hookAutoCapture,
297
+ captureProbe,
298
+ recommendation: ready
299
+ ? 'Typed thumbs feedback is automatically detected and durable capture is verified.'
300
+ : 'Install hooks and verify capture before claiming thumbs feedback improves future runs.',
301
+ };
302
+ }
303
+
168
304
  function generateAgentReadinessReport({
169
305
  projectRoot = PROJECT_ROOT,
170
306
  mcpProfile = null,
@@ -172,11 +308,13 @@ function generateAgentReadinessReport({
172
308
  const runtime = detectRuntimeIsolation();
173
309
  const bootstrap = collectBootstrapFiles(projectRoot);
174
310
  const permissions = summarizePermissionTier(mcpProfile || getActiveMcpProfile());
311
+ const feedbackLoop = assessFeedbackLoop(projectRoot);
175
312
 
176
313
  const warnings = [];
177
314
  if (!runtime.isolated) warnings.push(runtime.recommendation);
178
315
  if (!bootstrap.ready) warnings.push(bootstrap.recommendation);
179
316
  if (!permissions.ready) warnings.push(permissions.recommendation);
317
+ if (!feedbackLoop.ready) warnings.push(feedbackLoop.recommendation);
180
318
 
181
319
  return {
182
320
  generatedAt: new Date().toISOString(),
@@ -185,10 +323,12 @@ function generateAgentReadinessReport({
185
323
  runtime,
186
324
  bootstrap,
187
325
  permissions,
326
+ feedbackLoop,
188
327
  articleAlignment: {
189
328
  runtimeIsolation: runtime.isolated,
190
329
  contextConditioning: bootstrap.ready,
191
330
  permissionEnvelope: permissions.ready,
331
+ feedbackLoop: feedbackLoop.ready,
192
332
  },
193
333
  warnings,
194
334
  };
@@ -208,6 +348,10 @@ function reportToText(report) {
208
348
  lines.push(`Permissions: ${report.permissions.profile} (${report.permissions.tier})`);
209
349
  lines.push(` Write-capable tools: ${report.permissions.writeCapableTools.length}`);
210
350
  lines.push(` Recommendation: ${report.permissions.recommendation}`);
351
+ lines.push(`Feedback loop: ${report.feedbackLoop.ready ? 'ready' : 'needs_attention'}`);
352
+ lines.push(` Auto-capture hook: ${report.feedbackLoop.automatic ? 'wired' : 'missing'}`);
353
+ lines.push(` Durable capture: ${report.feedbackLoop.durableCapture ? 'verified' : 'failed'}`);
354
+ lines.push(` Recommendation: ${report.feedbackLoop.recommendation}`);
211
355
 
212
356
  if (report.warnings.length > 0) {
213
357
  lines.push('');
@@ -226,6 +370,9 @@ module.exports = {
226
370
  detectRuntimeIsolation,
227
371
  collectBootstrapFiles,
228
372
  summarizePermissionTier,
373
+ detectHookAutoCapture,
374
+ probeFeedbackCapture,
375
+ assessFeedbackLoop,
229
376
  generateAgentReadinessReport,
230
377
  reportToText,
231
378
  };