thumbgate 1.26.8 → 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.
Files changed (57) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.well-known/agentic-verify.txt +1 -0
  3. package/.well-known/llms.txt +2 -0
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +44 -31
  6. package/adapters/claude/.mcp.json +2 -2
  7. package/adapters/gcp/dfcx-webhook-gate.js +295 -0
  8. package/adapters/mcp/server-stdio.js +41 -1
  9. package/adapters/opencode/opencode.json +1 -1
  10. package/bench/thumbgate-bench.json +2 -2
  11. package/bin/cli.js +184 -8
  12. package/bin/dashboard-cli.js +7 -0
  13. package/config/gate-classifier-routing.json +98 -0
  14. package/config/gate-templates.json +60 -0
  15. package/config/mcp-allowlists.json +8 -7
  16. package/config/model-candidates.json +71 -6
  17. package/package.json +28 -12
  18. package/public/about.html +162 -0
  19. package/public/chatgpt-app.html +330 -0
  20. package/public/codex-plugin.html +66 -14
  21. package/public/compare.html +2 -2
  22. package/public/dashboard.html +224 -36
  23. package/public/guide.html +2 -2
  24. package/public/index.html +122 -40
  25. package/public/learn.html +70 -0
  26. package/public/lessons.html +129 -6
  27. package/public/numbers.html +2 -2
  28. package/public/pricing.html +28 -23
  29. package/public/pro.html +3 -3
  30. package/scripts/agent-operations-planner.js +621 -0
  31. package/scripts/agent-reward-model.js +53 -1
  32. package/scripts/ai-component-inventory.js +367 -0
  33. package/scripts/classifier-routing.js +130 -0
  34. package/scripts/cli-schema.js +26 -0
  35. package/scripts/commercial-offer.js +10 -2
  36. package/scripts/dashboard-chat.js +199 -51
  37. package/scripts/feedback-sanitizer.js +105 -0
  38. package/scripts/gates-engine.js +301 -67
  39. package/scripts/hybrid-feedback-context.js +141 -7
  40. package/scripts/memory-scope-readiness.js +159 -0
  41. package/scripts/oss-pr-opportunity-scout.js +35 -5
  42. package/scripts/parallel-workflow-orchestrator.js +293 -0
  43. package/scripts/plausible-domain-config.js +86 -0
  44. package/scripts/plausible-server-events.js +4 -2
  45. package/scripts/proxy-pointer-rag-guardrails.js +42 -1
  46. package/scripts/qa-scenario-planner.js +136 -0
  47. package/scripts/rate-limiter.js +2 -2
  48. package/scripts/repeat-metric.js +28 -12
  49. package/scripts/secret-fixture-tokens.js +61 -0
  50. package/scripts/secret-scanner.js +44 -5
  51. package/scripts/security-scanner.js +80 -0
  52. package/scripts/seo-gsd.js +113 -0
  53. package/scripts/thumbgate-bench.js +16 -1
  54. package/scripts/tool-registry.js +37 -0
  55. package/scripts/workflow-sentinel.js +282 -54
  56. package/src/api/server.js +466 -60
  57. package/.claude-plugin/marketplace.json +0 -85
@@ -64,11 +64,14 @@ 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?.workflowContract && typeof raw.workflowContract === 'object'
73
+ ? raw.workflowContract
74
+ : null,
72
75
  };
73
76
  }
74
77
 
@@ -361,6 +364,131 @@ function formatFileList(files, limit = 5) {
361
364
  return `${items.slice(0, limit).join(', ')} (+${items.length - limit} more)`;
362
365
  }
363
366
 
