thumbgate 1.27.18 → 1.27.19

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 (96) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +4 -3
  3. package/.well-known/agentic-verify.txt +1 -0
  4. package/.well-known/llms.txt +33 -12
  5. package/.well-known/mcp/server-card.json +8 -8
  6. package/README.md +249 -30
  7. package/adapters/chatgpt/openapi.yaml +12 -0
  8. package/adapters/claude/.mcp.json +2 -2
  9. package/adapters/codex/config.toml +2 -2
  10. package/adapters/gemini/function-declarations.json +1 -0
  11. package/adapters/mcp/server-stdio.js +263 -11
  12. package/adapters/opencode/opencode.json +1 -1
  13. package/bench/thumbgate-bench.json +2 -2
  14. package/bin/cli.js +1429 -121
  15. package/bin/postinstall.js +1 -8
  16. package/config/gate-classifier-routing.json +98 -0
  17. package/config/gate-templates.json +216 -0
  18. package/config/gates/claim-verification.json +12 -0
  19. package/config/gates/default.json +31 -2
  20. package/config/github-about.json +2 -2
  21. package/config/mcp-allowlists.json +23 -13
  22. package/config/merge-quality-checks.json +0 -1
  23. package/config/model-candidates.json +121 -6
  24. package/config/post-deploy-marketing-pages.json +80 -0
  25. package/config/tessl-tiles.json +1 -3
  26. package/openapi/openapi.yaml +12 -0
  27. package/package.json +1 -1
  28. package/public/blog.html +4 -4
  29. package/public/codex-plugin.html +72 -20
  30. package/public/compare.html +31 -8
  31. package/public/dashboard.html +930 -166
  32. package/public/federal.html +2 -2
  33. package/public/guide.html +33 -13
  34. package/public/index.html +469 -111
  35. package/public/learn.html +183 -18
  36. package/public/lessons.html +168 -10
  37. package/public/numbers.html +7 -7
  38. package/public/pro.html +34 -11
  39. package/scripts/agent-memory-lifecycle.js +211 -0
  40. package/scripts/agent-readiness.js +20 -3
  41. package/scripts/agent-reward-model.js +53 -1
  42. package/scripts/auto-promote-gates.js +82 -10
  43. package/scripts/auto-wire-hooks.js +14 -0
  44. package/scripts/billing.js +93 -1
  45. package/scripts/bot-detection.js +61 -3
  46. package/scripts/build-metadata.js +50 -10
  47. package/scripts/cli-feedback.js +4 -2
  48. package/scripts/cli-schema.js +97 -0
  49. package/scripts/cli-telemetry.js +6 -1
  50. package/scripts/commercial-offer.js +82 -2
  51. package/scripts/context-manager.js +74 -6
  52. package/scripts/dashboard.js +68 -2
  53. package/scripts/export-databricks-bundle.js +5 -1
  54. package/scripts/export-dpo-pairs.js +7 -2
  55. package/scripts/feedback-loop.js +123 -1
  56. package/scripts/feedback-quality.js +87 -0
  57. package/scripts/filesystem-search.js +35 -10
  58. package/scripts/gate-stats.js +89 -0
  59. package/scripts/gates-engine.js +1176 -85
  60. package/scripts/gemini-embedding-policy.js +2 -1
  61. package/scripts/hook-runtime.js +20 -14
  62. package/scripts/hook-thumbgate-cache-updater.js +18 -2
  63. package/scripts/hybrid-feedback-context.js +142 -7
  64. package/scripts/lesson-inference.js +8 -3
  65. package/scripts/lesson-search.js +17 -1
  66. package/scripts/license.js +10 -10
  67. package/scripts/llm-client.js +169 -4
  68. package/scripts/local-model-profile.js +15 -8
  69. package/scripts/mcp-config.js +7 -1
  70. package/scripts/memory-scope-readiness.js +159 -0
  71. package/scripts/meta-agent-loop.js +36 -0
  72. package/scripts/operational-integrity.js +39 -5
  73. package/scripts/oss-pr-opportunity-scout.js +35 -5
  74. package/scripts/plausible-server-events.js +9 -6
  75. package/scripts/pro-local-dashboard.js +4 -4
  76. package/scripts/proxy-pointer-rag-guardrails.js +42 -1
  77. package/scripts/published-cli.js +0 -8
  78. package/scripts/rate-limiter.js +64 -13
  79. package/scripts/secret-scanner.js +44 -5
  80. package/scripts/security-scanner.js +260 -10
  81. package/scripts/self-distill-agent.js +3 -1
  82. package/scripts/seo-gsd.js +916 -7
  83. package/scripts/statusline-cache-path.js +17 -2
  84. package/scripts/statusline-local-stats.js +9 -1
  85. package/scripts/statusline-meta.js +28 -2
  86. package/scripts/statusline.sh +20 -4
  87. package/scripts/telemetry-analytics.js +357 -0
  88. package/scripts/thompson-sampling.js +31 -10
  89. package/scripts/thumbgate-bench.js +16 -1
  90. package/scripts/thumbgate-search.js +85 -19
  91. package/scripts/tool-registry.js +169 -1
  92. package/scripts/vector-store.js +45 -0
  93. package/scripts/workflow-sentinel.js +286 -53
  94. package/scripts/workspace-evolver.js +62 -2
  95. package/src/api/server.js +2683 -319
  96. package/scripts/bot-detector.js +0 -50
