thumbgate 1.3.0 → 1.4.1

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 (156) hide show
  1. package/.claude-plugin/README.md +25 -0
  2. package/.claude-plugin/marketplace.json +32 -13
  3. package/.claude-plugin/plugin.json +15 -2
  4. package/.well-known/llms.txt +60 -0
  5. package/.well-known/mcp/server-card.json +1 -1
  6. package/README.md +242 -126
  7. package/adapters/README.md +1 -1
  8. package/adapters/chatgpt/INSTALL.md +59 -4
  9. package/adapters/chatgpt/openapi.yaml +168 -0
  10. package/adapters/claude/.mcp.json +2 -2
  11. package/adapters/codex/config.toml +2 -2
  12. package/adapters/mcp/server-stdio.js +84 -1
  13. package/adapters/opencode/opencode.json +1 -1
  14. package/bin/cli.js +204 -13
  15. package/bin/postinstall.js +8 -2
  16. package/config/budget.json +18 -0
  17. package/config/gates/code-edit.json +61 -0
  18. package/config/gates/db-write.json +61 -0
  19. package/config/gates/default.json +154 -3
  20. package/config/gates/deploy.json +61 -0
  21. package/config/github-about.json +2 -1
  22. package/config/merge-quality-checks.json +23 -0
  23. package/openapi/openapi.yaml +168 -0
  24. package/package.json +47 -11
  25. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  26. package/plugins/claude-codex-bridge/.mcp.json +1 -1
  27. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
  28. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  29. package/plugins/codex-profile/.mcp.json +1 -1
  30. package/plugins/codex-profile/INSTALL.md +27 -4
  31. package/plugins/codex-profile/README.md +33 -9
  32. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  33. package/plugins/opencode-profile/INSTALL.md +1 -1
  34. package/public/blog.html +73 -0
  35. package/public/compare/mem0.html +189 -0
  36. package/public/compare/speclock.html +180 -0
  37. package/public/compare.html +10 -2
  38. package/public/guide.html +2 -2
  39. package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
  40. package/public/guides/codex-cli-guardrails.html +158 -0
  41. package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
  42. package/public/guides/pre-action-gates.html +162 -0
  43. package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
  44. package/public/index.html +172 -65
  45. package/public/lessons.html +33 -24
  46. package/public/llm-context.md +140 -0
  47. package/public/pro.html +24 -22
  48. package/scripts/access-anomaly-detector.js +1 -1
  49. package/scripts/adk-consolidator.js +1 -5
  50. package/scripts/agent-security-hardening.js +4 -6
  51. package/scripts/agentic-data-pipeline.js +1 -3
  52. package/scripts/async-job-runner.js +1 -5
  53. package/scripts/audit-trail.js +1 -5
  54. package/scripts/auto-promote-gates.js +5 -3
  55. package/scripts/background-agent-governance.js +2 -10
  56. package/scripts/billing-setup.js +109 -0
  57. package/scripts/billing.js +2 -16
  58. package/scripts/budget-enforcer.js +173 -0
  59. package/scripts/build-claude-mcpb.js +71 -5
  60. package/scripts/build-codex-plugin.js +152 -0
  61. package/scripts/check-congruence.js +132 -14
  62. package/scripts/commercial-offer.js +5 -7
  63. package/scripts/content-engine/linkedin-content-generator.js +154 -0
  64. package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
  65. package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
  66. package/scripts/content-engine/reddit-thread-finder.js +154 -0
  67. package/scripts/context-engine.js +21 -6
  68. package/scripts/contextfs.js +1 -21
  69. package/scripts/dashboard.js +20 -0
  70. package/scripts/decision-journal.js +341 -0
  71. package/scripts/delegation-runtime.js +1 -5
  72. package/scripts/distribution-surfaces.js +54 -0
  73. package/scripts/document-intake.js +927 -0
  74. package/scripts/ephemeral-agent-store.js +1 -8
  75. package/scripts/evolution-state.js +1 -5
  76. package/scripts/experiment-tracker.js +1 -5
  77. package/scripts/export-databricks-bundle.js +1 -5
  78. package/scripts/export-hf-dataset.js +1 -5
  79. package/scripts/export-training.js +1 -5
  80. package/scripts/feedback-attribution.js +1 -16
  81. package/scripts/feedback-history-distiller.js +1 -16
  82. package/scripts/feedback-loop.js +1 -5
  83. package/scripts/feedback-root-consolidator.js +2 -21
  84. package/scripts/feedback-session.js +49 -0
  85. package/scripts/feedback-to-rules.js +215 -36
  86. package/scripts/filesystem-search.js +1 -9
  87. package/scripts/fs-utils.js +104 -0
  88. package/scripts/gates-engine.js +200 -11
  89. package/scripts/github-about.js +32 -8
  90. package/scripts/gtm-revenue-loop.js +1 -5
  91. package/scripts/harness-selector.js +148 -0
  92. package/scripts/hosted-config.js +2 -0
  93. package/scripts/hosted-job-launcher.js +1 -5
  94. package/scripts/hybrid-feedback-context.js +33 -49
  95. package/scripts/intervention-policy.js +58 -1
  96. package/scripts/lesson-db.js +3 -18
  97. package/scripts/lesson-inference.js +194 -16
  98. package/scripts/lesson-retrieval.js +60 -24
  99. package/scripts/llm-client.js +59 -0
  100. package/scripts/managed-lesson-agent.js +183 -0
  101. package/scripts/marketing-experiment.js +8 -22
  102. package/scripts/meta-agent-loop.js +624 -0
  103. package/scripts/metered-billing.js +1 -1
  104. package/scripts/money-watcher.js +1 -4
  105. package/scripts/obsidian-export.js +1 -5
  106. package/scripts/operational-integrity.js +15 -3
  107. package/scripts/operational-summary.js +41 -5
  108. package/scripts/org-dashboard.js +6 -1
  109. package/scripts/per-step-scoring.js +2 -4
  110. package/scripts/pr-manager.js +201 -19
  111. package/scripts/pro-features.js +3 -2
  112. package/scripts/prompt-dlp.js +3 -3
  113. package/scripts/prove-adapters.js +1 -5
  114. package/scripts/prove-attribution.js +1 -5
  115. package/scripts/prove-automation.js +1 -3
  116. package/scripts/prove-cloudflare-sandbox.js +1 -3
  117. package/scripts/prove-data-pipeline.js +1 -3
  118. package/scripts/prove-intelligence.js +1 -3
  119. package/scripts/prove-lancedb.js +1 -5
  120. package/scripts/prove-local-intelligence.js +1 -3
  121. package/scripts/prove-packaged-runtime.js +75 -9
  122. package/scripts/prove-predictive-insights.js +1 -3
  123. package/scripts/prove-training-export.js +1 -3
  124. package/scripts/prove-workflow-contract.js +1 -5
  125. package/scripts/ralph-loop.js +376 -0
  126. package/scripts/ralph-mode-ci.js +331 -0
  127. package/scripts/rate-limiter.js +3 -1
  128. package/scripts/reddit-dm-outreach.js +14 -4
  129. package/scripts/rotate-stripe-webhook-secret.js +314 -0
  130. package/scripts/schedule-manager.js +3 -5
  131. package/scripts/security-scanner.js +448 -0
  132. package/scripts/self-distill-agent.js +579 -0
  133. package/scripts/semantic-dedup.js +115 -0
  134. package/scripts/skill-exporter.js +1 -3
  135. package/scripts/skill-generator.js +1 -5
  136. package/scripts/social-analytics/engagement-audit.js +1 -18
  137. package/scripts/social-analytics/pollers/linkedin.js +26 -16
  138. package/scripts/social-analytics/publishers/linkedin.js +1 -1
  139. package/scripts/social-analytics/publishers/zernio.js +51 -0
  140. package/scripts/social-pipeline.js +1 -3
  141. package/scripts/social-post-hourly.js +47 -4
  142. package/scripts/statusline-links.js +6 -5
  143. package/scripts/statusline.sh +29 -153
  144. package/scripts/sync-branch-protection.js +340 -0
  145. package/scripts/tessl-export.js +1 -3
  146. package/scripts/thumbgate-search.js +32 -1
  147. package/scripts/tool-kpi-tracker.js +1 -1
  148. package/scripts/tool-registry.js +106 -2
  149. package/scripts/vector-store.js +1 -5
  150. package/scripts/weekly-auto-post.js +1 -1
  151. package/scripts/workflow-sentinel.js +91 -0
  152. package/skills/thumbgate/SKILL.md +1 -1
  153. package/src/api/server.js +296 -7
  154. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  155. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  156. /package/scripts/social-analytics/db/{social-analytics.db-wal → analytics.sqlite} +0 -0
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Shared filesystem utilities.
6
+ *
7
+ * Consolidates ensureDir() and readJsonl() which were duplicated
8
+ * across 43 and 19 files respectively.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ /**
15
+ * Recursively create a directory if it does not exist.
16
+ * @param {string} dirPath
17
+ */
18
+ function ensureDir(dirPath) {
19
+ if (!fs.existsSync(dirPath)) {
20
+ fs.mkdirSync(dirPath, { recursive: true });
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Recursively create the parent directory for a file path.
26
+ * @param {string} filePath
27
+ */
28
+ function ensureParentDir(filePath) {
29
+ ensureDir(path.dirname(filePath));
30
+ }
31
+
32
+ /**
33
+ * Read a JSONL (JSON Lines) file into an array of parsed objects.
34
+ * Silently skips malformed lines and returns [] if file is missing.
35
+ *
36
+ * @param {string} filePath
37
+ * @param {object} [options]
38
+ * @param {number} [options.maxLines] - Read at most N lines (from the end if reverse=true)
39
+ * @param {boolean} [options.reverse] - Read lines in reverse order (most recent first)
40
+ * @param {boolean} [options.tail] - Read from the end while preserving chronological order
41
+ * @returns {object[]}
42
+ */
43
+ function readJsonl(filePath, options = {}) {
44
+ if (!filePath || !fs.existsSync(filePath)) return [];
45
+ const raw = fs.readFileSync(filePath, 'utf-8').trim();
46
+ if (!raw) return [];
47
+
48
+ const normalizedOptions = typeof options === 'number'
49
+ ? { maxLines: options, tail: true }
50
+ : (options || {});
51
+ let lines = raw.split('\n');
52
+
53
+ if (normalizedOptions.tail && normalizedOptions.maxLines > 0) {
54
+ lines = lines.slice(-normalizedOptions.maxLines);
55
+ }
56
+
57
+ if (normalizedOptions.reverse) {
58
+ lines = lines.reverse();
59
+ }
60
+
61
+ if (!normalizedOptions.tail && normalizedOptions.maxLines && normalizedOptions.maxLines > 0) {
62
+ lines = lines.slice(0, normalizedOptions.maxLines);
63
+ }
64
+
65
+ return lines
66
+ .map((line) => {
67
+ try {
68
+ return JSON.parse(line);
69
+ } catch {
70
+ return null;
71
+ }
72
+ })
73
+ .filter(Boolean);
74
+ }
75
+
76
+ /**
77
+ * Append a JSON object as a line to a JSONL file.
78
+ * Creates parent directories if they do not exist.
79
+ *
80
+ * @param {string} filePath
81
+ * @param {object} payload
82
+ */
83
+ function appendJsonl(filePath, payload) {
84
+ ensureParentDir(filePath);
85
+ fs.appendFileSync(filePath, JSON.stringify(payload) + '\n');
86
+ }
87
+
88
+ /**
89
+ * Write a JSON object to a file with pretty-printing.
90
+ * Creates parent directories if they do not exist.
91
+ *
92
+ * @param {string} filePath
93
+ * @param {object} payload
94
+ */
95
+ function writeJson(filePath, payload) {
96
+ ensureParentDir(filePath);
97
+ fs.writeFileSync(filePath, JSON.stringify(payload, null, 2) + '\n');
98
+ }
99
+
100
+ function readJsonlTail(filePath, limit) {
101
+ return readJsonl(filePath, { maxLines: limit, tail: true });
102
+ }
103
+
104
+ module.exports = { ensureDir, ensureParentDir, readJsonl, readJsonlTail, appendJsonl, writeJson };
@@ -14,6 +14,10 @@ const {
14
14
  const {
15
15
  evaluateWorkflowSentinel,
16
16
  } = require('./workflow-sentinel');
17
+ const {
18
+ recordDecisionEvaluation,
19
+ recordDecisionOutcome,
20
+ } = require('./decision-journal');
17
21
 
18
22
  /**
19
23
  * Computes the SHA-256 hash of an executable binary to prevent path-based bypasses.
@@ -47,6 +51,9 @@ const {
47
51
  buildSafeSummary,
48
52
  redactText,
49
53
  } = require('./secret-scanner');
54
+ const {
55
+ evaluateSecurityScan,
56
+ } = require('./security-scanner');
50
57
  const { getAutoGatesPath } = require('./auto-promote-gates');
51
58
  const { recordAuditEvent, auditToFeedback } = require('./audit-trail');
52
59
 
@@ -81,7 +88,7 @@ const HIGH_RISK_BASH_PATTERN = /\b(?:git\s+(?:add|commit|push)|gh\s+pr\s+(?:crea
81
88
  // Config loading
82
89
  // ---------------------------------------------------------------------------
83
90
 
84
- function loadGatesConfig(configPath) {
91
+ function loadGatesConfig(configPath, harnessPath) {
85
92
  const primaryPath = configPath || process.env.THUMBGATE_GATES_CONFIG || DEFAULT_CONFIG_PATH;
86
93
 
87
94
  if (!fs.existsSync(primaryPath)) {
@@ -120,6 +127,15 @@ function loadGatesConfig(configPath) {
120
127
  mergedConfig.gates.push(...limitedAutoGates);
121
128
  }
122
129
 
130
+ // Load workflow-specific harness gates (always additive, never replaces default).
131
+ // Resolved by harness-selector based on tool name + command context.
132
+ const resolvedHarness = harnessPath || process.env.THUMBGATE_HARNESS_CONFIG;
133
+ if (resolvedHarness && fs.existsSync(resolvedHarness)) {
134
+ const harnessGates = (loadOne(resolvedHarness, false) || [])
135
+ .map(g => ({ ...g, layer: g.layer || 'Execution', source: g.source || 'harness' }));
136
+ mergedConfig.gates.push(...harnessGates);
137
+ }
138
+
123
139
  return mergedConfig;
124
140
  }
125
141
 
@@ -407,11 +423,15 @@ function recordStat(gateId, action, gate) {
407
423
  const stats = loadStats();
408
424
  if (action === 'block') stats.blocked = (stats.blocked || 0) + 1;
409
425
  else if (action === 'warn') stats.warned = (stats.warned || 0) + 1;
426
+ else if (action === 'approve') stats.pendingApproval = (stats.pendingApproval || 0) + 1;
427
+ else if (action === 'log') stats.logged = (stats.logged || 0) + 1;
410
428
  else stats.passed = (stats.passed || 0) + 1;
411
429
  if (!stats.byGate) stats.byGate = {};
412
- if (!stats.byGate[gateId]) stats.byGate[gateId] = { blocked: 0, warned: 0 };
430
+ if (!stats.byGate[gateId]) stats.byGate[gateId] = { blocked: 0, warned: 0, pendingApproval: 0, logged: 0 };
413
431
  if (action === 'block') stats.byGate[gateId].blocked += 1;
414
432
  else if (action === 'warn') stats.byGate[gateId].warned += 1;
433
+ else if (action === 'approve') stats.byGate[gateId].pendingApproval = (stats.byGate[gateId].pendingApproval || 0) + 1;
434
+ else if (action === 'log') stats.byGate[gateId].logged = (stats.byGate[gateId].logged || 0) + 1;
415
435
  saveStats(stats);
416
436
  // Track lesson freshness when an auto-promoted gate fires
417
437
  if (gate && gate.sourceLessonId) {
@@ -575,10 +595,18 @@ function extractAffectedFiles(toolName, toolInput = {}) {
575
595
  }
576
596
 
577
597
  function isHighRiskAction(toolName, toolInput = {}, affectedFiles = []) {
578
- if (EDIT_LIKE_TOOLS.has(toolName) && affectedFiles.length > 0) return true;
598
+ if (EDIT_LIKE_TOOLS.has(toolName)) return true;
579
599
  if (toolName !== 'Bash') return false;
580
600
  const command = String(toolInput.command || '');
581
- return HIGH_RISK_BASH_PATTERN.test(command);
601
+ // Original high-risk pattern (git writes, publishes, destructive ops)
602
+ if (HIGH_RISK_BASH_PATTERN.test(command)) return true;
603
+ // Broadened: any Bash command that modifies files or has side effects.
604
+ // Excludes pure read/analysis commands (node --test, cat, ls, echo, etc.)
605
+ // to avoid false positives on benign operations.
606
+ if (/\b(sed|awk|mv|cp|chmod|chown|truncate|tee|patch)\b/.test(command)) return true;
607
+ if (/\b(npm\s+(?:run|exec|install)|yarn|pnpm)\b/.test(command)) return true;
608
+ if (/\b(curl|wget)\b/.test(command)) return true;
609
+ return false;
582
610
  }
583
611
 
584
612
  function isScopeEnforcedAction(toolName, toolInput = {}, affectedFiles = []) {
@@ -997,6 +1025,47 @@ function buildSentinelGateResult(report) {
997
1025
  };
998
1026
  }
999
1027
 
1028
+ function recordSentinelDecision(report, toolName, toolInput) {
1029
+ if (!report) return null;
1030
+ const entry = recordDecisionEvaluation(report, {
1031
+ source: 'gates-engine',
1032
+ toolName,
1033
+ toolInput,
1034
+ changedFiles: report && report.blastRadius && Array.isArray(report.blastRadius.affectedFiles)
1035
+ ? report.blastRadius.affectedFiles
1036
+ : [],
1037
+ });
1038
+ report.actionId = entry.actionId;
1039
+ if (report.decisionControl && !report.decisionControl.actionId) {
1040
+ report.decisionControl.actionId = entry.actionId;
1041
+ }
1042
+ return entry;
1043
+ }
1044
+
1045
+ function recordMemoryGuardDecision(sentinelDecision, enrichedMemoryGuard) {
1046
+ if (!sentinelDecision) return;
1047
+ recordDecisionOutcome({
1048
+ actionId: sentinelDecision.actionId,
1049
+ outcome: 'blocked',
1050
+ actualDecision: 'deny',
1051
+ actor: 'system',
1052
+ source: 'gates-engine',
1053
+ notes: enrichedMemoryGuard.message,
1054
+ });
1055
+ }
1056
+
1057
+ function recordSentinelBlockDecision(sentinelDecision, sentinelResult) {
1058
+ if (!sentinelDecision) return;
1059
+ recordDecisionOutcome({
1060
+ actionId: sentinelDecision.actionId,
1061
+ outcome: sentinelResult.decision === 'deny' ? 'blocked' : 'warned',
1062
+ actualDecision: sentinelResult.decision,
1063
+ actor: 'system',
1064
+ source: 'workflow-sentinel',
1065
+ notes: sentinelResult.message,
1066
+ });
1067
+ }
1068
+
1000
1069
  function enrichResultWithSentinel(result, report) {
1001
1070
  if (!result || !report || report.decision === 'allow') {
1002
1071
  return result;
@@ -1036,7 +1105,12 @@ async function checkMetricCondition(metricCondition) {
1036
1105
  async function evaluateGatesAsync(toolName, toolInput, configPath) {
1037
1106
  let config;
1038
1107
  try {
1039
- config = loadGatesConfig(configPath);
1108
+ let harnessPath;
1109
+ try {
1110
+ const { selectHarness } = require('./harness-selector');
1111
+ harnessPath = selectHarness(toolName, toolInput);
1112
+ } catch { /* harness-selector is optional */ }
1113
+ config = loadGatesConfig(configPath, harnessPath);
1040
1114
  } catch {
1041
1115
  return null;
1042
1116
  }
@@ -1095,6 +1169,23 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
1095
1169
  return result;
1096
1170
  }
