thumbgate 1.6.0 → 1.8.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 +90 -2
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +39 -5
- package/config/mcp-allowlists.json +10 -1
- package/package.json +13 -7
- package/public/index.html +2 -2
- package/scripts/autonomous-workflow.js +377 -0
- package/scripts/billing.js +4 -2
- package/scripts/feedback-loop.js +22 -0
- package/scripts/gates-engine.js +308 -5
- package/scripts/mailer/resend-mailer.js +210 -40
- package/scripts/statusline-context.js +207 -0
- package/scripts/statusline.sh +31 -14
- package/scripts/tool-registry.js +39 -0
- package/CHANGELOG.md +0 -702
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
|
// ---------------------------------------------------------------------------
|