thumbgate 0.9.13 → 1.0.0
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/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +6 -3
- package/adapters/README.md +1 -1
- package/adapters/chatgpt/openapi.yaml +105 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/forge/forge.yaml +28 -0
- package/adapters/mcp/server-stdio.js +32 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +53 -3
- package/config/mcp-allowlists.json +10 -0
- package/openapi/openapi.yaml +105 -0
- package/package.json +4 -4
- package/plugins/amp-skill/INSTALL.md +3 -4
- package/plugins/amp-skill/SKILL.md +0 -1
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- package/plugins/claude-skill/INSTALL.md +1 -2
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +1 -1
- package/plugins/codex-profile/INSTALL.md +1 -1
- package/plugins/codex-profile/README.md +1 -1
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/blog.html +1 -0
- package/public/dashboard.html +1 -1
- package/public/guide.html +1 -1
- package/public/index.html +29 -5
- package/public/learn/agent-harness-pattern.html +1 -1
- package/public/learn/ai-agent-persistent-memory.html +1 -1
- package/public/learn/mcp-pre-action-gates-explained.html +1 -1
- package/public/learn/stop-ai-agent-force-push.html +1 -1
- package/public/learn/vibe-coding-safety-net.html +1 -1
- package/public/learn.html +62 -1
- package/public/lessons.html +1 -1
- package/public/pro.html +1 -1
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/agent-security-hardening.js +4 -4
- package/scripts/async-job-runner.js +84 -24
- package/scripts/auto-wire-hooks.js +59 -1
- package/scripts/context-manager.js +330 -0
- package/scripts/dashboard.js +1 -1
- package/scripts/distribution-surfaces.js +12 -0
- package/scripts/ensure-repo-bootstrap.js +15 -14
- package/scripts/feedback-history-distiller.js +7 -1
- package/scripts/feedback-loop.js +10 -4
- package/scripts/feedback-paths.js +142 -10
- package/scripts/feedback-root-consolidator.js +18 -4
- package/scripts/gates-engine.js +96 -10
- package/scripts/hook-auto-capture.sh +1 -1
- package/scripts/hosted-job-launcher.js +260 -0
- package/scripts/managed-dpo-export.js +91 -0
- package/scripts/obsidian-export.js +0 -1
- package/scripts/operational-integrity.js +50 -7
- package/scripts/post-everywhere.js +10 -0
- package/scripts/prove-lancedb.js +62 -4
- package/scripts/publish-decision.js +16 -0
- package/scripts/self-healing-check.js +6 -1
- package/scripts/seo-gsd.js +217 -4
- package/scripts/social-analytics/load-env.js +33 -2
- package/scripts/social-analytics/store.js +200 -2
- package/scripts/statusline-cache-path.js +9 -6
- package/scripts/sync-version.js +18 -11
- package/scripts/tool-registry.js +37 -0
- package/scripts/train_from_feedback.py +0 -4
- package/scripts/workflow-sentinel.js +793 -0
- package/src/api/server.js +297 -38
- /package/scripts/{rlhf_session_start.sh → thumbgate_session_start.sh} +0 -0
|
@@ -5,12 +5,13 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
7
|
const REPO_ROOT = path.resolve(process.argv[2] || process.cwd());
|
|
8
|
-
const
|
|
8
|
+
const THUMBGATE_ENTRY = {
|
|
9
9
|
command: 'npx',
|
|
10
10
|
args: ['-y', 'thumbgate@latest', 'serve'],
|
|
11
11
|
};
|
|
12
|
-
const
|
|
13
|
-
const
|
|
12
|
+
const MCP_SERVER_KEY = 'thumbgate';
|
|
13
|
+
const LEGACY_SERVER_NAMES = ['rlhf', 'mcp-memory-gateway', 'rlhf_feedback_loop'];
|
|
14
|
+
const INFO_EXCLUDE_ENTRIES = ['.thumbgate/', '.mcp.json'];
|
|
14
15
|
|
|
15
16
|
function readJson(filePath) {
|
|
16
17
|
try {
|
|
@@ -36,11 +37,11 @@ function writeJsonIfChanged(filePath, value) {
|
|
|
36
37
|
return true;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
function
|
|
40
|
+
function mergeThumbgateEntry(entry = {}) {
|
|
40
41
|
return {
|
|
41
42
|
...entry,
|
|
42
|
-
command:
|
|
43
|
-
args:
|
|
43
|
+
command: THUMBGATE_ENTRY.command,
|
|
44
|
+
args: THUMBGATE_ENTRY.args.slice(),
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -49,7 +50,7 @@ function ensureMcpJson(repoRoot) {
|
|
|
49
50
|
const existing = readJson(filePath);
|
|
50
51
|
const config = existing && typeof existing === 'object' ? existing : {};
|
|
51
52
|
config.mcpServers = config.mcpServers && typeof config.mcpServers === 'object' ? config.mcpServers : {};
|
|
52
|
-
config.mcpServers
|
|
53
|
+
config.mcpServers[MCP_SERVER_KEY] = mergeThumbgateEntry(config.mcpServers[MCP_SERVER_KEY]);
|
|
53
54
|
for (const legacyName of LEGACY_SERVER_NAMES) {
|
|
54
55
|
delete config.mcpServers[legacyName];
|
|
55
56
|
}
|
|
@@ -63,13 +64,13 @@ function ensureClaudeSettings(repoRoot) {
|
|
|
63
64
|
return false;
|
|
64
65
|
}
|
|
65
66
|
const hasRelevantServer =
|
|
66
|
-
Boolean(existing.mcpServers && existing.mcpServers
|
|
67
|
+
Boolean(existing.mcpServers && existing.mcpServers[MCP_SERVER_KEY]) ||
|
|
67
68
|
LEGACY_SERVER_NAMES.some((name) => Boolean(existing.mcpServers && existing.mcpServers[name]));
|
|
68
69
|
if (!hasRelevantServer) {
|
|
69
70
|
return false;
|
|
70
71
|
}
|
|
71
72
|
existing.mcpServers = existing.mcpServers && typeof existing.mcpServers === 'object' ? existing.mcpServers : {};
|
|
72
|
-
existing.mcpServers
|
|
73
|
+
existing.mcpServers[MCP_SERVER_KEY] = mergeThumbgateEntry(existing.mcpServers[MCP_SERVER_KEY]);
|
|
73
74
|
for (const legacyName of LEGACY_SERVER_NAMES) {
|
|
74
75
|
delete existing.mcpServers[legacyName];
|
|
75
76
|
}
|
|
@@ -106,19 +107,19 @@ function ensureInfoExclude(repoRoot) {
|
|
|
106
107
|
return true;
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
function
|
|
110
|
-
const
|
|
111
|
-
if (fs.existsSync(
|
|
110
|
+
function ensureThumbgateDir(repoRoot) {
|
|
111
|
+
const thumbgateDir = path.join(repoRoot, '.thumbgate');
|
|
112
|
+
if (fs.existsSync(thumbgateDir)) {
|
|
112
113
|
return false;
|
|
113
114
|
}
|
|
114
|
-
fs.mkdirSync(
|
|
115
|
+
fs.mkdirSync(thumbgateDir, { recursive: true });
|
|
115
116
|
return true;
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
function main() {
|
|
119
120
|
const results = {
|
|
120
121
|
repoRoot: REPO_ROOT,
|
|
121
|
-
|
|
122
|
+
createdThumbgateDir: ensureThumbgateDir(REPO_ROOT),
|
|
122
123
|
updatedMcpJson: ensureMcpJson(REPO_ROOT),
|
|
123
124
|
updatedClaudeSettings: ensureClaudeSettings(REPO_ROOT),
|
|
124
125
|
updatedInfoExclude: ensureInfoExclude(REPO_ROOT),
|
|
@@ -79,7 +79,13 @@ function readJsonlTail(filePath, limit = DEFAULT_HISTORY_LIMIT) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
function resolveFeedbackDir(feedbackDir) {
|
|
82
|
-
|
|
82
|
+
if (feedbackDir) {
|
|
83
|
+
return resolveSharedFeedbackDir({ feedbackDir });
|
|
84
|
+
}
|
|
85
|
+
const env = { ...process.env };
|
|
86
|
+
delete env.INIT_CWD;
|
|
87
|
+
delete env.PWD;
|
|
88
|
+
return resolveSharedFeedbackDir({ env });
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
function getConversationPaths(feedbackDir) {
|
package/scripts/feedback-loop.js
CHANGED
|
@@ -146,8 +146,8 @@ function updateStatuslineWithLesson({ accepted, signal, memoryId, feedbackId, le
|
|
|
146
146
|
} catch { /* statusline update is best-effort */ }
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
function getFeedbackPaths() {
|
|
150
|
-
return resolveFeedbackPaths();
|
|
149
|
+
function getFeedbackPaths(options = {}) {
|
|
150
|
+
return resolveFeedbackPaths(options);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
function getContextFsModule() {
|
|
@@ -1673,8 +1673,8 @@ function writePreventionRules(filePath, minOccurrences = 2) {
|
|
|
1673
1673
|
return { path: outPath, markdown };
|
|
1674
1674
|
}
|
|
1675
1675
|
|
|
1676
|
-
function feedbackSummary(recentN = 20) {
|
|
1677
|
-
const { FEEDBACK_LOG_PATH } = getFeedbackPaths();
|
|
1676
|
+
function feedbackSummary(recentN = 20, options = {}) {
|
|
1677
|
+
const { FEEDBACK_LOG_PATH } = getFeedbackPaths(options);
|
|
1678
1678
|
const entries = readJSONL(FEEDBACK_LOG_PATH);
|
|
1679
1679
|
if (entries.length === 0) {
|
|
1680
1680
|
return '## Feedback Summary\nNo feedback recorded yet.';
|
|
@@ -1794,6 +1794,10 @@ function runTests() {
|
|
|
1794
1794
|
const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'thumbgate-loop-test-'));
|
|
1795
1795
|
const localFeedbackLog = path.join(tmpDir, 'feedback-log.jsonl');
|
|
1796
1796
|
process.env.THUMBGATE_FEEDBACK_DIR = tmpDir;
|
|
1797
|
+
const savedInitCwd = process.env.INIT_CWD;
|
|
1798
|
+
process.env.INIT_CWD = savedInitCwd || process.cwd();
|
|
1799
|
+
|
|
1800
|
+
assert(getFeedbackPaths().FEEDBACK_DIR === tmpDir, 'explicit feedback dir wins over npm INIT_CWD');
|
|
1797
1801
|
|
|
1798
1802
|
appendJSONL(localFeedbackLog, { signal: 'positive', tags: ['testing'], skill: 'verify' });
|
|
1799
1803
|
appendJSONL(localFeedbackLog, { signal: 'negative', tags: ['testing'], skill: 'verify' });
|
|
@@ -1845,6 +1849,8 @@ function runTests() {
|
|
|
1845
1849
|
|
|
1846
1850
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1847
1851
|
delete process.env.THUMBGATE_FEEDBACK_DIR;
|
|
1852
|
+
if (savedInitCwd === undefined) delete process.env.INIT_CWD;
|
|
1853
|
+
else process.env.INIT_CWD = savedInitCwd;
|
|
1848
1854
|
console.log(`\nResults: ${passed} passed, ${failed} failed\n`);
|
|
1849
1855
|
process.exit(failed > 0 ? 1 : 0);
|
|
1850
1856
|
}
|
|
@@ -43,14 +43,139 @@ function dirExists(dirPath) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function getHomeDir(options = {}) {
|
|
47
|
+
const env = options.env || process.env;
|
|
48
|
+
return options.home || env.HOME || env.USERPROFILE || HOME;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeDir(dirPath) {
|
|
52
|
+
if (!dirPath) return null;
|
|
53
|
+
try {
|
|
54
|
+
return path.resolve(String(dirPath));
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isWithinDir(candidate, parent) {
|
|
61
|
+
const normalizedCandidate = normalizeDir(candidate);
|
|
62
|
+
const normalizedParent = normalizeDir(parent);
|
|
63
|
+
if (!normalizedCandidate || !normalizedParent) return false;
|
|
64
|
+
const relative = path.relative(normalizedParent, normalizedCandidate);
|
|
65
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getRuntimeDir(options = {}) {
|
|
69
|
+
return path.join(getHomeDir(options), '.thumbgate', 'runtime');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getActiveProjectStatePath(options = {}) {
|
|
73
|
+
return path.join(getRuntimeDir(options), 'active-project.json');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isTransientProjectDir(dirPath, options = {}) {
|
|
77
|
+
const normalizedDir = normalizeDir(dirPath);
|
|
78
|
+
if (!normalizedDir) return true;
|
|
79
|
+
if (!dirExists(normalizedDir)) return true;
|
|
80
|
+
|
|
81
|
+
const runtimeDir = getRuntimeDir(options);
|
|
82
|
+
if (isWithinDir(normalizedDir, runtimeDir)) return true;
|
|
83
|
+
|
|
84
|
+
return normalizedDir.includes(`${path.sep}.npm${path.sep}_npx${path.sep}`)
|
|
85
|
+
|| /thumbgate-published-cli-/i.test(normalizedDir);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readActiveProjectState(options = {}) {
|
|
89
|
+
const statePath = getActiveProjectStatePath(options);
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
92
|
+
if (!parsed || !parsed.projectDir) return null;
|
|
93
|
+
if (isTransientProjectDir(parsed.projectDir, options)) return null;
|
|
94
|
+
return {
|
|
95
|
+
...parsed,
|
|
96
|
+
projectDir: normalizeDir(parsed.projectDir),
|
|
97
|
+
};
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function writeActiveProjectState(projectDir, options = {}) {
|
|
104
|
+
const normalizedDir = normalizeDir(projectDir);
|
|
105
|
+
if (isTransientProjectDir(normalizedDir, options)) return null;
|
|
106
|
+
|
|
107
|
+
const payload = {
|
|
108
|
+
projectDir: normalizedDir,
|
|
109
|
+
projectName: path.basename(normalizedDir) || 'default',
|
|
110
|
+
updatedAt: new Date().toISOString(),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const statePath = getActiveProjectStatePath(options);
|
|
114
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
115
|
+
fs.writeFileSync(statePath, JSON.stringify(payload, null, 2));
|
|
116
|
+
return payload;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolveProjectDir(options = {}) {
|
|
120
|
+
const env = options.env || process.env;
|
|
121
|
+
const stored = options.includeStored === false ? null : readActiveProjectState(options);
|
|
122
|
+
const cwdCandidates = uniquePaths([
|
|
123
|
+
options.cwd,
|
|
124
|
+
env.PWD,
|
|
125
|
+
process.cwd(),
|
|
126
|
+
]);
|
|
127
|
+
const isTransientExecution = cwdCandidates.length > 0
|
|
128
|
+
&& cwdCandidates.every((candidate) => isTransientProjectDir(candidate, options));
|
|
129
|
+
const candidates = uniquePaths([
|
|
130
|
+
options.projectDir,
|
|
131
|
+
env.THUMBGATE_PROJECT_DIR,
|
|
132
|
+
env.CLAUDE_PROJECT_DIR,
|
|
133
|
+
isTransientExecution && stored && stored.projectDir,
|
|
134
|
+
env.INIT_CWD,
|
|
135
|
+
...cwdCandidates,
|
|
136
|
+
!isTransientExecution && stored && stored.projectDir,
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
for (const candidate of candidates) {
|
|
140
|
+
if (!candidate) continue;
|
|
141
|
+
if (!isTransientProjectDir(candidate, options)) {
|
|
142
|
+
return normalizeDir(candidate);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return normalizeDir(options.cwd || env.PWD || PROJECT_ROOT) || PROJECT_ROOT;
|
|
147
|
+
}
|
|
148
|
+
|
|
46
149
|
function getProjectName(cwd = process.cwd()) {
|
|
47
150
|
return path.basename(cwd || PROJECT_ROOT) || 'default';
|
|
48
151
|
}
|
|
49
152
|
|
|
153
|
+
function hasDirectProjectScope(options = {}) {
|
|
154
|
+
const env = options.env || process.env;
|
|
155
|
+
return Boolean(
|
|
156
|
+
options.explicitProjectDir
|
|
157
|
+
|| env.THUMBGATE_PROJECT_DIR
|
|
158
|
+
|| env.CLAUDE_PROJECT_DIR
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function hasExplicitProjectScope(options = {}) {
|
|
163
|
+
return Boolean(hasDirectProjectScope(options) || readActiveProjectState(options));
|
|
164
|
+
}
|
|
165
|
+
|
|
50
166
|
function getExplicitFeedbackDir(options = {}) {
|
|
51
167
|
const env = options.env || process.env;
|
|
52
168
|
if (options.feedbackDir) return options.feedbackDir;
|
|
53
|
-
if (
|
|
169
|
+
if (options.skipExplicitFeedbackDir) return null;
|
|
170
|
+
// A caller-provided feedback root should stay authoritative over stored
|
|
171
|
+
// active-project state so isolated CLI/test commands do not drift into a
|
|
172
|
+
// different project. Only direct project overrides suppress it.
|
|
173
|
+
if (env.THUMBGATE_FEEDBACK_DIR && !hasDirectProjectScope(options)) {
|
|
174
|
+
return env.THUMBGATE_FEEDBACK_DIR;
|
|
175
|
+
}
|
|
176
|
+
if (hasDirectProjectScope(options)) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
54
179
|
if (env.RAILWAY_VOLUME_MOUNT_PATH) {
|
|
55
180
|
return path.join(env.RAILWAY_VOLUME_MOUNT_PATH, 'feedback');
|
|
56
181
|
}
|
|
@@ -58,30 +183,29 @@ function getExplicitFeedbackDir(options = {}) {
|
|
|
58
183
|
}
|
|
59
184
|
|
|
60
185
|
function getThumbgateFeedbackDir(options = {}) {
|
|
61
|
-
const
|
|
62
|
-
return path.join(
|
|
186
|
+
const projectDir = resolveProjectDir(options);
|
|
187
|
+
return path.join(projectDir, '.thumbgate');
|
|
63
188
|
}
|
|
64
189
|
|
|
65
190
|
function getFallbackFeedbackDir(options = {}) {
|
|
66
191
|
const env = options.env || process.env;
|
|
67
192
|
if (env._TEST_THUMBGATE_FALLBACK_FEEDBACK_DIR) return env._TEST_THUMBGATE_FALLBACK_FEEDBACK_DIR;
|
|
68
193
|
if (env.THUMBGATE_FALLBACK_FEEDBACK_DIR) return env.THUMBGATE_FALLBACK_FEEDBACK_DIR;
|
|
69
|
-
const
|
|
70
|
-
return path.join(
|
|
194
|
+
const projectDir = resolveProjectDir(options);
|
|
195
|
+
return path.join(projectDir, '.thumbgate-compat');
|
|
71
196
|
}
|
|
72
197
|
|
|
73
198
|
function getLegacyFeedbackDir(options = {}) {
|
|
74
199
|
const env = options.env || process.env;
|
|
75
200
|
if (env._TEST_LEGACY_FEEDBACK_DIR) return env._TEST_LEGACY_FEEDBACK_DIR;
|
|
76
201
|
if (env.THUMBGATE_LEGACY_FEEDBACK_DIR) return env.THUMBGATE_LEGACY_FEEDBACK_DIR;
|
|
77
|
-
const
|
|
78
|
-
return path.join(
|
|
202
|
+
const projectDir = resolveProjectDir(options);
|
|
203
|
+
return path.join(projectDir, '.claude', 'memory', 'feedback');
|
|
79
204
|
}
|
|
80
205
|
|
|
81
206
|
function getGlobalFeedbackDir(options = {}) {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
return path.join(home, '.thumbgate', 'projects', getProjectName(cwd));
|
|
207
|
+
const projectDir = resolveProjectDir(options);
|
|
208
|
+
return path.join(getHomeDir(options), '.thumbgate', 'projects', getProjectName(projectDir));
|
|
85
209
|
}
|
|
86
210
|
|
|
87
211
|
function resolveFeedbackDir(options = {}) {
|
|
@@ -133,13 +257,21 @@ module.exports = {
|
|
|
133
257
|
PROJECT_ROOT,
|
|
134
258
|
HOME,
|
|
135
259
|
buildFeedbackPathsFromDir,
|
|
260
|
+
getActiveProjectStatePath,
|
|
136
261
|
getFeedbackPaths,
|
|
137
262
|
getGlobalFeedbackDir,
|
|
263
|
+
getHomeDir,
|
|
138
264
|
getLegacyFeedbackDir,
|
|
139
265
|
getFallbackFeedbackDir,
|
|
266
|
+
getRuntimeDir,
|
|
140
267
|
getThumbgateFeedbackDir,
|
|
268
|
+
hasDirectProjectScope,
|
|
269
|
+
hasExplicitProjectScope,
|
|
270
|
+
readActiveProjectState,
|
|
141
271
|
listFallbackFeedbackDirs,
|
|
142
272
|
listFeedbackArtifactPaths,
|
|
273
|
+
resolveProjectDir,
|
|
143
274
|
resolveFallbackArtifactPath,
|
|
144
275
|
resolveFeedbackDir,
|
|
276
|
+
writeActiveProjectState,
|
|
145
277
|
};
|
|
@@ -29,6 +29,19 @@ const CONSOLIDATED_ARTIFACTS = [
|
|
|
29
29
|
'workflow-sprint-leads.jsonl',
|
|
30
30
|
];
|
|
31
31
|
|
|
32
|
+
function getScopedProjectOptions(options = {}) {
|
|
33
|
+
if (options.feedbackDir) return options;
|
|
34
|
+
|
|
35
|
+
const projectDir = options.projectDir || options.cwd;
|
|
36
|
+
if (!projectDir) return options;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
...options,
|
|
40
|
+
projectDir,
|
|
41
|
+
explicitProjectDir: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
function ensureParentDir(filePath) {
|
|
33
46
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
34
47
|
}
|
|
@@ -194,11 +207,11 @@ function consolidateArtifact(fileName, options = {}) {
|
|
|
194
207
|
}
|
|
195
208
|
|
|
196
209
|
function consolidateFeedbackRoot(options = {}) {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|| getThumbgateFeedbackDir(
|
|
210
|
+
const scopedOptions = getScopedProjectOptions(options);
|
|
211
|
+
const feedbackDir = scopedOptions.feedbackDir
|
|
212
|
+
|| getThumbgateFeedbackDir(scopedOptions);
|
|
200
213
|
const artifacts = CONSOLIDATED_ARTIFACTS.map((fileName) => consolidateArtifact(fileName, {
|
|
201
|
-
...
|
|
214
|
+
...scopedOptions,
|
|
202
215
|
feedbackDir,
|
|
203
216
|
}));
|
|
204
217
|
const sourceRoots = Array.from(new Set(
|
|
@@ -229,6 +242,7 @@ module.exports = {
|
|
|
229
242
|
consolidateArtifact,
|
|
230
243
|
consolidateFeedbackRoot,
|
|
231
244
|
dedupeJsonlRows,
|
|
245
|
+
getScopedProjectOptions,
|
|
232
246
|
mergeCheckoutSessionsPayloads,
|
|
233
247
|
mergeKeyStorePayloads,
|
|
234
248
|
};
|
package/scripts/gates-engine.js
CHANGED
|
@@ -11,6 +11,9 @@ const {
|
|
|
11
11
|
DEFAULT_BASE_BRANCH,
|
|
12
12
|
evaluateOperationalIntegrity,
|
|
13
13
|
} = require('./operational-integrity');
|
|
14
|
+
const {
|
|
15
|
+
evaluateWorkflowSentinel,
|
|
16
|
+
} = require('./workflow-sentinel');
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* Computes the SHA-256 hash of an executable binary to prevent path-based bypasses.
|
|
@@ -764,6 +767,16 @@ function buildReasoning(gate, toolName, toolInput, extras = {}) {
|
|
|
764
767
|
steps.push(`Memory guard matched (${extras.memoryGuard.source}): ${extras.memoryGuard.reason}`);
|
|
765
768
|
}
|
|
766
769
|
|
|
770
|
+
if (extras.workflowSentinel) {
|
|
771
|
+
steps.push(`Workflow sentinel risk: ${extras.workflowSentinel.band} (${extras.workflowSentinel.riskScore})`);
|
|
772
|
+
if (extras.workflowSentinel.blastRadius && extras.workflowSentinel.blastRadius.summary) {
|
|
773
|
+
steps.push(`Workflow sentinel blast radius: ${extras.workflowSentinel.blastRadius.summary}`);
|
|
774
|
+
}
|
|
775
|
+
for (const remediation of (extras.workflowSentinel.remediations || []).slice(0, 3)) {
|
|
776
|
+
steps.push(`Workflow sentinel remediation: ${remediation.title} — ${remediation.action}`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
767
780
|
// 5. Unless condition status
|
|
768
781
|
if (gate.unless) {
|
|
769
782
|
steps.push(`Bypassable via satisfy_gate("${gate.unless}") — not currently satisfied`);
|
|
@@ -973,6 +986,39 @@ function evaluateMemoryGuard(toolName, toolInput = {}) {
|
|
|
973
986
|
};
|
|
974
987
|
}
|
|
975
988
|
|
|
989
|
+
function buildSentinelGateResult(report) {
|
|
990
|
+
return {
|
|
991
|
+
decision: report.decision,
|
|
992
|
+
gate: 'workflow-sentinel',
|
|
993
|
+
message: `${report.summary} ${report.blastRadius.summary}`,
|
|
994
|
+
severity: report.decision === 'deny' ? 'critical' : 'high',
|
|
995
|
+
reasoning: Array.isArray(report.reasoning) ? report.reasoning.slice() : [],
|
|
996
|
+
sentinel: report,
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function enrichResultWithSentinel(result, report) {
|
|
1001
|
+
if (!result || !report || report.decision === 'allow') {
|
|
1002
|
+
return result;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const next = {
|
|
1006
|
+
...result,
|
|
1007
|
+
reasoning: Array.isArray(result.reasoning) ? result.reasoning.slice() : [],
|
|
1008
|
+
sentinel: report,
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
if (report.blastRadius && report.blastRadius.summary) {
|
|
1012
|
+
next.message = `${result.message} Workflow sentinel: ${report.blastRadius.summary}`;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
next.reasoning = next.reasoning.concat(
|
|
1016
|
+
Array.isArray(report.reasoning) ? report.reasoning : []
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
return next;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
976
1022
|
async function checkMetricCondition(metricCondition) {
|
|
977
1023
|
if (!metricCondition) return true;
|
|
978
1024
|
const { getBusinessMetrics } = require('./semantic-layer');
|
|
@@ -1058,20 +1104,40 @@ async function evaluateGatesAsync(toolName, toolInput, configPath) {
|
|
|
1058
1104
|
}
|
|
1059
1105
|
}
|
|
1060
1106
|
|
|
1107
|
+
const sentinelReport = evaluateWorkflowSentinel(toolName, toolInput, {
|
|
1108
|
+
governanceState: loadGovernanceState(),
|
|
1109
|
+
});
|
|
1061
1110
|
const memoryGuard = evaluateMemoryGuard(toolName, toolInput);
|
|
1062
1111
|
if (memoryGuard) {
|
|
1063
|
-
|
|
1112
|
+
const enrichedMemoryGuard = enrichResultWithSentinel(memoryGuard, sentinelReport);
|
|
1113
|
+
recordStat(enrichedMemoryGuard.gate, 'block');
|
|
1064
1114
|
const auditRecord = recordAuditEvent({
|
|
1065
1115
|
toolName,
|
|
1066
1116
|
toolInput,
|
|
1067
1117
|
decision: 'deny',
|
|
1068
|
-
gateId:
|
|
1069
|
-
message:
|
|
1070
|
-
severity:
|
|
1118
|
+
gateId: enrichedMemoryGuard.gate,
|
|
1119
|
+
message: enrichedMemoryGuard.message,
|
|
1120
|
+
severity: enrichedMemoryGuard.severity,
|
|
1071
1121
|
source: 'gates-engine',
|
|
1072
1122
|
});
|
|
1073
1123
|
auditToFeedback(auditRecord);
|
|
1074
|
-
return
|
|
1124
|
+
return enrichedMemoryGuard;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (sentinelReport && sentinelReport.decision !== 'allow') {
|
|
1128
|
+
const sentinelResult = buildSentinelGateResult(sentinelReport);
|
|
1129
|
+
recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn');
|
|
1130
|
+
const auditRecord = recordAuditEvent({
|
|
1131
|
+
toolName,
|
|
1132
|
+
toolInput,
|
|
1133
|
+
decision: sentinelResult.decision,
|
|
1134
|
+
gateId: sentinelResult.gate,
|
|
1135
|
+
message: sentinelResult.message,
|
|
1136
|
+
severity: sentinelResult.severity,
|
|
1137
|
+
source: 'workflow-sentinel',
|
|
1138
|
+
});
|
|
1139
|
+
auditToFeedback(auditRecord);
|
|
1140
|
+
return sentinelResult;
|
|
1075
1141
|
}
|
|
1076
1142
|
|
|
1077
1143
|
// Audit trail: record allow (no gate matched)
|
|
@@ -1124,20 +1190,40 @@ function evaluateGates(toolName, toolInput, configPath) {
|
|
|
1124
1190
|
}
|
|
1125
1191
|
}
|
|
1126
1192
|
|
|
1193
|
+
const sentinelReport = evaluateWorkflowSentinel(toolName, toolInput, {
|
|
1194
|
+
governanceState: loadGovernanceState(),
|
|
1195
|
+
});
|
|
1127
1196
|
const memoryGuard = evaluateMemoryGuard(toolName, toolInput);
|
|
1128
1197
|
if (memoryGuard) {
|
|
1129
|
-
|
|
1198
|
+
const enrichedMemoryGuard = enrichResultWithSentinel(memoryGuard, sentinelReport);
|
|
1199
|
+
recordStat(enrichedMemoryGuard.gate, 'block');
|
|
1130
1200
|
const auditRecord = recordAuditEvent({
|
|
1131
1201
|
toolName,
|
|
1132
1202
|
toolInput,
|
|
1133
1203
|
decision: 'deny',
|
|
1134
|
-
gateId:
|
|
1135
|
-
message:
|
|
1136
|
-
severity:
|
|
1204
|
+
gateId: enrichedMemoryGuard.gate,
|
|
1205
|
+
message: enrichedMemoryGuard.message,
|
|
1206
|
+
severity: enrichedMemoryGuard.severity,
|
|
1137
1207
|
source: 'gates-engine',
|
|
1138
1208
|
});
|
|
1139
1209
|
auditToFeedback(auditRecord);
|
|
1140
|
-
return
|
|
1210
|
+
return enrichedMemoryGuard;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
if (sentinelReport && sentinelReport.decision !== 'allow') {
|
|
1214
|
+
const sentinelResult = buildSentinelGateResult(sentinelReport);
|
|
1215
|
+
recordStat(sentinelResult.gate, sentinelResult.decision === 'deny' ? 'block' : 'warn');
|
|
1216
|
+
const auditRecord = recordAuditEvent({
|
|
1217
|
+
toolName,
|
|
1218
|
+
toolInput,
|
|
1219
|
+
decision: sentinelResult.decision,
|
|
1220
|
+
gateId: sentinelResult.gate,
|
|
1221
|
+
message: sentinelResult.message,
|
|
1222
|
+
severity: sentinelResult.severity,
|
|
1223
|
+
source: 'workflow-sentinel',
|
|
1224
|
+
});
|
|
1225
|
+
auditToFeedback(auditRecord);
|
|
1226
|
+
return sentinelResult;
|
|
1141
1227
|
}
|
|
1142
1228
|
|
|
1143
1229
|
// Audit trail: record allow
|
|
@@ -10,7 +10,7 @@ PROMPT_GUARD="$SCRIPT_DIR/prompt-guard.js"
|
|
|
10
10
|
ACTIVE_CWD="${CLAUDE_PROJECT_DIR:-${PWD:-$(pwd)}}"
|
|
11
11
|
FEEDBACK_DIR="$(node -e "const path = require('path'); const { resolveFeedbackDir } = require(path.join(process.argv[1], 'feedback-paths.js')); process.stdout.write(resolveFeedbackDir({ cwd: process.argv[2] || process.cwd(), feedbackDir: process.env.THUMBGATE_FEEDBACK_DIR || undefined }));" "$SCRIPT_DIR" "$ACTIVE_CWD" 2>/dev/null)"
|
|
12
12
|
if [ -z "$FEEDBACK_DIR" ]; then
|
|
13
|
-
FEEDBACK_DIR="${THUMBGATE_FEEDBACK_DIR:-$ACTIVE_CWD/.
|
|
13
|
+
FEEDBACK_DIR="${THUMBGATE_FEEDBACK_DIR:-$ACTIVE_CWD/.thumbgate}"
|
|
14
14
|
fi
|
|
15
15
|
FEEDBACK_LOG="$FEEDBACK_DIR/feedback-log.jsonl"
|
|
16
16
|
MEMORY_LOG="$FEEDBACK_DIR/memory-log.jsonl"
|