@@ -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,20 +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
+
1386
+ // Background customer-system actions checkpoint (warn), never hard-deny.
1387
+ // The checkpoint IS the mitigation — blocking outright prevents legitimate work.
1388
+ if (actionProfile?.backgroundAgent && actionProfile?.customerSystemAction) {
1389
+ return 'warn';
1390
+ }
1391
+
1392
+ const repeatedHighBlast = isRepeatedHighBlast(memoryGuard, blastRadius);
1181
1393
  if (destructiveBypass || learnedHardStop || repeatedHighBlast || (hasOperationalBlockers && riskScore >= 0.72) || riskScore >= 0.86) {
1182
1394
  return 'deny';
1183
1395
  }
1184
- if (economicAction || (backgroundAgent && customerSystemAction) || (backgroundAgent && riskScore >= 0.3)) {
1396
+
1397
+ if (actionProfile?.economicAction || (actionProfile?.backgroundAgent && riskScore >= 0.3)) {
1185
1398
  return 'warn';
1186
1399
  }
1187
- 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 })) {
1188
1402
  return 'warn';
1189
1403
  }
1190
1404
  return 'allow';
@@ -1233,11 +1447,24 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
1233
1447
  baseBranch,
1234
1448
  command: normalizedToolInput.command,
1235
1449
  changedFiles: affectedFiles,
1450
+ currentBranch: options.currentBranch
1451
+ || normalizedToolInput.currentBranch
1452
+ || normalizedToolInput.branchName
1453
+ || normalizedToolInput.branch,
1236
1454
  headSha: options.headSha || toolInput.headSha,
1237
1455
  requirePrForReleaseSensitive: options.requirePrForReleaseSensitive === true,
1238
1456
  requireVersionNotBehindBase: options.requireVersionNotBehindBase === true,
1239
1457
  branchGovernance: governanceState.branchGovernance,
1240
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
+ );
1241
1468
  const taskScopeViolation = buildTaskScopeViolation(governanceState.taskScope, affectedFiles);
1242
1469
  const protectedSurface = buildProtectedSurface(governanceState, affectedFiles);
1243
1470
  const protectedSurfaceForRisk = isProtectedApprovalRelevant(normalizedToolName, normalizedToolInput)
@@ -1285,6 +1512,7 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
1285
1512
  protectedSurface: protectedSurfaceForRisk,
1286
1513
  costControl,
1287
1514
  workflowControl,
1515
+ workflowContract,
1288
1516
  actionProfile,
1289
1517
  });
1290
1518
  const executionSurface = buildDockerSandboxPlan({
@@ -1311,6 +1539,7 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
1311
1539
  command: normalizedToolInput.command || '',
1312
1540
  costControl,
1313
1541
  workflowControl,
1542
+ workflowContract,
1314
1543
  actionProfile,
1315
1544
  });