367
+ function normalizeStringList(values) {
368
+ if (!Array.isArray(values)) return [];
369
+ return [...new Set(values.map((value) => String(value || '').trim()).filter(Boolean))];
370
+ }
371
+
372
+ function normalizeWorkflowContract(contract) {
373
+ if (!contract || typeof contract !== 'object') return null;
374
+ return {
375
+ workflowId: String(contract.workflowId || contract.workflow_id || '').trim() || null,
376
+ allowedBranches: normalizeStringList(contract.allowedBranches || contract.allowed_branches),
377
+ blockedActions: normalizeStringList(contract.blockedActions || contract.blocked_actions),
378
+ requiredEvidence: normalizeStringList(contract.requiredEvidence || contract.required_evidence),
379
+ completionGate: String(contract.completionGate || contract.completion_gate || '').trim() || null,
380
+ };
381
+ }
382
+
383
+ function commandMatchesPattern(command, pattern) {
384
+ const text = String(command || '');
385
+ const raw = String(pattern || '').trim();
386
+ if (!text || !raw) return false;
387
+ try {
388
+ return new RegExp(raw, 'i').test(text);
389
+ } catch {
390
+ return text.toLowerCase().includes(raw.toLowerCase());
391
+ }
392
+ }
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
+
403
+ function isCompletionLikeAction(command) {
404
+ const text = String(command || '');
405
+ return COMPLETION_ACTION_PATTERNS.some((pattern) => pattern.test(text));
406
+ }
407
+
408
+ function collectEvidenceLabels(toolInput = {}, options = {}) {
409
+ const values = [];
410
+ for (const source of [
411
+ toolInput.evidence,
412
+ toolInput.evidenceLabels,
413
+ toolInput.proof,
414
+ toolInput.proofArtifacts,
415
+ toolInput.requiredEvidenceSatisfied,
416
+ options.evidence,
417
+ options.evidenceLabels,
418
+ options.proofArtifacts,
419
+ ]) {
420
+ if (Array.isArray(source)) values.push(...source);
421
+ else if (source && typeof source === 'object') values.push(...Object.keys(source).filter((key) => source[key]));
422
+ else if (typeof source === 'string') values.push(...source.split(/[,;\n]/));
423
+ }
424
+ return normalizeStringList(values).map((value) => value.toLowerCase());
425
+ }
426
+
427
+ function evaluateWorkflowContract(contractInput, context = {}) {
428
+ const contract = normalizeWorkflowContract(contractInput);
429
+ if (!contract) {
430
+ return {
431
+ active: false,
432
+ contract: null,
433
+ violations: [],
434
+ mode: 'allow',
435
+ };
436
+ }
437
+ const violations = [];
438
+ const command = String(context.command || '');
439
+ const currentBranch = String(context.currentBranch || '').trim();
440
+
441
+ const blockedAction = contract.blockedActions.find((pattern) => commandMatchesPattern(command, pattern));
442
+ if (blockedAction) {
443
+ violations.push({
444
+ code: 'blocked_action',
445
+ severity: 'block',
446
+ message: `Workflow contract blocks this action: ${blockedAction}`,
447
+ pattern: blockedAction,
448
+ });
449
+ }
450
+
451
+ if (contract.allowedBranches.length > 0 && currentBranch) {
452
+ const allowed = contract.allowedBranches.some((glob) => matchesAnyGlob(currentBranch, [glob]));
453
+ if (!allowed) {
454
+ violations.push({
455
+ code: 'branch_outside_contract',
456
+ severity: 'warn',
457
+ message: `Current branch ${currentBranch} is outside the workflow contract.`,
458
+ currentBranch,
459
+ allowedBranches: contract.allowedBranches,
460
+ });
461
+ }
462
+ }
463
+
464
+ if (contract.requiredEvidence.length > 0 && isCompletionLikeAction(command)) {
465
+ const evidenceLabels = collectEvidenceLabels(context.toolInput || {}, context.options || {});
466
+ const missing = contract.requiredEvidence.filter((label) => !evidenceLabels.includes(label.toLowerCase()));
467
+ if (missing.length > 0) {
468
+ violations.push({
469
+ code: 'missing_required_evidence',
470
+ severity: 'block',
471
+ message: `Workflow completion is missing required evidence: ${missing.join(', ')}`,
472
+ missingEvidence: missing,
473
+ });
474
+ }
475
+ }
476
+
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
+ }
484
+ return {
485
+ active: true,
486
+ contract,
487
+ violations,
488
+ mode,
489
+ };
490
+ }
491
+
364
492
  function severityFromScore(score) {
365
493
  if (score >= 0.8) return 'critical';
366
494
  if (score >= 0.55) return 'high';
@@ -434,6 +562,7 @@ function scoreRisk({
434
562
  protectedSurface,
435
563
  costControl,
436
564
  workflowControl,
565
+ workflowContract,
437
566
  actionProfile,
438
567
  }) {
439
568
  const drivers = [];
@@ -590,7 +719,18 @@ function scoreRisk({
590
719
  );
591
720
  }
592
721
  }
593
- if (memoryGuard && memoryGuard.mode && memoryGuard.mode !== 'allow') {
722
+ if (workflowContract?.active && workflowContract.violations.length > 0) {
723
+ for (const violation of workflowContract.violations) {
724
+ addDriver(
725
+ drivers,
726
+ `workflow_contract_${violation.code}`,
727
+ violation.severity === 'block' ? 0.38 : 0.18,
728
+ violation.message,
729
+ { workflowId: workflowContract.contract?.workflowId }
730
+ );
731
+ }
732
+ }
733
+ if (memoryGuard?.mode && memoryGuard.mode !== 'allow') {
594
734
  addDriver(
595
735
  drivers,
596
736
  'memory_recurrence',
@@ -599,7 +739,7 @@ function scoreRisk({
599
739
  { mode: memoryGuard.mode }
600
740
  );
601
741
  }
602
- if (learnedPolicy && learnedPolicy.enabled && learnedPolicy.prediction) {
742
+ if (learnedPolicy?.enabled && learnedPolicy.prediction) {
603
743
  const confidence = learnedPolicy.prediction.confidence || 0;
604
744
  const label = learnedPolicy.prediction.label;
605
745
  if (label === 'deny' && confidence >= 0.6) {
@@ -663,6 +803,7 @@ function buildEvidence({
663
803
  normalizedAction,
664
804
  costControl,
665
805
  workflowControl,
806
+ workflowContract,
666
807
  actionProfile,
667
808
  }) {
668
809
  const evidence = [];
@@ -683,10 +824,17 @@ function buildEvidence({
683
824
  evidence.push(`Workflow control ${workflowControl.mode}: ${workflowControl.reasons.join(' ')}`);
684
825
  }
685
826
  }
686
- if (actionProfile && actionProfile.backgroundAgent) {
827
+ if (workflowContract?.active) {
828
+ const workflowId = workflowContract.contract?.workflowId || 'unnamed';
829
+ evidence.push(`Workflow contract active: ${workflowId}.`);
830
+ for (const violation of workflowContract.violations.slice(0, 3)) {
831
+ evidence.push(`Workflow contract ${violation.severity}: ${violation.message}`);
832
+ }
833
+ }
834
+ if (actionProfile?.backgroundAgent) {
687
835
  evidence.push('Background or scheduled agent context detected for this action.');
688
836
  }
689
- if (actionProfile && actionProfile.economicAction) {
837
+ if (actionProfile?.economicAction) {
690
838
  evidence.push('Economic action keywords detected (billing, refunds, payouts, invoices, or subscriptions).');
691
839
  }
692
840
  if (actionProfile && actionProfile.customerSystemAction) {
@@ -801,6 +949,7 @@ function buildRemediations({
801
949
  executionSurface,
802
950
  costControl,
803
951
  workflowControl,
952
+ workflowContract,
804
953
  actionProfile,
805
954
  }) {
806
955
  const remediations = [];
@@ -895,7 +1044,7 @@ function buildRemediations({
895
1044
  'Isolated execution limits host damage when a high-risk local action goes wrong.'
896
1045
  );
897
1046
  }
898
- if (costControl && costControl.mode && costControl.mode !== 'allow') {
1047
+ if (costControl?.mode && costControl.mode !== 'allow') {
899
1048
  push(
900
1049
  'reduce_model_budget',
901
1050
  'Reduce model budget before execution',
@@ -903,6 +1052,33 @@ function buildRemediations({
903
1052
  'High token or cost estimates should be reviewed before the model/tool loop continues.'
904
1053
  );
905
1054
  }
1055
+ if (workflowContract?.active && workflowContract.violations.length > 0) {
1056
+ const codes = new Set(workflowContract.violations.map((violation) => violation.code));
1057
+ if (codes.has('missing_required_evidence')) {
1058
+ push(
1059
+ 'attach_workflow_evidence',
1060
+ 'Attach workflow evidence before completion',
1061
+ 'Attach every required evidence label from the workflow contract before commit, push, PR, merge, release, or publish.',
1062
+ 'Deterministic workflows should not claim completion until the run contract is proven.'
1063
+ );
1064
+ }
1065
+ if (codes.has('branch_outside_contract')) {
1066
+ push(
1067
+ 'switch_to_contract_branch',
1068
+ 'Move to an allowed workflow branch',
1069
+ 'Switch to a branch allowed by the workflow contract or update the contract before retrying.',
1070
+ 'Workflow contracts define where repeatable agent runs are allowed to mutate state.'
1071
+ );
1072
+ }
1073
+ if (codes.has('blocked_action')) {
1074
+ push(
1075
+ 'remove_blocked_workflow_action',
1076
+ 'Remove blocked workflow action',
1077
+ 'Change the workflow step or request an explicit contract update before retrying this action.',
1078
+ 'The run contract blocks this action before execution.'
1079
+ );
1080
+ }
1081
+ }
906
1082
  if (workflowControl && workflowControl.mode && workflowControl.mode !== 'allow') {
907
1083
  push(
908
1084
  'add_environment_inspection',
@@ -1068,6 +1244,7 @@ function buildDecisionControl({
1068
1244
  protectedSurface,
1069
1245
  costControl,
1070
1246
  workflowControl,
1247
+ workflowContract,
1071
1248
  actionProfile,
1072
1249
  }) {
1073
1250
  const reversibility = classifyReversibility({
@@ -1077,16 +1254,19 @@ function buildDecisionControl({
1077
1254
  protectedSurface,
1078
1255
  actionProfile,
1079
1256
  });
1080
- const hasOperationalBlockers = Boolean(integrity && Array.isArray(integrity.blockers) && integrity.blockers.length > 0);
1081
- const hasCostWarning = Boolean(costControl && costControl.mode === 'warn');
1082
- const hasCostBlock = Boolean(costControl && costControl.mode === 'block');
1083
- const hasWorkflowWarning = Boolean(workflowControl && workflowControl.mode === 'warn');
1084
- const hasWorkflowBlock = Boolean(workflowControl && workflowControl.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';
1085
1264
  const requiresCheckpoint = decision === 'warn'
1086
- || (decision === 'allow' && (reversibility !== 'two_way_door' || hasOperationalBlockers || hasCostWarning || hasWorkflowWarning));
1265
+ || (decision === 'allow' && (reversibility !== 'two_way_door' || hasOperationalBlockers || hasCostWarning || hasWorkflowWarning || hasContractWarning));
1087
1266
  const executionMode = decision === 'deny'
1088
1267
  || hasCostBlock
1089
1268
  || hasWorkflowBlock
1269
+ || hasContractBlock
1090
1270
  ? 'blocked'
1091
1271
  : requiresCheckpoint
1092
1272
  ? 'checkpoint_required'
@@ -1110,7 +1290,7 @@ function buildDecisionControl({
1110
1290
  decisionOwner,
1111
1291
  reversibility,
1112
1292
  deliberation,
1113
- requiresHumanApproval: (executionMode === 'checkpoint_required' && decisionOwner !== 'agent') || hasCostBlock || hasWorkflowBlock,
1293
+ requiresHumanApproval: (executionMode === 'checkpoint_required' && decisionOwner !== 'agent') || hasCostBlock || hasWorkflowBlock || hasContractBlock,
1114
1294
  recommendedAction: executionMode === 'blocked'
1115
1295
  ? 'halt'
1116
1296
  : executionMode === 'checkpoint_required'
@@ -1124,46 +1304,51 @@ function buildDecisionControl({
1124
1304
  };
1125
1305
  }
1126
1306
 
1127
- function chooseDecision({ riskScore, integrity, memoryGuard, learnedPolicy, blastRadius, command, costControl, workflowControl, actionProfile }) {
1128
- const hasOperationalBlockers = Boolean(integrity && Array.isArray(integrity.blockers) && integrity.blockers.length > 0);
1129
- if (costControl && costControl.mode === 'block') {
1130
- return 'deny';
1131
- }
1132
- if (workflowControl && workflowControl.mode === 'block') {
1133
- return 'deny';
1134
- }
1135
- const destructiveBypass = /\bgit\s+push\b.*(?:--force|-f)\b/i.test(command) || /\bgh\s+pr\s+merge\b.*--admin\b/i.test(command);
1136
- const learnedPrediction = learnedPolicy && learnedPolicy.enabled ? learnedPolicy.prediction : null;
1137
- const learnedHardStop = Boolean(
1138
- learnedPrediction
1139
- && learnedPrediction.label === 'deny'
1140
- && learnedPrediction.confidence >= 0.7
1141
- );
1142
- const learnedWarning = Boolean(
1143
- learnedPrediction
1144
- && ['warn', 'verify', 'deny'].includes(learnedPrediction.label)
1145
- && learnedPrediction.confidence >= 0.3
1146
- );
1147
- const learnedRecall = Boolean(
1148
- learnedPrediction
1149
- && learnedPrediction.label === 'recall'
1150
- && 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
1151
1321
  );
1152
- const lowBlastRadius = blastRadius.fileCount <= 1
1322
+ }
1323
+
1324
+ function isLowBlastRadius(blastRadius) {
1325
+ return blastRadius.fileCount <= 1
1153
1326
  && blastRadius.surfaceCount <= 1
1154
1327
  && blastRadius.releaseSensitiveFiles.length === 0
1155
1328
  && blastRadius.unapprovedProtectedFiles === 0;
1156
- 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)
1157
1341
  && !destructiveBypass
1158
1342
  && !learnedHardStop
1159
- && lowBlastRadius
1343
+ && isLowBlastRadius(blastRadius)
1160
1344
  && !hasOperationalBlockers
1161
- && memoryGuard
1162
- && memoryGuard.mode !== 'allow'
1345
+ && memoryGuard?.mode !== 'allow'
1163
1346
  && riskScore <= 0.62;
1164
- const repeatedHighBlast = Boolean(
1165
- memoryGuard
1166
- && memoryGuard.mode === 'block'
1347
+ }
1348
+
1349
+ function isRepeatedHighBlast(memoryGuard, blastRadius) {
1350
+ return Boolean(
1351
+ memoryGuard?.mode === 'block'
1167
1352
  && (
1168
1353
  blastRadius.severity === 'high'
1169
1354
  || blastRadius.severity === 'critical'
@@ -1171,25 +1356,49 @@ function chooseDecision({ riskScore, integrity, memoryGuard, learnedPolicy, blas
1171
1356
  || blastRadius.unapprovedProtectedFiles > 0
1172
1357
  )
1173
1358
  );
1174
- const economicAction = Boolean(actionProfile && actionProfile.economicAction);
1175
- const backgroundAgent = Boolean(actionProfile && actionProfile.backgroundAgent);
1176
- 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
+ }
1177
1369
 
1178
- if (lowRiskHandoff) {
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);
1381
+
1382
+ if (isLowRiskHandoff({ command, destructiveBypass, learnedHardStop, blastRadius, hasOperationalBlockers, memoryGuard, riskScore })) {
1179
1383
  return 'allow';
1180
1384
  }
1385
+
1181
1386
  // Background customer-system actions checkpoint (warn), never hard-deny.
1182
1387
  // The checkpoint IS the mitigation — blocking outright prevents legitimate work.
1183
- if (backgroundAgent && customerSystemAction) {
1388
+ if (actionProfile?.backgroundAgent && actionProfile?.customerSystemAction) {
1184
1389
  return 'warn';
1185
1390
  }
1391
+
1392
+ const repeatedHighBlast = isRepeatedHighBlast(memoryGuard, blastRadius);
1186
1393
  if (destructiveBypass || learnedHardStop || repeatedHighBlast || (hasOperationalBlockers && riskScore >= 0.72) || riskScore >= 0.86) {
1187
1394
  return 'deny';
1188
1395
  }
1189
- if (economicAction || (backgroundAgent && riskScore >= 0.3)) {
1396
+
1397
+ if (actionProfile?.economicAction || (actionProfile?.backgroundAgent && riskScore >= 0.3)) {
1190
1398
  return 'warn';
1191
1399
  }
1192
- if ((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 })) {
1193
1402
  return 'warn';
1194
1403
  }
1195
1404
  return 'allow';
@@ -1238,11 +1447,24 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
1238
1447
  baseBranch,
1239
1448
  command: normalizedToolInput.command,
1240
1449
  changedFiles: affectedFiles,
1450
+ currentBranch: options.currentBranch
1451
+ || normalizedToolInput.currentBranch
1452
+ || normalizedToolInput.branchName
1453
+ || normalizedToolInput.branch,
1241
1454
  headSha: options.headSha || toolInput.headSha,
1242
1455
  requirePrForReleaseSensitive: options.requirePrForReleaseSensitive === true,
1243
1456
  requireVersionNotBehindBase: options.requireVersionNotBehindBase === true,
1244
1457
  branchGovernance: governanceState.branchGovernance,
1245
1458
  });
1459
+ const workflowContract = evaluateWorkflowContract(
1460
+ normalizedToolInput.workflowContract || options.workflowContract || governanceState.workflowContract,
1461
+ {
1462
+ command: normalizedToolInput.command || '',
1463
+ currentBranch: integrity.currentBranch,
1464
+ toolInput: normalizedToolInput,
1465
+ options,
1466
+ }
1467
+ );
1246
1468
  const taskScopeViolation = buildTaskScopeViolation(governanceState.taskScope, affectedFiles);
1247
1469
  const protectedSurface = buildProtectedSurface(governanceState, affectedFiles);
1248
1470
  const protectedSurfaceForRisk = isProtectedApprovalRelevant(normalizedToolName, normalizedToolInput)
@@ -1290,6 +1512,7 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
1290
1512
  protectedSurface: protectedSurfaceForRisk,
1291
1513
  costControl,
1292
1514
  workflowControl,
1515
+ workflowContract,
1293
1516
  actionProfile,
1294
1517
  });
1295
1518
  const executionSurface = buildDockerSandboxPlan({
@@ -1316,6 +1539,7 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
1316
1539
  command: normalizedToolInput.command || '',
1317
1540
  costControl,
1318
1541
  workflowControl,
1542
+ workflowContract,
1319
1543
  actionProfile,
1320
1544
  });
1321
1545
  const evidence = buildEvidence({
@@ -1328,6 +1552,7 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
1328
1552
  normalizedAction,
1329
1553
  costControl,
1330
1554
  workflowControl,
1555
+ workflowContract,
1331
1556
  actionProfile,
1332
1557
  });
1333
1558
  const remediations = buildRemediations({
@@ -1340,6 +1565,7 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
1340
1565
  executionSurface,
1341
1566
  costControl,
1342
1567
  workflowControl,
1568
+ workflowContract,
1343
1569
  actionProfile,
1344
1570
  });
1345
1571
  const summary = decision === 'allow'
@@ -1353,6 +1579,7 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
1353
1579
  normalizedAction,
1354
1580
  costControl,
1355
1581
  workflowControl,
1582
+ workflowContract,
1356
1583
  decision,
1357
1584
  riskScore: risk.score,
1358
1585
  band: risk.band,
@@ -1388,6 +1615,7 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
1388
1615
  protectedSurface: protectedSurfaceForRisk,
1389
1616
  costControl,
1390
1617
  workflowControl,
1618
+ workflowContract,
1391
1619
  actionProfile,
1392
1620
  });
1393
1621
  report.reasoning = buildReasoning(report);