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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/adapters/README.md +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +41 -2
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +39 -5
- package/package.json +4 -3
- package/public/index.html +2 -2
- package/scripts/feedback-loop.js +22 -0
- package/scripts/gates-engine.js +308 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thumbgate-marketplace",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
package/adapters/README.md
CHANGED
|
@@ -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
|
+
- `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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
};
|
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
|
-
//
|
|
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
|
-
|
|
1602
|
-
blockPatterns.forEach((p, i) =>
|
|
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 (_) { /*
|
|
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.
|
|
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.
|
|
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.
|
|
1333
|
+
<span class="footer-copy">© 2026 Max Smith KDP LLC · MIT License · v1.7.0</span>
|
|
1334
1334
|
</div>
|
|
1335
1335
|
</footer>
|
|
1336
1336
|
|
package/scripts/feedback-loop.js
CHANGED
|
@@ -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,
|
package/scripts/gates-engine.js
CHANGED
|
@@ -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:
|
|
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
|
// ---------------------------------------------------------------------------
|