thumbgate 1.27.2 → 1.27.3

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.
@@ -8,6 +8,7 @@ const DEFAULT_PACKAGE_PATH = path.join(__dirname, '..', 'package.json');
8
8
  const DEFAULT_OUTPUT_DIR = path.join(__dirname, '..', 'docs', 'marketing');
9
9
  const KNOWN_REPOS = Object.freeze({
10
10
  '@anthropic-ai/sdk': 'anthropics/anthropic-sdk-typescript',
11
+ '@modelcontextprotocol/sdk': 'modelcontextprotocol/typescript-sdk',
11
12
  '@google/genai': 'googleapis/js-genai',
12
13
  '@huggingface/transformers': 'huggingface/transformers.js',
13
14
  '@lancedb/lancedb': 'lancedb/lancedb',
@@ -23,6 +24,24 @@ const KNOWN_REPOS = Object.freeze({
23
24
  undici: 'nodejs/undici',
24
25
  });
25
26
 
27
+ // Communities where ThumbGate's buyers live even though they are not npm
28
+ // dependencies. ThumbGate ships an MCP server, so the Model Context Protocol
29
+ // repos are the single highest-ROI ecosystem to contribute to — but the
30
+ // dependency-scan above would never surface them. These are always scouted on
31
+ // the default (no explicit --dependencies) path, de-duped against package.json.
32
+ const STRATEGIC_DEPENDENCIES = Object.freeze([
33
+ '@modelcontextprotocol/sdk', // MCP TypeScript SDK — the protocol ThumbGate implements
34
+ 'modelcontextprotocol/servers', // MCP servers ecosystem — where MCP authors (our buyers) collaborate
35
+ ]);
36
+
37
+ // Honest, repo-accurate framing for the outreach draft. ThumbGate does not
38
+ // `import` these — it implements the protocol — so the generic "while using X"
39
+ // line would be a false claim. Keep drafts truthful (CEO honesty rule).
40
+ const RELATIONSHIP_OVERRIDES = Object.freeze({
41
+ '@modelcontextprotocol/sdk': 'building ThumbGate as an MCP server against the Model Context Protocol spec',
42
+ 'modelcontextprotocol/servers': 'building and testing ThumbGate as an MCP server alongside the reference MCP servers',
43
+ });
44
+
26
45
  const BOUNTY_KEYWORDS = [
27
46
  'bug bounty',
28
47
  'bounty',
@@ -70,7 +89,10 @@ function dependencyNames(pkg = {}) {
70
89
  }
71
90
 
72
91
  function repoFromDependency(name) {
73
- return KNOWN_REPOS[name] || '';
92
+ if (KNOWN_REPOS[name]) return KNOWN_REPOS[name];
93
+ // A strategic identifier may itself be an "owner/repo" slug (not an npm name).
94
+ if (!name.startsWith('@') && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(name)) return name;
95
+ return '';
74
96
  }
75
97
 
76
98
  function buildIssueSearchQueries(repo) {
@@ -89,14 +111,18 @@ function scoreOpportunity(depName, repo, options = {}) {
89
111
  score += 20;
90
112
  reasons.push('known upstream repository');
91
113
  }
92
- if (/sdk|genai|stripe|playwright|lancedb|transformers|sqlite|undici/i.test(depName)) {
114
+ if (/sdk|genai|stripe|playwright|lancedb|transformers|sqlite|undici|mcp|modelcontext/i.test(depName)) {
93
115
  score += 20;
94
116
  reasons.push('high product adjacency for agent tooling');
95
117
  }
96
- if (/anthropic|google|huggingface|stripe|microsoft|nodejs/i.test(repo)) {
118
+ if (/anthropic|google|huggingface|stripe|microsoft|nodejs|modelcontextprotocol/i.test(repo)) {
97
119
  score += 15;
98
120
  reasons.push('large ecosystem visibility');
99
121
  }
122
+ if (/modelcontext|mcp/i.test(depName) || /modelcontextprotocol/i.test(repo)) {
123
+ score += 12;
124
+ reasons.push("ThumbGate's own protocol surface — buyers are MCP authors");
125
+ }
100
126
  if (options.includeBounties) {
101
127
  score += 10;
102
128
  reasons.push('bounty search enabled');
@@ -138,7 +164,7 @@ function buildOpportunity(depName, options = {}) {
138
164
  'no bounty, security, or maintainer-policy claim without source link',
139
165
  ],
140
166
  outreachDraft: repo
141
- ? `I found this while using ${depName} in ThumbGate. I reproduced the issue, added a minimal fix with tests, and kept the PR scoped to the maintainer's issue.`
167
+ ? `I found this while ${RELATIONSHIP_OVERRIDES[depName] || `using ${depName} in ThumbGate`}. I reproduced the issue, added a minimal fix with tests, and kept the PR scoped to the maintainer's issue.`
142
168
  : '',
143
169
  };
144
170
  }
@@ -149,7 +175,9 @@ function buildOssPrOpportunityScoutPlan(rawOptions = {}) {
149
175
  const explicitDeps = splitList(rawOptions.dependencies || rawOptions.deps);
150
176
  const includeBounties = rawOptions.includeBounties !== false && rawOptions['include-bounties'] !== false;
151
177
  const maxRepos = Math.max(1, Number.parseInt(String(rawOptions.maxRepos || rawOptions['max-repos'] || 12), 10) || 12);
152
- const deps = explicitDeps.length ? explicitDeps : dependencyNames(pkg);
178
+ const deps = explicitDeps.length
179
+ ? explicitDeps
180
+ : [...new Set([...dependencyNames(pkg), ...STRATEGIC_DEPENDENCIES])];
153
181
  const opportunities = deps
154
182
  .map((dep) => buildOpportunity(dep, { includeBounties }))
155
183
  .filter((opportunity) => opportunity.repo)
@@ -223,6 +251,8 @@ function writeOssPrOpportunityScoutPack(outputDir = DEFAULT_OUTPUT_DIR, options
223
251
 
224
252
  module.exports = {
225
253
  KNOWN_REPOS,
254
+ STRATEGIC_DEPENDENCIES,
255
+ RELATIONSHIP_OVERRIDES,
226
256
  buildIssueSearchQueries,
227
257
  buildOpportunity,
228
258
  buildOssPrOpportunityScoutPlan,
@@ -6,7 +6,7 @@ const path = require('path');
6
6
  const {
7
7
  PRO_MONTHLY_PAYMENT_LINK,
8
8
  PRO_PRICE_LABEL,
9
- TEAM_PRICE_LABEL,
9
+ ENTERPRISE_PRICE_LABEL,
10
10
  } = require('./commercial-offer');
11
11
 
12
12
  const USAGE_FILE = path.join(process.env.HOME || '/tmp', '.thumbgate', 'usage-limits.json');
@@ -31,7 +31,7 @@ const FREE_TIER_LIMITS = {
31
31
  const FREE_TIER_MAX_GATES = 3; // 3 active prevention rules on free; Pro is unlimited
32
32
  const FREE_TIER_DAILY_BLOCKS = 3; // 3 gate blocks/day on free; after limit, deny → warn + upgrade CTA
33
33
 
34
- const UPGRADE_MESSAGE = `Pro: ${PRO_PRICE_LABEL} — unlimited rules, recall, lesson search, dashboard, and exports: ${PRO_MONTHLY_PAYMENT_LINK}\n Team: ${TEAM_PRICE_LABEL} after workflow qualification.`;
34
+ const UPGRADE_MESSAGE = `Pro: ${PRO_PRICE_LABEL} — unlimited rules, recall, lesson search, dashboard, and exports: ${PRO_MONTHLY_PAYMENT_LINK}\n Enterprise: ${ENTERPRISE_PRICE_LABEL} after workflow qualification.`;
35
35
 
36
36
  const PAYWALL_MESSAGES = {
37
37
  capture_feedback: 'Free tier: 5 captures/day (25 total). Your feedback is stored locally — upgrade to capture unlimited.',
@@ -446,6 +446,65 @@ function buildZeroTrustGuide() {
446
446
  });
447
447
  }
448
448
 
449
+ const GOVERN_CLAUDE_FOR_LEGAL_GUIDE_SPEC = Object.freeze({
450
+ slug: 'govern-claude-for-legal-agents',
451
+ meta: {
452
+ query: 'govern claude for legal agents',
453
+ title: 'Govern Claude for Legal Agents | A Gate Before They Act',
454
+ heroTitle: 'Govern Claude for Legal’s 90+ Agents at the Tool Call',
455
+ heroSummary: 'Claude for Legal ships 90+ named agents that review contracts, answer DSARs, and run continuously on document and email streams. Anthropic’s own guidance is that there must be a gate before anything is filed, sent, or relied on. ThumbGate is that gate — it checks each agent action at the tool-call boundary, in your tenant, and logs every decision for the record.',
456
+ },
457
+ takeaways: [
458
+ 'Claude for Legal’s agents take real side effects — sending a DSAR response, filing a document, writing to a system of record. ThumbGate gates the action before the side effect runs, not after, on a dashboard.',
459
+ 'Intent-agnostic: whether an agent is wrong, prompt-injected, or off-playbook, ThumbGate blocks the same way and records the rule that fired. The risk is not a “rogue” agent — it is an ordinary one acting at volume.',
460
+ 'Every gated decision is logged with its source rule — a SIEM-exportable audit trail your ethics, risk, and conflicts owners can query.',
461
+ ],
462
+ sections: [
463
+ ['paragraphs', 'Why 90+ legal agents need a gate before the side effect', [
464
+ 'A firm running Claude for Legal now has dozens of agents acting on ongoing document and email streams — vendor-agreement review, termination review, DSAR responses, claim charts. No one can review every action by hand. The risk is not malice; it is an ordinary agent that sends the wrong response, files against the wrong playbook, or surfaces a privileged document.',
465
+ 'Anthropic’s own framing names the control: an explicit gate before anything is filed, sent, or relied on. ThumbGate implements that gate at the tool-call boundary — the moment before the action executes — instead of trusting the agent’s stated intent.',
466
+ ]],
467
+ ['bullets', 'What ThumbGate gates for legal agents', [
468
+ 'The send/file/write action itself — e.g. a DSAR or client response before it leaves, a filing before it goes out, a write to a conflicted matter — held or blocked at the boundary.',
469
+ 'Playbook deviations — an action that departs from the firm’s approved workflow is stopped for review rather than executed.',
470
+ 'Privileged-document exposure — flagged before an agent surfaces or forwards it.',
471
+ 'Continuous runs — one rule set covers every agent and every scheduled run, so coverage scales with agent count, not headcount.',
472
+ ]],
473
+ ['paragraphs', 'Enforcement in your tenant, with an audit trail', [
474
+ 'ThumbGate runs as a pre-action gate in front of agent fulfillment, including a Dialogflow CX webhook gate deployed in your own GCP tenant, so matter content does not leave your boundary. Risk and planning scoring can run on Gemini via Vertex, in-tenant. This is a white-glove design-partner pilot, not a turnkey product purchase.',
475
+ 'Every gated detection is logged with the rule that fired and the feedback event that generated it. That decision trail is the evidence a firm needs for malpractice defense and bar-compliance review — queryable, exportable, and tied to a named owner.',
476
+ ]],
477
+ ['paragraphs', 'ThumbGate complements Claude for Legal — it does not replace it', [
478
+ 'Claude for Legal decides what the work is. ThumbGate decides what is allowed to execute. Use both: keep the 90+ agents doing the legal work, and put a gate between each agent and its next side effect. A thumbs-down on a bad action becomes a prevention rule, so the same mistake is blocked across every agent and matter next time.',
479
+ ]],
480
+ ],
481
+ faq: [
482
+ [
483
+ 'Does ThumbGate replace Claude for Legal?',
484
+ 'No. Claude for Legal’s agents do the legal work; ThumbGate governs what they are allowed to execute — a gate before anything is filed, sent, or relied on. You run both.',
485
+ ],
486
+ [
487
+ 'Where does the gate run?',
488
+ 'In your tenant. ThumbGate gates agent fulfillment locally or via a Dialogflow CX webhook gate in your own GCP project; matter content does not leave your boundary, and Vertex/Gemini scoring runs in-tenant. It is a white-glove design-partner pilot, not a turnkey purchase.',
489
+ ],
490
+ [
491
+ 'What proof does a firm get?',
492
+ 'Every gated decision is logged with the rule that fired and the feedback that generated it — a SIEM-exportable audit trail for ethics, risk, and conflicts owners.',
493
+ ],
494
+ ],
495
+ relatedPaths: ['/guides/ai-coding-agent-zero-trust', '/guides/pre-action-checks'],
496
+ });
497
+
498
+ function buildGovernClaudeForLegalGuide() {
499
+ return preActionGuide(GOVERN_CLAUDE_FOR_LEGAL_GUIDE_SPEC.slug, {
500
+ ...GOVERN_CLAUDE_FOR_LEGAL_GUIDE_SPEC.meta,
501
+ takeaways: GOVERN_CLAUDE_FOR_LEGAL_GUIDE_SPEC.takeaways,
502
+ sections: GOVERN_CLAUDE_FOR_LEGAL_GUIDE_SPEC.sections.map(([kind, heading, entries]) => buildSectionFromSpec(kind, heading, entries)),
503
+ faq: GOVERN_CLAUDE_FOR_LEGAL_GUIDE_SPEC.faq.map(([question, text]) => answer(question, text)),
504
+ relatedPaths: GOVERN_CLAUDE_FOR_LEGAL_GUIDE_SPEC.relatedPaths,
505
+ });
506
+ }
507
+
449
508
  const PROXY_POINTER_RAG_GUARDRAILS_SPEC = Object.freeze({
450
509
  slug: 'proxy-pointer-rag-guardrails',
451
510
  meta: {
@@ -1589,6 +1648,7 @@ const PAGE_BLUEPRINTS = [
1589
1648
  },
1590
1649
  buildSemanticPseoGuide(),
1591
1650
  buildZeroTrustGuide(),
1651
+ buildGovernClaudeForLegalGuide(),
1592
1652
  buildProxyPointerRagGuide(),
1593
1653
  buildRagPrecisionTuningGuide(),
1594
1654
  buildAiEngineeringStackGuide(),
@@ -64,12 +64,12 @@ function loadJson(filePath) {
64
64
  function loadGovernanceState() {
65
65
  const raw = loadJson(GOVERNANCE_STATE_PATH);
66
66
  return {
67
- taskScope: raw && raw.taskScope && typeof raw.taskScope === 'object' ? raw.taskScope : null,
68
- protectedApprovals: Array.isArray(raw && raw.protectedApprovals) ? raw.protectedApprovals : [],
69
- branchGovernance: raw && raw.branchGovernance && typeof raw.branchGovernance === 'object'
67
+ taskScope: raw?.taskScope && typeof raw.taskScope === 'object' ? raw.taskScope : null,
68
+ protectedApprovals: Array.isArray(raw?.protectedApprovals) ? raw.protectedApprovals : [],
69
+ branchGovernance: raw?.branchGovernance && typeof raw.branchGovernance === 'object'
70
70
  ? raw.branchGovernance
71
71
  : null,
72
- workflowContract: raw && raw.workflowContract && typeof raw.workflowContract === 'object'
72
+ workflowContract: raw?.workflowContract && typeof raw.workflowContract === 'object'
73
73
  ? raw.workflowContract
74
74
  : null,
75
75
  };
@@ -391,9 +391,18 @@ function commandMatchesPattern(command, pattern) {
391
391
  }
392
392
  }
393
393
 
394
+ const COMPLETION_ACTION_PATTERNS = [
395
+ /\bgit\s+(?:commit|push)\b/i,
396
+ /\bgh\s+pr\s+(?:create|merge)\b/i,
397
+ /\bgh\s+release\s+create\b/i,
398
+ /\bnpm\s+publish\b/i,
399
+ /\byarn\s+publish\b/i,
400
+ /\bpnpm\s+publish\b/i,
401
+ ];
402
+
394
403
  function isCompletionLikeAction(command) {
395
- return /\b(?:git\s+(?:commit|push)|gh\s+pr\s+(?:create|merge)|gh\s+release\s+create|npm\s+publish|yarn\s+publish|pnpm\s+publish)\b/i
396
- .test(String(command || ''));
404
+ const text = String(command || '');
405
+ return COMPLETION_ACTION_PATTERNS.some((pattern) => pattern.test(text));
397
406
  }
398
407
 
399
408
  function collectEvidenceLabels(toolInput = {}, options = {}) {
@@ -466,11 +475,17 @@ function evaluateWorkflowContract(contractInput, context = {}) {
466
475
  }
467
476
 
468
477
  const hasBlock = violations.some((violation) => violation.severity === 'block');
478
+ let mode = 'allow';
479
+ if (hasBlock) {
480
+ mode = 'block';
481
+ } else if (violations.length > 0) {
482
+ mode = 'warn';
483
+ }
469
484
  return {
470
485
  active: true,
471
486
  contract,
472
487
  violations,
473
- mode: hasBlock ? 'block' : violations.length > 0 ? 'warn' : 'allow',
488
+ mode,
474
489
  };
475
490
  }
476
491
 
@@ -704,18 +719,18 @@ function scoreRisk({
704
719
  );
705
720
  }
706
721
  }
707
- if (workflowContract && workflowContract.active && workflowContract.violations.length > 0) {
722
+ if (workflowContract?.active && workflowContract.violations.length > 0) {
708
723
  for (const violation of workflowContract.violations) {
709
724
  addDriver(
710
725
  drivers,
711
726
  `workflow_contract_${violation.code}`,
712
727
  violation.severity === 'block' ? 0.38 : 0.18,
713
728
  violation.message,
714
- { workflowId: workflowContract.contract && workflowContract.contract.workflowId }
729
+ { workflowId: workflowContract.contract?.workflowId }
715
730
  );
716
731
  }
717
732
  }
718
- if (memoryGuard && memoryGuard.mode && memoryGuard.mode !== 'allow') {
733
+ if (memoryGuard?.mode && memoryGuard.mode !== 'allow') {
719
734
  addDriver(
720
735
  drivers,
721
736
  'memory_recurrence',
@@ -724,7 +739,7 @@ function scoreRisk({
724
739
  { mode: memoryGuard.mode }
725
740
  );
726
741
  }
727
- if (learnedPolicy && learnedPolicy.enabled && learnedPolicy.prediction) {
742
+ if (learnedPolicy?.enabled && learnedPolicy.prediction) {
728
743
  const confidence = learnedPolicy.prediction.confidence || 0;
729
744
  const label = learnedPolicy.prediction.label;
730
745
  if (label === 'deny' && confidence >= 0.6) {
@@ -809,19 +824,17 @@ function buildEvidence({
809
824
  evidence.push(`Workflow control ${workflowControl.mode}: ${workflowControl.reasons.join(' ')}`);
810
825
  }
811
826
  }
812
- if (workflowContract && workflowContract.active) {
813
- const workflowId = workflowContract.contract && workflowContract.contract.workflowId
814
- ? workflowContract.contract.workflowId
815
- : 'unnamed';
827
+ if (workflowContract?.active) {
828
+ const workflowId = workflowContract.contract?.workflowId || 'unnamed';
816
829
  evidence.push(`Workflow contract active: ${workflowId}.`);
817
830
  for (const violation of workflowContract.violations.slice(0, 3)) {
818
831
  evidence.push(`Workflow contract ${violation.severity}: ${violation.message}`);
819
832
  }
820
833
  }
821
- if (actionProfile && actionProfile.backgroundAgent) {
834
+ if (actionProfile?.backgroundAgent) {
822
835
  evidence.push('Background or scheduled agent context detected for this action.');
823
836
  }
824
- if (actionProfile && actionProfile.economicAction) {
837
+ if (actionProfile?.economicAction) {
825
838
  evidence.push('Economic action keywords detected (billing, refunds, payouts, invoices, or subscriptions).');
826
839
  }
827
840
  if (actionProfile && actionProfile.customerSystemAction) {
@@ -1031,7 +1044,7 @@ function buildRemediations({
1031
1044
  'Isolated execution limits host damage when a high-risk local action goes wrong.'
1032
1045
  );
1033
1046
  }
1034
- if (costControl && costControl.mode && costControl.mode !== 'allow') {
1047
+ if (costControl?.mode && costControl.mode !== 'allow') {
1035
1048
  push(
1036
1049
  'reduce_model_budget',
1037
1050
  'Reduce model budget before execution',
@@ -1039,7 +1052,7 @@ function buildRemediations({
1039
1052
  'High token or cost estimates should be reviewed before the model/tool loop continues.'
1040
1053
  );
1041
1054
  }
1042
- if (workflowContract && workflowContract.active && workflowContract.violations.length > 0) {
1055
+ if (workflowContract?.active && workflowContract.violations.length > 0) {
1043
1056
  const codes = new Set(workflowContract.violations.map((violation) => violation.code));
1044
1057
  if (codes.has('missing_required_evidence')) {
1045
1058
  push(
@@ -1241,13 +1254,13 @@ function buildDecisionControl({
1241
1254
  protectedSurface,
1242
1255
  actionProfile,
1243
1256
  });
1244
- const hasOperationalBlockers = Boolean(integrity && Array.isArray(integrity.blockers) && integrity.blockers.length > 0);
1245
- const hasCostWarning = Boolean(costControl && costControl.mode === 'warn');
1246
- const hasCostBlock = Boolean(costControl && costControl.mode === 'block');
1247
- const hasWorkflowWarning = Boolean(workflowControl && workflowControl.mode === 'warn');
1248
- const hasWorkflowBlock = Boolean(workflowControl && workflowControl.mode === 'block');
1249
- const hasContractWarning = Boolean(workflowContract && workflowContract.mode === 'warn');
1250
- const hasContractBlock = Boolean(workflowContract && workflowContract.mode === 'block');
1257
+ const hasOperationalBlockers = Boolean(integrity?.blockers?.length);
1258
+ const hasCostWarning = costControl?.mode === 'warn';
1259
+ const hasCostBlock = costControl?.mode === 'block';
1260
+ const hasWorkflowWarning = workflowControl?.mode === 'warn';
1261
+ const hasWorkflowBlock = workflowControl?.mode === 'block';
1262
+ const hasContractWarning = workflowContract?.mode === 'warn';
1263
+ const hasContractBlock = workflowContract?.mode === 'block';
1251
1264
  const requiresCheckpoint = decision === 'warn'
1252
1265
  || (decision === 'allow' && (reversibility !== 'two_way_door' || hasOperationalBlockers || hasCostWarning || hasWorkflowWarning || hasContractWarning));
1253
1266
  const executionMode = decision === 'deny'
@@ -1291,49 +1304,51 @@ function buildDecisionControl({
1291
1304
  };
1292
1305
  }
1293
1306
 
1294
- function chooseDecision({ riskScore, integrity, memoryGuard, learnedPolicy, blastRadius, command, costControl, workflowControl, workflowContract, actionProfile }) {
1295
- const hasOperationalBlockers = Boolean(integrity && Array.isArray(integrity.blockers) && integrity.blockers.length > 0);
1296
- if (costControl && costControl.mode === 'block') {
1297
- return 'deny';
1298
- }
1299
- if (workflowControl && workflowControl.mode === 'block') {
1300
- return 'deny';
1301
- }
1302
- if (workflowContract && workflowContract.mode === 'block') {
1303
- return 'deny';
1304
- }
1305
- const destructiveBypass = /\bgit\s+push\b.*(?:--force|-f)\b/i.test(command) || /\bgh\s+pr\s+merge\b.*--admin\b/i.test(command);
1306
- const learnedPrediction = learnedPolicy && learnedPolicy.enabled ? learnedPolicy.prediction : null;
1307
- const learnedHardStop = Boolean(
1308
- learnedPrediction
1309
- && learnedPrediction.label === 'deny'
1310
- && learnedPrediction.confidence >= 0.7
1311
- );
1312
- const learnedWarning = Boolean(
1313
- learnedPrediction
1314
- && ['warn', 'verify', 'deny'].includes(learnedPrediction.label)
1315
- && learnedPrediction.confidence >= 0.3
1316
- );
1317
- const learnedRecall = Boolean(
1318
- learnedPrediction
1319
- && learnedPrediction.label === 'recall'
1320
- && learnedPrediction.confidence >= 0.3
1307
+ function isDestructiveBypass(command) {
1308
+ return /\bgit\s+push\b.*(?:--force|-f)\b/i.test(command)
1309
+ || /\bgh\s+pr\s+merge\b.*--admin\b/i.test(command);
1310
+ }
1311
+
1312
+ function getLearnedPrediction(learnedPolicy) {
1313
+ return learnedPolicy?.enabled ? learnedPolicy.prediction : null;
1314
+ }
1315
+
1316
+ function learnedPredictionMatches(prediction, labels, minConfidence) {
1317
+ return Boolean(
1318
+ prediction
1319
+ && labels.includes(prediction.label)
1320
+ && prediction.confidence >= minConfidence
1321
1321
  );
1322
- const lowBlastRadius = blastRadius.fileCount <= 1
1322
+ }
1323
+
1324
+ function isLowBlastRadius(blastRadius) {
1325
+ return blastRadius.fileCount <= 1
1323
1326
  && blastRadius.surfaceCount <= 1
1324
1327
  && blastRadius.releaseSensitiveFiles.length === 0
1325
1328
  && blastRadius.unapprovedProtectedFiles === 0;
1326
- const lowRiskHandoff = /\bgit\s+push\b|\bgh\s+pr\s+(?:create|merge)\b/i.test(command)
1329
+ }
1330
+
1331
+ function isLowRiskHandoff({
1332
+ command,
1333
+ destructiveBypass,
1334
+ learnedHardStop,
1335
+ blastRadius,
1336
+ hasOperationalBlockers,
1337
+ memoryGuard,
1338
+ riskScore,
1339
+ }) {
1340
+ return /\bgit\s+push\b|\bgh\s+pr\s+(?:create|merge)\b/i.test(command)
1327
1341
  && !destructiveBypass
1328
1342
  && !learnedHardStop
1329
- && lowBlastRadius
1343
+ && isLowBlastRadius(blastRadius)
1330
1344
  && !hasOperationalBlockers
1331
- && memoryGuard
1332
- && memoryGuard.mode !== 'allow'
1345
+ && memoryGuard?.mode !== 'allow'
1333
1346
  && riskScore <= 0.62;
1334
- const repeatedHighBlast = Boolean(
1335
- memoryGuard
1336
- && memoryGuard.mode === 'block'
1347
+ }
1348
+
1349
+ function isRepeatedHighBlast(memoryGuard, blastRadius) {
1350
+ return Boolean(
1351
+ memoryGuard?.mode === 'block'
1337
1352
  && (
1338
1353
  blastRadius.severity === 'high'
1339
1354
  || blastRadius.severity === 'critical'
@@ -1341,25 +1356,49 @@ function chooseDecision({ riskScore, integrity, memoryGuard, learnedPolicy, blas
1341
1356
  || blastRadius.unapprovedProtectedFiles > 0
1342
1357
  )
1343
1358
  );
1344
- const economicAction = Boolean(actionProfile && actionProfile.economicAction);
1345
- const backgroundAgent = Boolean(actionProfile && actionProfile.backgroundAgent);
1346
- const customerSystemAction = Boolean(actionProfile && actionProfile.customerSystemAction);
1359
+ }
1360
+
1361
+ function hasSoftControlWarning({ workflowContract, workflowControl, costControl, riskScore, learnedWarning, learnedRecall }) {
1362
+ return workflowContract?.mode === 'warn'
1363
+ || workflowControl?.mode === 'warn'
1364
+ || costControl?.mode === 'warn'
1365
+ || riskScore >= 0.45
1366
+ || (learnedWarning && riskScore >= 0.3)
1367
+ || (learnedRecall && riskScore >= 0.34);
1368
+ }
1369
+
1370
+ function chooseDecision({ riskScore, integrity, memoryGuard, learnedPolicy, blastRadius, command, costControl, workflowControl, workflowContract, actionProfile }) {
1371
+ if (costControl?.mode === 'block' || workflowControl?.mode === 'block' || workflowContract?.mode === 'block') {
1372
+ return 'deny';
1373
+ }
1374
+
1375
+ const hasOperationalBlockers = Boolean(integrity?.blockers?.length);
1376
+ const destructiveBypass = isDestructiveBypass(command);
1377
+ const learnedPrediction = getLearnedPrediction(learnedPolicy);
1378
+ const learnedHardStop = learnedPredictionMatches(learnedPrediction, ['deny'], 0.7);
1379
+ const learnedWarning = learnedPredictionMatches(learnedPrediction, ['warn', 'verify', 'deny'], 0.3);
1380
+ const learnedRecall = learnedPredictionMatches(learnedPrediction, ['recall'], 0.3);
1347
1381
 
1348
- if (lowRiskHandoff) {
1382
+ if (isLowRiskHandoff({ command, destructiveBypass, learnedHardStop, blastRadius, hasOperationalBlockers, memoryGuard, riskScore })) {
1349
1383
  return 'allow';
1350
1384
  }
1385
+
1351
1386
  // Background customer-system actions checkpoint (warn), never hard-deny.
1352
1387
  // The checkpoint IS the mitigation — blocking outright prevents legitimate work.
1353
- if (backgroundAgent && customerSystemAction) {
1388
+ if (actionProfile?.backgroundAgent && actionProfile?.customerSystemAction) {
1354
1389
  return 'warn';
1355
1390
  }
1391
+
1392
+ const repeatedHighBlast = isRepeatedHighBlast(memoryGuard, blastRadius);
1356
1393
  if (destructiveBypass || learnedHardStop || repeatedHighBlast || (hasOperationalBlockers && riskScore >= 0.72) || riskScore >= 0.86) {
1357
1394
  return 'deny';
1358
1395
  }
1359
- if (economicAction || (backgroundAgent && riskScore >= 0.3)) {
1396
+
1397
+ if (actionProfile?.economicAction || (actionProfile?.backgroundAgent && riskScore >= 0.3)) {
1360
1398
  return 'warn';
1361
1399
  }
1362
- if ((workflowContract && workflowContract.mode === 'warn') || (workflowControl && workflowControl.mode === 'warn') || (costControl && costControl.mode === 'warn') || riskScore >= 0.45 || (learnedWarning && riskScore >= 0.3) || (learnedRecall && riskScore >= 0.34)) {
1400
+
1401
+ if (hasSoftControlWarning({ workflowContract, workflowControl, costControl, riskScore, learnedWarning, learnedRecall })) {
1363
1402
  return 'warn';
1364
1403
  }
1365
1404
  return 'allow';
@@ -1408,6 +1447,10 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
1408
1447
  baseBranch,
1409
1448
  command: normalizedToolInput.command,
1410
1449
  changedFiles: affectedFiles,
1450
+ currentBranch: options.currentBranch
1451
+ || normalizedToolInput.currentBranch
1452
+ || normalizedToolInput.branchName
1453
+ || normalizedToolInput.branch,
1411
1454
  headSha: options.headSha || toolInput.headSha,
1412
1455
  requirePrForReleaseSensitive: options.requirePrForReleaseSensitive === true,
1413
1456
  requireVersionNotBehindBase: options.requireVersionNotBehindBase === true,