1097
1171
 
1172
+ if (gate.action === 'approve') {
1173
+ recordStat(gate.id, 'approve', gate);
1174
+ const result = { decision: 'approve', gate: gate.id, message, severity: gate.severity, reasoning, requiresApproval: true };
1175
+ const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'approve', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
1176
+ auditToFeedback(auditRecord);
1177
+ return result;
1178
+ }
1179
+
1180
+ if (gate.action === 'log') {
1181
+ recordStat(gate.id, 'log', gate);
1182
+ const result = { decision: 'log', gate: gate.id, message, severity: gate.severity, reasoning, logged: true };
1183
+ const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'log', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
1184
+ auditToFeedback(auditRecord);
1185
+ // 'log' action allows the tool call to proceed — do not return early, continue to next gate
1186
+ continue;
1187
+ }
1188
+
1098
1189
  if (gate.action === 'warn') {
1099
1190
  recordStat(gate.id, 'warn', gate);
1100
1191
  const result = { decision: 'warn', gate: gate.id, message, severity: gate.severity, reasoning };
@@ -1107,10 +1198,12 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
1107
1198
  const sentinelReport = evaluateWorkflowSentinel(toolName, toolInput, {
1108
1199
  governanceState: loadGovernanceState(),
1109
1200
  });
1201
+ const sentinelDecision = recordSentinelDecision(sentinelReport, toolName, toolInput);
1110
1202
  const memoryGuard = evaluateMemoryGuard(toolName, toolInput);
1111
1203
  if (memoryGuard) {
1112
1204
  const enrichedMemoryGuard = enrichResultWithSentinel(memoryGuard, sentinelReport);
1113
1205
  recordStat(enrichedMemoryGuard.gate, 'block');
1206
+ recordMemoryGuardDecision(sentinelDecision, enrichedMemoryGuard);
1114
1207
  const auditRecord = recordAuditEvent({
1115
1208
  toolName,
1116
1209
  toolInput,
@@ -1127,6 +1220,7 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
1127
1220
  if (sentinelReport && sentinelReport.decision !== 'allow') {
1128
1221
  const sentinelResult = buildSentinelGateResult(sentinelReport);
1129
1222
  recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn');
1223
+ recordSentinelBlockDecision(sentinelDecision, sentinelResult);
1130
1224
  const auditRecord = recordAuditEvent({
1131
1225
  toolName,
1132
1226
  toolInput,
@@ -1148,7 +1242,12 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
1148
1242
  function evaluateGates(toolName, toolInput, configPath) {
1149
1243
  let config;
1150
1244
  try {
1151
- config = loadGatesConfig(configPath);
1245
+ let harnessPath;
1246
+ try {
1247
+ const { selectHarness } = require('./harness-selector');
1248
+ harnessPath = selectHarness(toolName, toolInput);
1249
+ } catch { /* harness-selector is optional */ }
1250
+ config = loadGatesConfig(configPath, harnessPath);
1152
1251
  } catch {
1153
1252
  // If config can't be loaded, pass through
1154
1253
  return null;
@@ -1181,6 +1280,22 @@ function evaluateGates(toolName, toolInput, configPath) {
1181
1280
  return result;
1182
1281
  }
1183
1282
 
1283
+ if (gate.action === 'approve') {
1284
+ recordStat(gate.id, 'approve', gate);
1285
+ const result = { decision: 'approve', gate: gate.id, message, severity: gate.severity, reasoning, requiresApproval: true };
1286
+ const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'approve', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
1287
+ auditToFeedback(auditRecord);
1288
+ return result;
1289
+ }
1290
+
1291
+ if (gate.action === 'log') {
1292
+ recordStat(gate.id, 'log', gate);
1293
+ const auditRecord = recordAuditEvent({ toolName, toolInput, decision: 'log', gateId: gate.id, message, severity: gate.severity, source: 'gates-engine' });
1294
+ auditToFeedback(auditRecord);
1295
+ // 'log' action allows the tool call to proceed — continue to next gate
1296
+ continue;
1297
+ }
1298
+
1184
1299
  if (gate.action === 'warn') {
1185
1300
  recordStat(gate.id, 'warn', gate);
1186
1301
  const result = { decision: 'warn', gate: gate.id, message, severity: gate.severity, reasoning };
@@ -1193,10 +1308,12 @@ function evaluateGates(toolName, toolInput, configPath) {
1193
1308
  const sentinelReport = evaluateWorkflowSentinel(toolName, toolInput, {
1194
1309
  governanceState: loadGovernanceState(),
1195
1310
  });
1311
+ const sentinelDecision = recordSentinelDecision(sentinelReport, toolName, toolInput);
1196
1312
  const memoryGuard = evaluateMemoryGuard(toolName, toolInput);
1197
1313
  if (memoryGuard) {
1198
1314
  const enrichedMemoryGuard = enrichResultWithSentinel(memoryGuard, sentinelReport);
1199
1315
  recordStat(enrichedMemoryGuard.gate, 'block');
1316
+ recordMemoryGuardDecision(sentinelDecision, enrichedMemoryGuard);
1200
1317
  const auditRecord = recordAuditEvent({
1201
1318
  toolName,
1202
1319
  toolInput,
@@ -1213,6 +1330,7 @@ function evaluateGates(toolName, toolInput, configPath) {
1213
1330
  if (sentinelReport && sentinelReport.decision !== 'allow') {
1214
1331
  const sentinelResult = buildSentinelGateResult(sentinelReport);
1215
1332
  recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn');
1333
+ recordSentinelBlockDecision(sentinelDecision, sentinelResult);
1216
1334
  const auditRecord = recordAuditEvent({
1217
1335
  toolName,
1218
1336
  toolInput,
@@ -1338,9 +1456,16 @@ function evaluateSecretGuard(input = {}) {
1338
1456
  // PreToolUse hook interface (stdin/stdout JSON)
1339
1457
  // ---------------------------------------------------------------------------
1340
1458
 
1341
- function formatOutput(result) {
1459
+ function formatOutput(result, behavioralContext) {
1342
1460
  if (!result) {
1343
- // No gate matched — pass through
1461
+ // No gate matched — inject behavioral context if available
1462
+ if (behavioralContext) {
1463
+ return JSON.stringify({
1464
+ hookSpecificOutput: {
1465
+ additionalContext: behavioralContext,
1466
+ },
1467
+ });
1468
+ }
1344
1469
  return JSON.stringify({});
1345
1470
  }
1346
1471
 
@@ -1358,9 +1483,10 @@ function formatOutput(result) {
1358
1483
  }
1359
1484
 
1360
1485
  if (result.decision === 'warn') {
1486
+ const extra = behavioralContext ? `\n${behavioralContext}` : '';
1361
1487
  return JSON.stringify({
1362
1488
  hookSpecificOutput: {
1363
- additionalContext: `[GATE:${result.gate}] WARNING: ${result.message}${reasoningSuffix}`,
1489
+ additionalContext: `[GATE:${result.gate}] WARNING: ${result.message}${reasoningSuffix}${extra}`,
1364
1490
  },
1365
1491
  });
1366
1492
  }
@@ -1368,16 +1494,58 @@ function formatOutput(result) {
1368
1494
  return JSON.stringify({});
1369
1495
  }
1370
1496
 
1497
+ /**
1498
+ * Build behavioral context string from recurring feedback patterns.
1499
+ * Injected as additionalContext on EVERY tool call so the AI constantly
1500
+ * sees its failure patterns — even when no gate blocks.
1501
+ */
1502
+ function buildBehavioralContext() {
1503
+ const hybrid = getHybridFeedbackModule();
1504
+ if (!hybrid || typeof hybrid.buildHybridState !== 'function') return null;
1505
+
1506
+ try {
1507
+ const state = hybrid.buildHybridState({});
1508
+ if (!state || !state.recurringNegativePatterns || state.recurringNegativePatterns.length === 0) {
1509
+ return null;
1510
+ }
1511
+
1512
+ const constraints = hybrid.deriveConstraints(state, 3);
1513
+ if (constraints.length === 0) return null;
1514
+
1515
+ return `[ThumbGate] Recurring failure patterns (enforce these):\n${constraints.map(c => ` - ${c}`).join('\n')}`;
1516
+ } catch {
1517
+ return null;
1518
+ }
1519
+ }
1520
+
1371
1521
  async function runAsync(input) {
1372
1522
  const secretGuard = evaluateSecretGuard(input);
1373
1523
  if (secretGuard) {
1374
1524
  return formatOutput(secretGuard);
1375
1525
  }
1376
1526
 
1527
+ // Security vulnerability scan (Tier 1: pattern match, Tier 2: supply chain)
1528
+ const securityScan = evaluateSecurityScan(input);
1529
+ if (securityScan && securityScan.decision === 'deny') {
1530
+ return formatOutput(securityScan);
1531
+ }
1532
+
1377
1533
  const toolName = input.tool_name || '';
1378
1534
  const toolInput = input.tool_input || {};
1379
1535
  const result = await evaluateGatesAsync(toolName, toolInput);
1380
- return formatOutput(result);
1536
+
1537
+ // Attach security warnings to allow/warn results
1538
+ if (securityScan && securityScan.decision === 'warn') {
1539
+ if (result) {
1540
+ result.securityWarnings = securityScan.securityScan.findings;
1541
+ result.reasoning = (result.reasoning || []).concat(securityScan.reasoning);
1542
+ } else {
1543
+ return formatOutput(securityScan);
1544
+ }
1545
+ }
1546
+
1547
+ const behavioralContext = buildBehavioralContext();
1548
+ return formatOutput(result, behavioralContext);
1381
1549
  }
1382
1550
 
1383
1551
  function run(input) {
@@ -1386,10 +1554,28 @@ function run(input) {
1386
1554
  return formatOutput(secretGuard);
1387
1555
  }
1388
1556
 
1557
+ // Security vulnerability scan (Tier 1: pattern match, Tier 2: supply chain)
1558
+ const securityScan = evaluateSecurityScan(input);
1559
+ if (securityScan && securityScan.decision === 'deny') {
1560
+ return formatOutput(securityScan);
1561
+ }
1562
+
1389
1563
  const toolName = input.tool_name || '';
1390
1564
  const toolInput = input.tool_input || {};
1391
1565
  const result = evaluateGates(toolName, toolInput);
1392
- return formatOutput(result);
1566
+
1567
+ // Attach security warnings to allow/warn results
1568
+ if (securityScan && securityScan.decision === 'warn') {
1569
+ if (result) {
1570
+ result.securityWarnings = securityScan.securityScan.findings;
1571
+ result.reasoning = (result.reasoning || []).concat(securityScan.reasoning);
1572
+ } else {
1573
+ return formatOutput(securityScan);
1574
+ }
1575
+ }
1576
+
1577
+ const behavioralContext = buildBehavioralContext();
1578
+ return formatOutput(result, behavioralContext);
1393
1579
  }
1394
1580
 
1395
1581
  // ---------------------------------------------------------------------------
@@ -1580,6 +1766,7 @@ module.exports = {
1580
1766
  saveStats,
1581
1767
  recordStat,
1582
1768
  evaluateSecretGuard,
1769
+ evaluateSecurityScan,
1583
1770
  buildSecretGuardResult,
1584
1771
  buildReasoning,
1585
1772
  matchesGate,
@@ -1608,6 +1795,8 @@ module.exports = {
1608
1795
  SESSION_ACTION_TTL_MS,
1609
1796
  PROTECTED_APPROVAL_TTL_MS,
1610
1797
  DEFAULT_PROTECTED_FILE_GLOBS,
1798
+ buildBehavioralContext,
1799
+ isHighRiskAction,
1611
1800
  };
1612
1801
 
1613
1802
  // ---------------------------------------------------------------------------
@@ -14,6 +14,9 @@ const LEGACY_REPOSITORY_URL = 'https://github.com/IgorGanapolsky/thumbgate';
14
14
  const GITHUB_API_BASE_URL = 'https://api.github.com';
15
15
  const DEFAULT_VERIFY_ATTEMPTS = 5;
16
16
  const DEFAULT_VERIFY_DELAY_MS = 2000;
17
+ const MAX_GITHUB_DESCRIPTION_LENGTH = 160;
18
+ const VERIFY_ATTEMPTS_ENV = 'THUMBGATE_GITHUB_ABOUT_VERIFY_ATTEMPTS';
19
+ const VERIFY_DELAY_MS_ENV = 'THUMBGATE_GITHUB_ABOUT_VERIFY_DELAY_MS';
17
20
 
18
21
  function readText(root, relativePath) {
19
22
  return fs.readFileSync(path.join(root, relativePath), 'utf8');
@@ -63,11 +66,14 @@ function hasRepositoryUrl(text, targetUrl) {
63
66
 
64
67
  function loadGitHubAboutConfig(root = ROOT) {
65
68
  const about = readJson(root, CONFIG_RELATIVE_PATH);
69
+ const metaDescription = normalizeText(about.metaDescription || about.description);
70
+ const githubDescription = normalizeText(about.githubDescription || about.description);
66
71
  return {
67
72
  repo: normalizeText(about.repo),
68
73
  repositoryUrl: normalizeText(about.repositoryUrl),
69
74
  homepageUrl: normalizeText(about.homepageUrl),
70
- description: normalizeText(about.description),
75
+ githubDescription,
76
+ metaDescription,
71
77
  topics: normalizeTopics(about.topics),
72
78
  };
73
79
  }
@@ -101,7 +107,7 @@ function compareGitHubAbout(expected, actual, label = 'Live GitHub About') {
101
107
  const actualHomepage = normalizeText(actual.homepageUrl || actual.homepage);
102
108
  const actualTopics = normalizeTopics(actual.topics);
103
109
 
104
- if (actualDescription !== expected.description) {
110
+ if (actualDescription !== expected.githubDescription) {
105
111
  errors.push(`${label} description mismatch`);
106
112
  }
107
113
  if (actualHomepage !== expected.homepageUrl) {
@@ -135,8 +141,12 @@ function collectLocalGitHubAboutErrors(root = ROOT) {
135
141
  }
136
142
 
137
143
  check(
138
- extractMetaDescription(landingHtml) === about.description,
139
- 'config/github-about.json description must match public/index.html meta description'
144
+ extractMetaDescription(landingHtml) === about.metaDescription,
145
+ 'config/github-about.json metaDescription must match public/index.html meta description'
146
+ );
147
+ check(
148
+ about.githubDescription.length <= MAX_GITHUB_DESCRIPTION_LENGTH,
149
+ `config/github-about.json githubDescription must be ${MAX_GITHUB_DESCRIPTION_LENGTH} characters or fewer for GitHub repo metadata`
140
150
  );
141
151
  check(
142
152
  packageJson.homepage === about.homepageUrl,
@@ -179,7 +189,11 @@ function collectLocalGitHubAboutErrors(root = ROOT) {
179
189
  'docs/MARKETING_COPY_CONGRUENCE.md must reference config/github-about.json as the source of truth'
180
190
  );
181
191
  check(
182
- marketingCopy.includes(about.description),
192
+ marketingCopy.includes(about.metaDescription),
193
+ 'docs/MARKETING_COPY_CONGRUENCE.md must include the canonical landing meta description'
194
+ );
195
+ check(
196
+ marketingCopy.includes(about.githubDescription),
183
197
  'docs/MARKETING_COPY_CONGRUENCE.md must include the canonical GitHub About description'
184
198
  );
185
199
  check(
@@ -303,6 +317,13 @@ function normalizePositiveInteger(value, fallback) {
303
317
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
304
318
  }
305
319
 
320
+ function resolveVerifySetting(optionValue, envName, fallback) {
321
+ if (optionValue !== undefined) {
322
+ return normalizePositiveInteger(optionValue, fallback);
323
+ }
324
+ return normalizePositiveInteger(process.env[envName], fallback);
325
+ }
326
+
306
327
  function sleep(delayMs) {
307
328
  return new Promise((resolve) => {
308
329
  setTimeout(resolve, delayMs);
@@ -314,8 +335,8 @@ async function verifyLiveGitHubAbout(options = {}) {
314
335
  const expected = options.expected || loadGitHubAboutConfig(root);
315
336
  const repo = normalizeText(options.repo) || expected.repo;
316
337
  const label = options.label || `Live GitHub About (${repo})`;
317
- const attempts = normalizePositiveInteger(options.attempts, DEFAULT_VERIFY_ATTEMPTS);
318
- const delayMs = normalizePositiveInteger(options.delayMs, DEFAULT_VERIFY_DELAY_MS);
338
+ const attempts = resolveVerifySetting(options.attempts, VERIFY_ATTEMPTS_ENV, DEFAULT_VERIFY_ATTEMPTS);
339
+ const delayMs = resolveVerifySetting(options.delayMs, VERIFY_DELAY_MS_ENV, DEFAULT_VERIFY_DELAY_MS);
319
340
  const fetcher = typeof options.fetcher === 'function' ? options.fetcher : fetchLiveGitHubAbout;
320
341
  const sleeper = typeof options.sleep === 'function' ? options.sleep : sleep;
321
342
  let actual = null;
@@ -362,7 +383,7 @@ async function updateLiveGitHubAbout(options = {}) {
362
383
  method: 'PATCH',
363
384
  headers: buildGitHubApiHeaders(token),
364
385
  body: JSON.stringify({
365
- description: about.description,
386
+ description: about.githubDescription,
366
387
  homepage: about.homepageUrl,
367
388
  }),
368
389
  });
@@ -390,6 +411,9 @@ module.exports = {
390
411
  DEFAULT_VERIFY_ATTEMPTS,
391
412
  DEFAULT_VERIFY_DELAY_MS,
392
413
  LEGACY_REPOSITORY_URL,
414
+ MAX_GITHUB_DESCRIPTION_LENGTH,
415
+ VERIFY_ATTEMPTS_ENV,
416
+ VERIFY_DELAY_MS_ENV,
393
417
  buildCanonicalRepoUrls,
394
418
  collectLocalGitHubAboutErrors,
395
419
  compareGitHubAbout,
@@ -6,6 +6,7 @@ const path = require('node:path');
6
6
  const { spawnSync } = require('node:child_process');
7
7
  const { resolveHostedBillingConfig } = require('./hosted-config');
8
8
  const { getOperationalBillingSummary } = require('./operational-summary');
9
+ const { ensureDir } = require('./fs-utils');
9
10
 
10
11
  const COMMERCIAL_TRUTH_LINK = 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/COMMERCIAL_TRUTH.md';
11
12
  const VERIFICATION_EVIDENCE_LINK = 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md';
@@ -65,11 +66,6 @@ function clampTargetCount(value) {
65
66
  return Math.max(1, Math.min(parsed, 12));
66
67
  }
67
68
 
68
- function ensureDir(dirPath) {
69
- if (!dirPath) return;
70
- fs.mkdirSync(dirPath, { recursive: true });
71
- }
72
-
73
69
  function normalizeText(value) {
74
70
  if (value === undefined || value === null) return '';
75
71
  return String(value).trim();