thumbgate 1.6.0 → 1.7.0

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thumbgate-marketplace",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "owner": {
5
5
  "name": "Igor Ganapolsky",
6
6
  "email": "ig5973700@gmail.com"
@@ -13,7 +13,7 @@
13
13
  "source": "npm",
14
14
  "package": "thumbgate"
15
15
  },
16
- "version": "1.6.0",
16
+ "version": "1.7.0",
17
17
  "author": {
18
18
  "name": "Igor Ganapolsky"
19
19
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "thumbgate",
3
3
  "description": "Type 👍 or 👎 on any agent action. ThumbGate captures it, distills a lesson, and blocks the pattern from repeating. One thumbs-down = the agent physically cannot make that mistake again. 33 pre-action gates, budget enforcement, self-protection, and NIST/SOC2 compliance tags.",
4
- "version": "1.6.0",
4
+ "version": "1.7.0",
5
5
  "author": {
6
6
  "name": "Igor Ganapolsky"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thumbgate",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "ThumbGate — 👍👎 feedback that teaches your AI agent. Thumbs down a mistake, it never happens again.",
5
5
  "homepage": "https://github.com/IgorGanapolsky/thumbgate",
6
6
  "transport": "stdio",
@@ -3,7 +3,7 @@
3
3
  - `chatgpt/openapi.yaml`: import into GPT Actions.
4
4
  - `gemini/function-declarations.json`: Gemini function-calling definitions.
5
5
  - `mcp/server-stdio.js`: underlying local MCP stdio server implementation.
6
- - `claude/.mcp.json`: example Claude Code MCP config using `npx --yes --package thumbgate@1.6.0 thumbgate serve`.
6
+ - `claude/.mcp.json`: example Claude Code MCP config using `npx --yes --package thumbgate@1.7.0 thumbgate serve`.
7
7
  - `codex/config.toml`: example Codex MCP profile section using the same version-pinned portable launcher.
8
8
  - `amp/skills/thumbgate-feedback/SKILL.md`: Amp skill template.
9
9
  - `opencode/opencode.json`: portable OpenCode MCP profile using the same version-pinned portable launcher.
@@ -2,13 +2,13 @@
2
2
  "mcpServers": {
3
3
  "thumbgate": {
4
4
  "command": "npx",
5
- "args": ["--yes", "--package", "thumbgate@1.6.0", "thumbgate", "serve"]
5
+ "args": ["--yes", "--package", "thumbgate@1.7.0", "thumbgate", "serve"]
6
6
  }
7
7
  },
8
8
  "hooks": {
9
9
  "preToolUse": {
10
10
  "command": "npx",
11
- "args": ["--yes", "--package", "thumbgate@1.6.0", "thumbgate", "gate-check"]
11
+ "args": ["--yes", "--package", "thumbgate@1.7.0", "thumbgate", "gate-check"]
12
12
  }
13
13
  }
14
14
  }
@@ -146,7 +146,7 @@ const {
146
146
  finalizeSession: finalizeFeedbackSession,
147
147
  } = require('../../scripts/feedback-session');
148
148
 
149
- const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.6.0' };
149
+ const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.7.0' };
150
150
  const COMMERCE_CATEGORIES = [
151
151
  'product_recommendation',
152
152
  'brand_compliance',
@@ -201,6 +201,43 @@ function toTextResult(payload) {
201
201
  };
202
202
  }
203
203
 
204
+ // Format corrective actions as a top-level <system-reminder> block so the
205
+ // calling agent treats them as first-class guidance, not buried JSON.
206
+ // Shape of `actions` varies: lesson-db.inferCorrectiveActions returns
207
+ // {whatToChange, tags, timestamp}; lesson-search.buildSystemActions returns
208
+ // {type, source, text}. Normalize to a single bulleted list.
209
+ function formatCorrectiveActionsReminder(actions) {
210
+ if (!Array.isArray(actions) || actions.length === 0) return '';
211
+ const lines = ['<system-reminder>', 'ThumbGate surfaced prior lessons matching this failure.', 'REVIEW BEFORE YOUR NEXT ACTION:'];
212
+ actions.slice(0, 5).forEach((action, idx) => {
213
+ const text = (action && (action.whatToChange || action.text || action.message)) || '';
214
+ if (!text) return;
215
+ const tags = Array.isArray(action.tags) && action.tags.length > 0 ? ` [${action.tags.join(', ')}]` : '';
216
+ lines.push(`${idx + 1}. ${String(text).trim()}${tags}`);
217
+ });
218
+ lines.push('</system-reminder>', '');
219
+ return lines.join('\n');
220
+ }
221
+
222
+ // Wrap capture_feedback payloads so correctiveActions surface as a
223
+ // top-level <system-reminder> text block appended alongside the JSON body.
224
+ //
225
+ // Ordering: JSON body is content[0] (preserves backward compatibility with
226
+ // callers that parse content[0].text as JSON); the reminder is content[1]
227
+ // so it still appears as a top-level block the agent must process — not
228
+ // buried inside the JSON structure.
229
+ function toCaptureFeedbackTextResult(result) {
230
+ const body = JSON.stringify(result, null, 2);
231
+ const blocks = [{ type: 'text', text: body }];
232
+ const reminder = result && Array.isArray(result.correctiveActions)
233
+ ? formatCorrectiveActionsReminder(result.correctiveActions)
234
+ : '';
235
+ if (reminder) {
236
+ blocks.push({ type: 'text', text: reminder });
237
+ }
238
+ return { content: blocks };
239
+ }
240
+
204
241
  function formatContextPack(pack) {
205
242
  const lines = [
206
243
  '## Context Pack',
@@ -447,7 +484,7 @@ async function callToolInner(name, args) {
447
484
  switch (name) {
448
485
  case 'capture_feedback':
449
486
 
450
- return toTextResult(captureFeedback(args));
487
+ return toCaptureFeedbackTextResult(captureFeedback(args));
451
488
  case 'feedback_summary':
452
489
  return toTextResult(feedbackSummary(Number(args.recent || 20)));
453
490
  case 'search_lessons':
@@ -1008,4 +1045,6 @@ module.exports = {
1008
1045
  callTool,
1009
1046
  startStdioServer,
1010
1047
  acquireLock,
1048
+ toCaptureFeedbackTextResult,
1049
+ formatCorrectiveActionsReminder,
1011
1050
  };
@@ -7,7 +7,7 @@
7
7
  "npx",
8
8
  "--yes",
9
9
  "--package",
10
- "thumbgate@1.6.0",
10
+ "thumbgate@1.7.0",
11
11
  "thumbgate",
12
12
  "serve"
13
13
  ],
package/bin/cli.js CHANGED
@@ -1591,19 +1591,53 @@ function sessionStart() {
1591
1591
  const { refreshStatuslineCache } = require(path.join(PKG_ROOT, 'scripts', 'hook-thumbgate-cache-updater'));
1592
1592
  refreshStatuslineCache(analyzeFeedback());
1593
1593
 
1594
- // Surface gate-program.md active rules so the agent starts aware of what is blocked.
1594
+ // Build a top-level <system-reminder> block that Claude Code's SessionStart
1595
+ // hook surfaces to the agent as first-class context — not buried stderr.
1596
+ // Contract: emit JSON `{hookSpecificOutput:{hookEventName:"SessionStart",
1597
+ // additionalContext:"..."}}` to stdout. Supported by Claude Code v0.4+.
1598
+ const reminderLines = [];
1599
+
1600
+ // Active hard-block rules from gate-program.md
1595
1601
  try {
1596
1602
  const { readGateProgram, extractBlockPatterns } = require(path.join(PKG_ROOT, 'scripts', 'meta-agent-loop'));
1597
1603
  const gateProgram = readGateProgram();
1598
1604
  if (gateProgram) {
1599
1605
  const blockPatterns = extractBlockPatterns(gateProgram);
1600
1606
  if (blockPatterns.length > 0) {
1601
- process.stderr.write('\n[ThumbGate] Active hard-block rules from gate-program.md:\n');
1602
- blockPatterns.forEach((p, i) => process.stderr.write(` ${i + 1}. ${p}\n`));
1603
- process.stderr.write('\n');
1607
+ reminderLines.push('Active ThumbGate hard-block rules:');
1608
+ blockPatterns.forEach((p, i) => reminderLines.push(` ${i + 1}. ${p}`));
1604
1609
  }
1605
1610
  }
1606
- } catch (_) { /* gate-program awareness is best-effort */ }
1611
+ } catch (_) { /* non-critical */ }
1612
+
1613
+ // Top high-risk tags — force agent to see them at session start, not opt-in
1614
+ try {
1615
+ const { getRiskSummary } = require(path.join(PKG_ROOT, 'scripts', 'risk-scorer'));
1616
+ const summary = getRiskSummary();
1617
+ if (summary && Array.isArray(summary.highRiskTags) && summary.highRiskTags.length > 0) {
1618
+ if (reminderLines.length > 0) reminderLines.push('');
1619
+ reminderLines.push('Top high-risk tags from prior failures:');
1620
+ summary.highRiskTags.slice(0, 5).forEach((bucket, i) => {
1621
+ const key = bucket && (bucket.key || bucket.tag);
1622
+ const score = bucket && (bucket.risk || bucket.score || bucket.riskScore);
1623
+ if (key) reminderLines.push(` ${i + 1}. ${key} (risk=${score || '?'})`);
1624
+ });
1625
+ }
1626
+ } catch (_) { /* non-critical */ }
1627
+
1628
+ if (reminderLines.length > 0) {
1629
+ const additionalContext = ['<system-reminder>', ...reminderLines, '</system-reminder>'].join('\n');
1630
+ try {
1631
+ process.stdout.write(JSON.stringify({
1632
+ hookSpecificOutput: {
1633
+ hookEventName: 'SessionStart',
1634
+ additionalContext,
1635
+ },
1636
+ }));
1637
+ } catch (_) { /* stdout write failure is non-critical */ }
1638
+ // Legacy stderr fallback for older Claude Code versions
1639
+ process.stderr.write('\n[ThumbGate] ' + reminderLines.join('\n[ThumbGate] ') + '\n');
1640
+ }
1607
1641
  }
1608
1642
 
1609
1643
  function installMcp() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thumbgate",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Self-improving agent governance: type thumbs-up or thumbs-down on any AI agent action. ThumbGate turns every mistake into a prevention rule and blocks the pattern from repeating. One thumbs-down, never again. 33 pre-action gates, budget enforcement, and self-protection for Claude Code, Cursor, Codex, Gemini CLI, and Amp.",
5
5
  "homepage": "https://thumbgate-production.up.railway.app",
6
6
  "repository": {
@@ -251,7 +251,7 @@
251
251
  "trace:eval": "node scripts/decision-trace.js eval",
252
252
  "social:reply-monitor": "node scripts/social-reply-monitor.js",
253
253
  "social:reply-monitor:dry": "node scripts/social-reply-monitor.js --dry-run",
254
- "test": "npm run test:schema && npm run test:loop && npm run test:dpo && npm run test:kto && npm run test:api && npm run test:proof && npm run test:e2e && npm run test:rlaif && npm run test:attribution && npm run test:quality && npm run test:intelligence && npm run test:training-export && npm run test:deployment && npm run test:operational-integrity && npm run test:workflow && npm run test:billing && npm run test:cli && npm run test:watcher && npm run test:autoresearch && npm run test:ops && npm run test:session-analyzer && npm run test:tessl && npm run test:gates && npm run test:evoskill && npm run test:gates-hardening && npm run test:workers && npm run test:social-analytics && npm run test:memalign && npm run test:xmemory-lite && npm run test:filesystem-search && npm run test:zernio && npm run test:platform-limits && npm run test:post-video && npm run test:post-everywhere-instagram && npm run test:obsidian-export && npm run test:lesson-db && npm run test:lesson-rotation && npm run test:memory-dedup && npm run test:feedback-quality && npm run test:sync-version && npm run test:check-congruence && npm run test:tool-registry && npm run test:feedback-to-rules && npm run test:memory-firewall && npm run test:belief-update && npm run test:hosted-config && npm run test:operational-summary && npm run test:operator-key-auth && npm run test:cloudflare-sandbox && npm run test:mcp-config && npm run test:plan-gate && npm run test:pulse && npm run test:semantic-layer && npm run test:data-pipeline && npm run test:optimize-context && npm run test:principle-extractor && npm run test:analytics-window && npm run test:funnel-analytics && npm run test:experiment-tracker && npm run test:build-metadata && npm run test:context-engine && npm run test:hf-papers && npm run test:marketing-experiment && npm run test:seo-gsd && npm run test:verify-run && npm run test:export-dpo-pairs && npm run test:export-hf-dataset && npm run test:license && npm run test:bot-detector && npm run test:postinstall && npm run test:funnel-invariants && npm run test:cli-telemetry && npm run test:pro-parity && npm run test:model-tier-router && npm run test:computer-use-firewall && npm run test:skill-exporter && npm run test:statusline && npm run test:evolution && npm run test:org-dashboard && npm run test:multi-hop-recall && npm run test:synthetic-dpo && npm run test:thumbgate-skill && npm run test:learn-hub && npm run test:feedback-fallback && npm run test:metaclaw && npm run test:server-lock && npm run test:control-tower && npm run test:pii-scanner && npm run test:data-governance && npm run test:lesson-inference && npm run test:semantic-dedup && npm run test:fs-utils && npm run test:cli-schema && npm run test:explore && npm run test:lesson-reranker && npm run test:lesson-retrieval && npm run test:cross-encoder && npm run test:reflector-agent && npm run test:feedback-session && npm run test:feedback-history-distiller && npm run test:hallucination-detector && npm run test:history-distiller && npm run test:predictive-insights && npm run test:prove-predictive-insights && npm run test:statusbar-cli && npm run test:generate-instagram-card && npm run test:instagram-thumbgate-post && npm run test:publish-instagram-thumbgate && npm run test:lesson-synthesis && npm run test:background-governance && npm run test:memory-migration && npm run test:prompt-dlp && npm run test:ephemeral-store && npm run test:agent-security && npm run test:skill-progressive && npm run test:per-step-scoring && npm run test:weekly-auto-post && npm run test:social-post-hourly && npm run test:social-quality-gate && npm run test:a2ui-engine && npm run test:gate-satisfy && npm run test:money-watcher && npm run test:budget && npm run test:quick-start && npm run test:utm && npm run test:product-feedback && npm run test:feedback-root-consolidator && npm run test:engagement-audit && npm run test:install-growth-automation && npm run test:publish-thumbgate-launch && npm run test:reconcile-thumbgate-campaign && npm run test:reddit-publisher && npm run test:schedule-thumbgate-campaign && npm run test:social-reply-monitor && npm run test:sync-launch-assets && npm run test:ai-search-visibility && npm run test:perplexity && npm run test:security-scanner && npm run test:llm-client && npm run test:managed-lesson-agent && npm run test:self-distill && npm run test:meta-agent && npm run test:harness-selector && npm run test:thumbgate-bench && npm run test:seo-guides && npm run test:enforcement-loop && npm run test:cli-agent-experience && npm run test:bot-detection && npm run test:checkout-bot-guard && npm run test:session-health && npm run test:session-episodes && npm run test:spec-gate && npm run test:decision-trace && npm run test:dashboard-insights && npm run test:prompt-eval && npm run test:demo-voiceover && npm run test:gate-coherence && npm run test:gate-eval && npm run test:high-roi && npm run test:public-static-assets && npm run test:token-savings && npm run test:workflow-gate-checkpoint && npm run test:lesson-export-import && npm run test:landing-page-claims && npm run test:dashboard-deeplink-e2e && npm run test:public-package-parity && npm run test:token-savings-dashboard && npm run test:cursor-wiring && npm run test:pretooluse-injection && npm run test:recent-corrective-context && npm run test:durability-step && npm run test:mailer && npm run test:brand-assets",
254
+ "test": "npm run test:schema && npm run test:loop && npm run test:dpo && npm run test:kto && npm run test:api && npm run test:proof && npm run test:e2e && npm run test:rlaif && npm run test:attribution && npm run test:quality && npm run test:intelligence && npm run test:training-export && npm run test:deployment && npm run test:operational-integrity && npm run test:workflow && npm run test:billing && npm run test:cli && npm run test:watcher && npm run test:autoresearch && npm run test:ops && npm run test:session-analyzer && npm run test:tessl && npm run test:gates && npm run test:evoskill && npm run test:gates-hardening && npm run test:workers && npm run test:social-analytics && npm run test:memalign && npm run test:xmemory-lite && npm run test:filesystem-search && npm run test:zernio && npm run test:platform-limits && npm run test:post-video && npm run test:post-everywhere-instagram && npm run test:obsidian-export && npm run test:lesson-db && npm run test:lesson-rotation && npm run test:memory-dedup && npm run test:feedback-quality && npm run test:sync-version && npm run test:check-congruence && npm run test:tool-registry && npm run test:feedback-to-rules && npm run test:memory-firewall && npm run test:belief-update && npm run test:hosted-config && npm run test:operational-summary && npm run test:operator-key-auth && npm run test:cloudflare-sandbox && npm run test:mcp-config && npm run test:plan-gate && npm run test:pulse && npm run test:semantic-layer && npm run test:data-pipeline && npm run test:optimize-context && npm run test:principle-extractor && npm run test:analytics-window && npm run test:funnel-analytics && npm run test:experiment-tracker && npm run test:build-metadata && npm run test:context-engine && npm run test:hf-papers && npm run test:marketing-experiment && npm run test:seo-gsd && npm run test:verify-run && npm run test:export-dpo-pairs && npm run test:export-hf-dataset && npm run test:license && npm run test:bot-detector && npm run test:postinstall && npm run test:funnel-invariants && npm run test:cli-telemetry && npm run test:pro-parity && npm run test:model-tier-router && npm run test:computer-use-firewall && npm run test:skill-exporter && npm run test:statusline && npm run test:evolution && npm run test:org-dashboard && npm run test:multi-hop-recall && npm run test:synthetic-dpo && npm run test:thumbgate-skill && npm run test:learn-hub && npm run test:feedback-fallback && npm run test:metaclaw && npm run test:server-lock && npm run test:control-tower && npm run test:pii-scanner && npm run test:data-governance && npm run test:lesson-inference && npm run test:semantic-dedup && npm run test:fs-utils && npm run test:cli-schema && npm run test:explore && npm run test:lesson-reranker && npm run test:lesson-retrieval && npm run test:cross-encoder && npm run test:reflector-agent && npm run test:feedback-session && npm run test:feedback-history-distiller && npm run test:hallucination-detector && npm run test:history-distiller && npm run test:predictive-insights && npm run test:prove-predictive-insights && npm run test:statusbar-cli && npm run test:generate-instagram-card && npm run test:instagram-thumbgate-post && npm run test:publish-instagram-thumbgate && npm run test:lesson-synthesis && npm run test:background-governance && npm run test:memory-migration && npm run test:prompt-dlp && npm run test:ephemeral-store && npm run test:agent-security && npm run test:skill-progressive && npm run test:per-step-scoring && npm run test:weekly-auto-post && npm run test:social-post-hourly && npm run test:social-quality-gate && npm run test:a2ui-engine && npm run test:gate-satisfy && npm run test:money-watcher && npm run test:budget && npm run test:quick-start && npm run test:utm && npm run test:product-feedback && npm run test:feedback-root-consolidator && npm run test:engagement-audit && npm run test:install-growth-automation && npm run test:publish-thumbgate-launch && npm run test:reconcile-thumbgate-campaign && npm run test:reddit-publisher && npm run test:schedule-thumbgate-campaign && npm run test:social-reply-monitor && npm run test:sync-launch-assets && npm run test:ai-search-visibility && npm run test:perplexity && npm run test:security-scanner && npm run test:llm-client && npm run test:managed-lesson-agent && npm run test:self-distill && npm run test:meta-agent && npm run test:harness-selector && npm run test:thumbgate-bench && npm run test:seo-guides && npm run test:enforcement-loop && npm run test:cli-agent-experience && npm run test:bot-detection && npm run test:checkout-bot-guard && npm run test:session-health && npm run test:session-episodes && npm run test:spec-gate && npm run test:decision-trace && npm run test:dashboard-insights && npm run test:prompt-eval && npm run test:demo-voiceover && npm run test:gate-coherence && npm run test:gate-eval && npm run test:high-roi && npm run test:public-static-assets && npm run test:token-savings && npm run test:workflow-gate-checkpoint && npm run test:lesson-export-import && npm run test:landing-page-claims && npm run test:dashboard-deeplink-e2e && npm run test:public-package-parity && npm run test:token-savings-dashboard && npm run test:cursor-wiring && npm run test:pretooluse-injection && npm run test:recent-corrective-context && npm run test:durability-step && npm run test:mailer && npm run test:brand-assets && npm run test:enforcement-teeth",
255
255
  "test:session-health": "node --test tests/session-health-sensor.test.js",
256
256
  "test:session-episodes": "node --test tests/session-episode-store.test.js",
257
257
  "test:spec-gate": "node --test tests/spec-gate.test.js",
@@ -498,7 +498,8 @@
498
498
  "test:pretooluse-injection": "node --test tests/pretooluse-lesson-injection.test.js",
499
499
  "test:recent-corrective-context": "node --test tests/recent-corrective-actions-context.test.js",
500
500
  "test:mailer": "node --test tests/mailer.test.js tests/billing-webhook-email.test.js",
501
- "test:brand-assets": "node --test tests/brand-assets.test.js"
501
+ "test:brand-assets": "node --test tests/brand-assets.test.js",
502
+ "test:enforcement-teeth": "node --test tests/enforcement-teeth.test.js"
502
503
  },
503
504
  "keywords": [
504
505
  "mcp",
package/public/index.html CHANGED
@@ -974,7 +974,7 @@ __GA_BOOTSTRAP__
974
974
  <!-- HOW IT WORKS -->
975
975
  <section class="how-it-works" id="how-it-works">
976
976
  <div class="container">
977
- <div class="section-label">New in v1.6.0</div>
977
+ <div class="section-label">New in v1.7.0</div>
978
978
  <h2 class="section-title">Three steps to stop repeated AI failures</h2>
979
979
  <div class="steps">
980
980
  <div class="step">
@@ -1330,7 +1330,7 @@ __GA_BOOTSTRAP__
1330
1330
  <a href="https://www.linkedin.com/in/igorganapolsky" target="_blank" rel="noopener">LinkedIn</a>
1331
1331
  <a href="/blog">Blog</a>
1332
1332
  </div>
1333
- <span class="footer-copy">© 2026 Max Smith KDP LLC · MIT License · v1.6.0</span>
1333
+ <span class="footer-copy">© 2026 Max Smith KDP LLC · MIT License · v1.7.0</span>
1334
1334
  </div>
1335
1335
  </footer>
1336
1336
 
@@ -1265,6 +1265,8 @@ function captureFeedback(params) {
1265
1265
  feedbackSession = openSession(feedbackEvent.id, signal, inferredContext);
1266
1266
  } catch (_err) { /* non-critical */ }
1267
1267
 
1268
+ const correctiveActionsReminder = buildCorrectiveActionsReminder(correctiveActions);
1269
+
1268
1270
  // Build result immediately — all remaining side-effects are deferred
1269
1271
  const result = {
1270
1272
  accepted: true,
@@ -1274,6 +1276,10 @@ function captureFeedback(params) {
1274
1276
  memoryRecord,
1275
1277
  _captureMs,
1276
1278
  ...(correctiveActions.length > 0 && { correctiveActions }),
1279
+ ...(correctiveActionsReminder && {
1280
+ systemReminder: correctiveActionsReminder,
1281
+ thumbgateSystemReminder: correctiveActionsReminder,
1282
+ }),
1277
1283
  ...(reflection && { reflection }),
1278
1284
  ...(feedbackSession && { feedbackSession }),
1279
1285
  ...(synthesisResult && { synthesis: synthesisResult }),
@@ -1911,9 +1917,25 @@ function compactMemories() {
1911
1917
  };
1912
1918
  }
1913
1919
 
1920
+ function buildCorrectiveActionsReminder(correctiveActions = []) {
1921
+ if (!Array.isArray(correctiveActions) || correctiveActions.length === 0) return null;
1922
+ const lines = correctiveActions
1923
+ .slice(0, 3)
1924
+ .map((action) => {
1925
+ const type = String(action.type || action.source || 'corrective_action').replace(/_/g, ' ');
1926
+ const text = String(action.text || action.action || action.description || '').trim();
1927
+ if (!text) return null;
1928
+ return ` - ${type}: ${text.slice(0, 240)}`;
1929
+ })
1930
+ .filter(Boolean);
1931
+ if (lines.length === 0) return null;
1932
+ return `[ThumbGate] Corrective actions from prior lessons - apply before the next tool call:\n${lines.join('\n')}`;
1933
+ }
1934
+
1914
1935
  module.exports = {
1915
1936
  captureFeedback,
1916
1937
  compactMemories,
1938
+ buildCorrectiveActionsReminder,
1917
1939
  analyzeFeedback,
1918
1940
  buildPreventionRules,
1919
1941
  writePreventionRules,
@@ -83,6 +83,11 @@ const DEFAULT_PROTECTED_FILE_GLOBS = [
83
83
  ];
84
84
  const EDIT_LIKE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
85
85
  const HIGH_RISK_BASH_PATTERN = /\b(?:git\s+(?:add|commit|push)|gh\s+pr\s+(?:create|merge)|npm\s+publish|yarn\s+publish|pnpm\s+publish|rm\s+-rf)\b/i;
86
+ const BOOSTED_RISK_BLOCK_SCORE = 0.8;
87
+ const BOOSTED_RISK_MIN_EXAMPLES = 3;
88
+ const PR_THREAD_RESOLUTION_ACTION = 'pr_thread_resolution_verified_after_commit';
89
+ const PR_THREAD_RESOLUTION_CLAIM_PATTERN = '(?:thread|review|comment).*?(?:resolved|verified|checked|addressed|fixed)|(?:resolved|verified|checked|addressed|fixed).*?(?:thread|review|comment)';
90
+ const PR_THREAD_RESOLUTION_REQUIRED_ACTIONS = ['pr_threads_checked', 'thread_resolution_verified'];
86
91
 
87
92
  // ---------------------------------------------------------------------------
88
93
  // Config loading
@@ -609,6 +614,218 @@ function isHighRiskAction(toolName, toolInput = {}, affectedFiles = []) {
609
614
  return false;
610
615
  }
611
616
 
617
+ function normalizeRiskToken(value) {
618
+ return String(value || '')
619
+ .toLowerCase()
620
+ .replace(/[^a-z0-9]+/g, ' ')
621
+ .trim();
622
+ }
623
+
624
+ function singularizeRiskToken(token) {
625
+ const value = String(token || '').trim();
626
+ if (value.length > 3 && value.endsWith('ies')) return `${value.slice(0, -3)}y`;
627
+ if (value.length > 3 && value.endsWith('s')) return value.slice(0, -1);
628
+ return value;
629
+ }
630
+
631
+ function riskTokenVariants(token) {
632
+ const normalized = singularizeRiskToken(token);
633
+ const variants = new Set([token, normalized]);
634
+ const synonyms = {
635
+ comment: ['comment', 'comments', 'review', 'reviews', 'reply', 'replies', 'thread', 'threads'],
636
+ thread: ['thread', 'threads', 'review', 'reviews', 'comment', 'comments'],
637
+ bot: ['bot', 'bots', 'automation', 'automated', 'assistant', 'claude', 'codex'],
638
+ pr: ['pr', 'pull', 'pullrequest', 'pullrequests'],
639
+ file: ['file', 'files', 'path', 'paths'],
640
+ test: ['test', 'tests', 'ci', 'coverage', 'verify', 'verification'],
641
+ };
642
+ for (const candidate of [token, normalized]) {
643
+ for (const item of synonyms[candidate] || []) {
644
+ variants.add(item);
645
+ variants.add(singularizeRiskToken(item));
646
+ }
647
+ }
648
+ return [...variants].filter(Boolean);
649
+ }
650
+
651
+ function normalizeRiskTagEntry(entry) {
652
+ if (!entry) return null;
653
+ if (typeof entry === 'string') {
654
+ return { tag: entry };
655
+ }
656
+ if (typeof entry !== 'object') return null;
657
+ const tag = entry.tag || entry.key || entry.name || entry.domain || entry.label || entry.id;
658
+ if (!tag) return null;
659
+ return {
660
+ tag: String(tag),
661
+ count: Number(entry.count ?? entry.examples ?? entry.exampleCount ?? entry.total ?? entry.samples),
662
+ failures: Number(entry.failures ?? entry.failureCount),
663
+ riskRate: Number(entry.riskRate ?? entry.rate ?? entry.failureRate ?? entry.score ?? entry.riskScore),
664
+ };
665
+ }
666
+
667
+ function collectBoostedRiskTags(toolInput = {}) {
668
+ const boostedRisk = toolInput.boostedRisk && typeof toolInput.boostedRisk === 'object'
669
+ ? toolInput.boostedRisk
670
+ : {};
671
+ const sources = [
672
+ toolInput.highRiskTags,
673
+ toolInput.riskTags,
674
+ boostedRisk.highRiskTags,
675
+ boostedRisk.tags,
676
+ boostedRisk.highRiskDomains,
677
+ ];
678
+ const tags = [];
679
+ for (const source of sources) {
680
+ if (Array.isArray(source)) {
681
+ tags.push(...source.map(normalizeRiskTagEntry).filter(Boolean));
682
+ }
683
+ }
684
+ return tags;
685
+ }
686
+
687
+ function isBoostedRiskHigh(toolInput = {}) {
688
+ const boostedRisk = toolInput.boostedRisk && typeof toolInput.boostedRisk === 'object'
689
+ ? toolInput.boostedRisk
690
+ : {};
691
+ const level = String(boostedRisk.riskLevel || boostedRisk.level || boostedRisk.mode || '').toLowerCase();
692
+ if (/\b(?:high|critical|block|deny)\b/.test(level)) return true;
693
+
694
+ const riskScore = Number(boostedRisk.riskScore ?? boostedRisk.score ?? boostedRisk.riskRate ?? boostedRisk.failureRate ?? boostedRisk.baseRate);
695
+ if (Number.isFinite(riskScore) && riskScore >= BOOSTED_RISK_BLOCK_SCORE) return true;
696
+
697
+ const exampleCount = Number(boostedRisk.exampleCount ?? boostedRisk.count ?? boostedRisk.samples ?? boostedRisk.total);
698
+ const failureCount = Number(boostedRisk.failureCount ?? boostedRisk.failures);
699
+ if (
700
+ Number.isFinite(exampleCount) &&
701
+ exampleCount >= BOOSTED_RISK_MIN_EXAMPLES &&
702
+ Number.isFinite(failureCount) &&
703
+ failureCount / Math.max(exampleCount, 1) >= BOOSTED_RISK_BLOCK_SCORE
704
+ ) {
705
+ return true;
706
+ }
707
+
708
+ return collectBoostedRiskTags(toolInput).some((entry) => {
709
+ if (Number.isFinite(entry.riskRate) && entry.riskRate >= BOOSTED_RISK_BLOCK_SCORE) return true;
710
+ if (Number.isFinite(entry.count) && entry.count >= BOOSTED_RISK_MIN_EXAMPLES && !Number.isFinite(entry.riskRate)) return true;
711
+ if (
712
+ Number.isFinite(entry.count) &&
713
+ entry.count >= BOOSTED_RISK_MIN_EXAMPLES &&
714
+ Number.isFinite(entry.failures) &&
715
+ entry.failures / Math.max(entry.count, 1) >= BOOSTED_RISK_BLOCK_SCORE
716
+ ) {
717
+ return true;
718
+ }
719
+ return false;
720
+ });
721
+ }
722
+
723
+ function riskTagMatchesAction(tag, actionContext) {
724
+ const normalizedTag = normalizeRiskToken(tag);
725
+ const normalizedAction = normalizeRiskToken(actionContext);
726
+ if (!normalizedTag || !normalizedAction) return false;
727
+ const actionTokens = new Set(normalizedAction.split(/\s+/).filter(Boolean));
728
+ const tagTokens = normalizedTag.split(/\s+/).filter(Boolean);
729
+ return tagTokens.some((token) => riskTokenVariants(token).some((variant) => actionTokens.has(variant)));
730
+ }
731
+
732
+ function evaluateBoostedRiskTagGuard(toolName, toolInput = {}) {
733
+ const tags = collectBoostedRiskTags(toolInput);
734
+ if (tags.length === 0 || !isBoostedRiskHigh(toolInput)) return null;
735
+
736
+ const actionContext = extractActionContext(toolName, toolInput);
737
+ const matchedTag = tags.find((entry) => riskTagMatchesAction(entry.tag, actionContext));
738
+ if (!matchedTag) return null;
739
+
740
+ const matchText = toolInput.command || toolInput.file_path || toolInput.path || actionContext;
741
+ const message = `Boosted-risk history matched this action (${matchedTag.tag}). This pattern is denied by default until explicit evidence lowers the risk.`;
742
+ return {
743
+ decision: 'deny',
744
+ gate: 'boosted-risk-tag-default-deny',
745
+ message,
746
+ severity: 'critical',
747
+ reasoning: [
748
+ `High-risk tag "${matchedTag.tag}" matched "${String(matchText).slice(0, 120)}"`,
749
+ `Risk threshold: score >= ${BOOSTED_RISK_BLOCK_SCORE} or at least ${BOOSTED_RISK_MIN_EXAMPLES} examples`,
750
+ 'Hook enforcement blocks this pre-tool call instead of relying on advisory recall',
751
+ ],
752
+ };
753
+ }
754
+
755
+ function isGitCommitCommand(toolName, toolInput = {}) {
756
+ return toolName === 'Bash' && /\bgit\s+commit\b/i.test(String(toolInput.command || ''));
757
+ }
758
+
759
+ function isProtectedBranchName(branchName) {
760
+ return /^(?:main|master|develop|dev|trunk|release)$/i.test(String(branchName || '').trim());
761
+ }
762
+
763
+ function detectBranchName(toolInput = {}, repoRoot = null) {
764
+ const inline = toolInput.branchName || toolInput.currentBranch || toolInput.branch || toolInput.headRefName;
765
+ if (inline) return String(inline).trim();
766
+ if (!repoRoot) return '';
767
+ return safeExecFileLines('git', ['rev-parse', '--abbrev-ref', 'HEAD'], repoRoot)[0] || '';
768
+ }
769
+
770
+ function hasPrBranchContext(toolInput = {}, repoRoot = null) {
771
+ if (toolInput.prNumber || toolInput.prUrl || toolInput.pullRequestNumber || toolInput.pullRequestUrl) {
772
+ return true;
773
+ }
774
+ const branchName = detectBranchName(toolInput, repoRoot);
775
+ return Boolean(branchName && !isProtectedBranchName(branchName));
776
+ }
777
+
778
+ function registerPrThreadResolutionClaimGate(toolName, toolInput = {}) {
779
+ if (!isGitCommitCommand(toolName, toolInput)) return null;
780
+ const repoRoot = resolveRepoRoot(toolInput);
781
+ if (!hasPrBranchContext(toolInput, repoRoot)) return null;
782
+
783
+ const branchName = detectBranchName(toolInput, repoRoot);
784
+ const claimGate = registerClaimGate(
785
+ PR_THREAD_RESOLUTION_CLAIM_PATTERN,
786
+ PR_THREAD_RESOLUTION_REQUIRED_ACTIONS,
787
+ 'A PR-branch commit requires verified review-thread resolution before more tool calls or readiness claims.',
788
+ );
789
+ trackAction(PR_THREAD_RESOLUTION_ACTION, {
790
+ branchName: branchName || null,
791
+ repoRoot: repoRoot || null,
792
+ commandHash: crypto.createHash('sha256').update(String(toolInput.command || '')).digest('hex'),
793
+ });
794
+ return claimGate;
795
+ }
796
+
797
+ function isThreadResolutionSatisfied() {
798
+ return PR_THREAD_RESOLUTION_REQUIRED_ACTIONS.some((actionId) => (
799
+ hasAction(actionId) || isConditionSatisfied(actionId)
800
+ ));
801
+ }
802
+
803
+ function isThreadResolutionEvidenceAction(toolName, toolInput = {}) {
804
+ if (isGitCommitCommand(toolName, toolInput)) return true;
805
+ if (['recall', 'search_lessons', 'verify_claim', 'satisfy_gate', 'track_action'].includes(toolName)) return true;
806
+ if (toolName !== 'Bash') return false;
807
+ const command = String(toolInput.command || '');
808
+ return /\b(?:gate-satisfy|satisfy_gate|track_action|gh\s+pr\s+(?:view|checks|status)|gh\s+api\b.*(?:reviewThreads|reviews|comments|threads)|git\s+(?:status|diff|show))\b/i.test(command);
809
+ }
810
+
811
+ function evaluatePendingPrThreadResolutionGate(toolName, toolInput = {}) {
812
+ if (!hasAction(PR_THREAD_RESOLUTION_ACTION)) return null;
813
+ if (isThreadResolutionSatisfied()) return null;
814
+ if (isThreadResolutionEvidenceAction(toolName, toolInput)) return null;
815
+
816
+ const message = 'A git commit was made on a PR branch. Verify review threads are resolved before the next tool call.';
817
+ return {
818
+ decision: 'deny',
819
+ gate: 'pr-thread-resolution-verified-required',
820
+ message,
821
+ severity: 'critical',
822
+ reasoning: [
823
+ `Tracked action ${PR_THREAD_RESOLUTION_ACTION} is pending`,
824
+ 'Satisfy pr_threads_checked or thread_resolution_verified with evidence before continuing',
825
+ ],
826
+ };
827
+ }
828
+
612
829
  function isScopeEnforcedAction(toolName, toolInput = {}, affectedFiles = []) {
613
830
  if (EDIT_LIKE_TOOLS.has(toolName) && affectedFiles.length > 0) return true;
614
831
  if (toolName !== 'Bash') return false;
@@ -1116,6 +1333,38 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
1116
1333
  }
1117
1334
 
1118
1335
  const constraints = loadConstraints();
1336
+ registerPrThreadResolutionClaimGate(toolName, toolInput);
1337
+ const pendingThreadResolutionGate = evaluatePendingPrThreadResolutionGate(toolName, toolInput);
1338
+ if (pendingThreadResolutionGate) {
1339
+ recordStat(pendingThreadResolutionGate.gate, 'block');
1340
+ const auditRecord = recordAuditEvent({
1341
+ toolName,
1342
+ toolInput,
1343
+ decision: 'deny',
1344
+ gateId: pendingThreadResolutionGate.gate,
1345
+ message: pendingThreadResolutionGate.message,
1346
+ severity: pendingThreadResolutionGate.severity,
1347
+ source: 'gates-engine',
1348
+ });
1349
+ auditToFeedback(auditRecord);
1350
+ return pendingThreadResolutionGate;
1351
+ }
1352
+
1353
+ const boostedRiskGuard = evaluateBoostedRiskTagGuard(toolName, toolInput);
1354
+ if (boostedRiskGuard) {
1355
+ recordStat(boostedRiskGuard.gate, 'block');
1356
+ const auditRecord = recordAuditEvent({
1357
+ toolName,
1358
+ toolInput,
1359
+ decision: 'deny',
1360
+ gateId: boostedRiskGuard.gate,
1361
+ message: boostedRiskGuard.message,
1362
+ severity: boostedRiskGuard.severity,
1363
+ source: 'gates-engine',
1364
+ });
1365
+ auditToFeedback(auditRecord);
1366
+ return boostedRiskGuard;
1367
+ }
1119
1368
 
1120
1369
  // Fast-path: feedback/recall tools skip metric gates entirely (avoids Stripe API calls)
1121
1370
  const METRIC_SKIP_TOOLS = ['capture_feedback', 'feedback_stats', 'recall', 'feedback_summary', 'prevention_rules'];
@@ -1254,6 +1503,38 @@ function evaluateGates(toolName, toolInput, configPath) {
1254
1503
  }
1255
1504
 
1256
1505
  const constraints = loadConstraints();
1506
+ registerPrThreadResolutionClaimGate(toolName, toolInput);
1507
+ const pendingThreadResolutionGate = evaluatePendingPrThreadResolutionGate(toolName, toolInput);
1508
+ if (pendingThreadResolutionGate) {
1509
+ recordStat(pendingThreadResolutionGate.gate, 'block');
1510
+ const auditRecord = recordAuditEvent({
1511
+ toolName,
1512
+ toolInput,
1513
+ decision: 'deny',
1514
+ gateId: pendingThreadResolutionGate.gate,
1515
+ message: pendingThreadResolutionGate.message,
1516
+ severity: pendingThreadResolutionGate.severity,
1517
+ source: 'gates-engine',
1518
+ });
1519
+ auditToFeedback(auditRecord);
1520
+ return pendingThreadResolutionGate;
1521
+ }
1522
+
1523
+ const boostedRiskGuard = evaluateBoostedRiskTagGuard(toolName, toolInput);
1524
+ if (boostedRiskGuard) {
1525
+ recordStat(boostedRiskGuard.gate, 'block');
1526
+ const auditRecord = recordAuditEvent({
1527
+ toolName,
1528
+ toolInput,
1529
+ decision: 'deny',
1530
+ gateId: boostedRiskGuard.gate,
1531
+ message: boostedRiskGuard.message,
1532
+ severity: boostedRiskGuard.severity,
1533
+ source: 'gates-engine',
1534
+ });
1535
+ auditToFeedback(auditRecord);
1536
+ return boostedRiskGuard;
1537
+ }
1257
1538
 
1258
1539
  for (const gate of config.gates) {
1259
1540
  const matchDetails = matchGate(gate, toolName, toolInput);
@@ -1456,14 +1737,20 @@ function evaluateSecretGuard(input = {}) {
1456
1737
  // PreToolUse hook interface (stdin/stdout JSON)
1457
1738
  // ---------------------------------------------------------------------------
1458
1739
 
1740
+ function buildReminderOutput(context) {
1741
+ return {
1742
+ additionalContext: context,
1743
+ systemReminder: context,
1744
+ thumbgateSystemReminder: context,
1745
+ };
1746
+ }
1747
+
1459
1748
  function formatOutput(result, behavioralContext) {
1460
1749
  if (!result) {
1461
1750
  // No gate matched — inject behavioral context if available
1462
1751
  if (behavioralContext) {
1463
1752
  return JSON.stringify({
1464
- hookSpecificOutput: {
1465
- additionalContext: behavioralContext,
1466
- },
1753
+ hookSpecificOutput: buildReminderOutput(behavioralContext),
1467
1754
  });
1468
1755
  }
1469
1756
  return JSON.stringify({});
@@ -1474,19 +1761,27 @@ function formatOutput(result, behavioralContext) {
1474
1761
  : '';
1475
1762
 
1476
1763
  if (result.decision === 'deny') {
1764
+ const reminder = behavioralContext ? buildReminderOutput(behavioralContext) : {};
1765
+ const reminderSuffix = behavioralContext ? `\n\nSystem reminder:\n${behavioralContext}` : '';
1477
1766
  return JSON.stringify({
1478
1767
  hookSpecificOutput: {
1768
+ ...reminder,
1479
1769
  permissionDecision: 'deny',
1480
- permissionDecisionReason: `[GATE:${result.gate}] ${result.message}${reasoningSuffix}`,
1770
+ permissionDecisionReason: `[GATE:${result.gate}] ${result.message}${reasoningSuffix}${reminderSuffix}`,
1481
1771
  },
1482
1772
  });
1483
1773
  }
1484
1774
 
1485
1775
  if (result.decision === 'warn') {
1486
1776
  const extra = behavioralContext ? `\n${behavioralContext}` : '';
1777
+ const context = `[GATE:${result.gate}] WARNING: ${result.message}${reasoningSuffix}${extra}`;
1487
1778
  return JSON.stringify({
1488
1779
  hookSpecificOutput: {
1489
- additionalContext: `[GATE:${result.gate}] WARNING: ${result.message}${reasoningSuffix}${extra}`,
1780
+ additionalContext: context,
1781
+ ...(behavioralContext ? {
1782
+ systemReminder: behavioralContext,
1783
+ thumbgateSystemReminder: behavioralContext,
1784
+ } : {}),
1490
1785
  },
1491
1786
  });
1492
1787
  }
@@ -1947,7 +2242,15 @@ module.exports = {
1947
2242
  extractActionContext,
1948
2243
  extractAvoidanceAdvice,
1949
2244
  mergeContextStrings,
2245
+ buildReminderOutput,
1950
2246
  isHighRiskAction,
2247
+ collectBoostedRiskTags,
2248
+ isBoostedRiskHigh,
2249
+ riskTagMatchesAction,
2250
+ evaluateBoostedRiskTagGuard,
2251
+ registerPrThreadResolutionClaimGate,
2252
+ evaluatePendingPrThreadResolutionGate,
2253
+ PR_THREAD_RESOLUTION_ACTION,
1951
2254
  };
1952
2255
 
1953
2256
  // ---------------------------------------------------------------------------