1316
1545
  const evidence = buildEvidence({
@@ -1323,6 +1552,7 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
1323
1552
  normalizedAction,
1324
1553
  costControl,
1325
1554
  workflowControl,
1555
+ workflowContract,
1326
1556
  actionProfile,
1327
1557
  });
1328
1558
  const remediations = buildRemediations({
@@ -1335,6 +1565,7 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
1335
1565
  executionSurface,
1336
1566
  costControl,
1337
1567
  workflowControl,
1568
+ workflowContract,
1338
1569
  actionProfile,
1339
1570
  });
1340
1571
  const summary = decision === 'allow'
@@ -1348,6 +1579,7 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
1348
1579
  normalizedAction,
1349
1580
  costControl,
1350
1581
  workflowControl,
1582
+ workflowContract,
1351
1583
  decision,
1352
1584
  riskScore: risk.score,
1353
1585
  band: risk.band,
@@ -1383,6 +1615,7 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
1383
1615
  protectedSurface: protectedSurfaceForRisk,
1384
1616
  costControl,
1385
1617
  workflowControl,
1618
+ workflowContract,
1386
1619
  actionProfile,
1387
1620
  });
1388
1621
  report.reasoning = buildReasoning(report);
@@ -108,14 +108,74 @@ function parseCommandScore(output = '', status = 0, approvalRate = 0.5) {
108
108
  };
109
109
  }
110
110
 
111
+ function parseCommandLine(cmdString) {
112
+ const args = [];
113
+ let current = '';
114
+ let inDoubleQuote = false;
115
+ let inSingleQuote = false;
116
+ let escaped = false;
117
+
118
+ for (let i = 0; i < cmdString.length; i++) {
119
+ const char = cmdString[i];
120
+
121
+ if (escaped) {
122
+ current += char;
123
+ escaped = false;
124
+ } else if (char === '\\') {
125
+ if (inSingleQuote) {
126
+ current += char;
127
+ } else {
128
+ escaped = true;
129
+ }
130
+ } else if (char === '"' && !inSingleQuote) {
131
+ inDoubleQuote = !inDoubleQuote;
132
+ } else if (char === "'" && !inDoubleQuote) {
133
+ inSingleQuote = !inSingleQuote;
134
+ } else if (char === ' ' && !inDoubleQuote && !inSingleQuote) {
135
+ if (current) {
136
+ args.push(current);
137
+ current = '';
138
+ }
139
+ } else {
140
+ current += char;
141
+ }
142
+ }
143
+ if (current) {
144
+ args.push(current);
145
+ }
146
+ return args;
147
+ }
148
+
111
149
  function runCommand(command, {
112
150
  cwd = process.cwd(),
113
151
  env = process.env,
114
152
  timeoutMs = DEFAULT_TIMEOUT_MS,
115
153
  } = {}) {
116
154
  const startedAt = Date.now();
117
- const result = spawnSync(command, [], {
118
- shell: true,
155
+ const args = parseCommandLine(command);
156
+ const exec = args.shift();
157
+
158
+ const execBase = require('node:path').basename(exec).toLowerCase();
159
+ let safeExec;
160
+ if (exec === process.execPath) {
161
+ safeExec = process.execPath;
162
+ } else if (execBase === 'node' || execBase === 'node.exe') {
163
+ safeExec = process.execPath;
164
+ } else if (execBase === 'npm') {
165
+ safeExec = 'npm';
166
+ } else if (execBase === 'npm.cmd') {
167
+ safeExec = 'npm.cmd';
168
+ } else if (execBase === 'python3') {
169
+ safeExec = 'python3';
170
+ } else if (execBase === 'python') {
171
+ safeExec = 'python';
172
+ } else if (execBase === 'pytest') {
173
+ safeExec = 'pytest';
174
+ } else {
175
+ throw new Error(`Binary ${exec} is not authorized for workspace evolution.`);
176
+ }
177
+
178
+ const result = spawnSync(safeExec, args, {
119
179
  cwd,
120
180
  env,
121
181
  encoding: 'utf8',