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.
- package/.claude-plugin/README.md +25 -0
- package/.claude-plugin/marketplace.json +32 -13
- package/.claude-plugin/plugin.json +15 -2
- package/.well-known/llms.txt +60 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +242 -126
- package/adapters/README.md +1 -1
- package/adapters/chatgpt/INSTALL.md +59 -4
- package/adapters/chatgpt/openapi.yaml +168 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +84 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +204 -13
- package/bin/postinstall.js +8 -2
- package/config/budget.json +18 -0
- package/config/gates/code-edit.json +61 -0
- package/config/gates/db-write.json +61 -0
- package/config/gates/default.json +154 -3
- package/config/gates/deploy.json +61 -0
- package/config/github-about.json +2 -1
- package/config/merge-quality-checks.json +23 -0
- package/openapi/openapi.yaml +168 -0
- package/package.json +47 -11
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +1 -1
- package/plugins/codex-profile/INSTALL.md +27 -4
- package/plugins/codex-profile/README.md +33 -9
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/blog.html +73 -0
- package/public/compare/mem0.html +189 -0
- package/public/compare/speclock.html +180 -0
- package/public/compare.html +10 -2
- package/public/guide.html +2 -2
- package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
- package/public/guides/codex-cli-guardrails.html +158 -0
- package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
- package/public/guides/pre-action-gates.html +162 -0
- package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
- package/public/index.html +172 -65
- package/public/lessons.html +33 -24
- package/public/llm-context.md +140 -0
- package/public/pro.html +24 -22
- package/scripts/access-anomaly-detector.js +1 -1
- package/scripts/adk-consolidator.js +1 -5
- package/scripts/agent-security-hardening.js +4 -6
- package/scripts/agentic-data-pipeline.js +1 -3
- package/scripts/async-job-runner.js +1 -5
- package/scripts/audit-trail.js +1 -5
- package/scripts/auto-promote-gates.js +5 -3
- package/scripts/background-agent-governance.js +2 -10
- package/scripts/billing-setup.js +109 -0
- package/scripts/billing.js +2 -16
- package/scripts/budget-enforcer.js +173 -0
- package/scripts/build-claude-mcpb.js +71 -5
- package/scripts/build-codex-plugin.js +152 -0
- package/scripts/check-congruence.js +132 -14
- package/scripts/commercial-offer.js +5 -7
- package/scripts/content-engine/linkedin-content-generator.js +154 -0
- package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
- package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
- package/scripts/content-engine/reddit-thread-finder.js +154 -0
- package/scripts/context-engine.js +21 -6
- package/scripts/contextfs.js +1 -21
- package/scripts/dashboard.js +20 -0
- package/scripts/decision-journal.js +341 -0
- package/scripts/delegation-runtime.js +1 -5
- package/scripts/distribution-surfaces.js +54 -0
- package/scripts/document-intake.js +927 -0
- package/scripts/ephemeral-agent-store.js +1 -8
- package/scripts/evolution-state.js +1 -5
- package/scripts/experiment-tracker.js +1 -5
- package/scripts/export-databricks-bundle.js +1 -5
- package/scripts/export-hf-dataset.js +1 -5
- package/scripts/export-training.js +1 -5
- package/scripts/feedback-attribution.js +1 -16
- package/scripts/feedback-history-distiller.js +1 -16
- package/scripts/feedback-loop.js +1 -5
- package/scripts/feedback-root-consolidator.js +2 -21
- package/scripts/feedback-session.js +49 -0
- package/scripts/feedback-to-rules.js +215 -36
- package/scripts/filesystem-search.js +1 -9
- package/scripts/fs-utils.js +104 -0
- package/scripts/gates-engine.js +200 -11
- package/scripts/github-about.js +32 -8
- package/scripts/gtm-revenue-loop.js +1 -5
- package/scripts/harness-selector.js +148 -0
- package/scripts/hosted-config.js +2 -0
- package/scripts/hosted-job-launcher.js +1 -5
- package/scripts/hybrid-feedback-context.js +33 -49
- package/scripts/intervention-policy.js +58 -1
- package/scripts/lesson-db.js +3 -18
- package/scripts/lesson-inference.js +194 -16
- package/scripts/lesson-retrieval.js +60 -24
- package/scripts/llm-client.js +59 -0
- package/scripts/managed-lesson-agent.js +183 -0
- package/scripts/marketing-experiment.js +8 -22
- package/scripts/meta-agent-loop.js +624 -0
- package/scripts/metered-billing.js +1 -1
- package/scripts/money-watcher.js +1 -4
- package/scripts/obsidian-export.js +1 -5
- package/scripts/operational-integrity.js +15 -3
- package/scripts/operational-summary.js +41 -5
- package/scripts/org-dashboard.js +6 -1
- package/scripts/per-step-scoring.js +2 -4
- package/scripts/pr-manager.js +201 -19
- package/scripts/pro-features.js +3 -2
- package/scripts/prompt-dlp.js +3 -3
- package/scripts/prove-adapters.js +1 -5
- package/scripts/prove-attribution.js +1 -5
- package/scripts/prove-automation.js +1 -3
- package/scripts/prove-cloudflare-sandbox.js +1 -3
- package/scripts/prove-data-pipeline.js +1 -3
- package/scripts/prove-intelligence.js +1 -3
- package/scripts/prove-lancedb.js +1 -5
- package/scripts/prove-local-intelligence.js +1 -3
- package/scripts/prove-packaged-runtime.js +75 -9
- package/scripts/prove-predictive-insights.js +1 -3
- package/scripts/prove-training-export.js +1 -3
- package/scripts/prove-workflow-contract.js +1 -5
- package/scripts/ralph-loop.js +376 -0
- package/scripts/ralph-mode-ci.js +331 -0
- package/scripts/rate-limiter.js +3 -1
- package/scripts/reddit-dm-outreach.js +14 -4
- package/scripts/rotate-stripe-webhook-secret.js +314 -0
- package/scripts/schedule-manager.js +3 -5
- package/scripts/security-scanner.js +448 -0
- package/scripts/self-distill-agent.js +579 -0
- package/scripts/semantic-dedup.js +115 -0
- package/scripts/skill-exporter.js +1 -3
- package/scripts/skill-generator.js +1 -5
- package/scripts/social-analytics/engagement-audit.js +1 -18
- package/scripts/social-analytics/pollers/linkedin.js +26 -16
- package/scripts/social-analytics/publishers/linkedin.js +1 -1
- package/scripts/social-analytics/publishers/zernio.js +51 -0
- package/scripts/social-pipeline.js +1 -3
- package/scripts/social-post-hourly.js +47 -4
- package/scripts/statusline-links.js +6 -5
- package/scripts/statusline.sh +29 -153
- package/scripts/sync-branch-protection.js +340 -0
- package/scripts/tessl-export.js +1 -3
- package/scripts/thumbgate-search.js +32 -1
- package/scripts/tool-kpi-tracker.js +1 -1
- package/scripts/tool-registry.js +106 -2
- package/scripts/vector-store.js +1 -5
- package/scripts/weekly-auto-post.js +1 -1
- package/scripts/workflow-sentinel.js +91 -0
- package/skills/thumbgate/SKILL.md +1 -1
- package/src/api/server.js +296 -7
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
- /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 };
|
package/scripts/gates-engine.js
CHANGED
|
@@ -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)
|
|
598
|
+
if (EDIT_LIKE_TOOLS.has(toolName)) return true;
|
|
579
599
|
if (toolName !== 'Bash') return false;
|
|
580
600
|
const command = String(toolInput.command || '');
|
|
581
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 —
|
|
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
|
-
|
|
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
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|
package/scripts/github-about.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
139
|
-
'config/github-about.json
|
|
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.
|
|
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 =
|
|
318
|
-
const delayMs =
|
|
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.
|
|
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();
|