thumbgate 0.9.14 → 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.
Files changed (63) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.well-known/mcp/server-card.json +1 -1
  4. package/README.md +1 -0
  5. package/adapters/README.md +1 -1
  6. package/adapters/chatgpt/openapi.yaml +105 -0
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/codex/config.toml +2 -2
  9. package/adapters/forge/forge.yaml +28 -0
  10. package/adapters/mcp/server-stdio.js +32 -1
  11. package/adapters/opencode/opencode.json +1 -1
  12. package/bin/cli.js +18 -3
  13. package/config/mcp-allowlists.json +10 -0
  14. package/openapi/openapi.yaml +105 -0
  15. package/package.json +4 -4
  16. package/plugins/amp-skill/INSTALL.md +3 -4
  17. package/plugins/amp-skill/SKILL.md +0 -1
  18. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  19. package/plugins/claude-codex-bridge/.mcp.json +1 -1
  20. package/plugins/claude-skill/INSTALL.md +1 -2
  21. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  22. package/plugins/codex-profile/.mcp.json +1 -1
  23. package/plugins/codex-profile/INSTALL.md +1 -1
  24. package/plugins/codex-profile/README.md +1 -1
  25. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  26. package/plugins/opencode-profile/INSTALL.md +1 -1
  27. package/public/blog.html +1 -0
  28. package/public/dashboard.html +1 -1
  29. package/public/guide.html +1 -1
  30. package/public/index.html +3 -3
  31. package/public/learn/agent-harness-pattern.html +1 -1
  32. package/public/learn/ai-agent-persistent-memory.html +1 -1
  33. package/public/learn/mcp-pre-action-gates-explained.html +1 -1
  34. package/public/learn/stop-ai-agent-force-push.html +1 -1
  35. package/public/learn/vibe-coding-safety-net.html +1 -1
  36. package/public/learn.html +1 -1
  37. package/public/lessons.html +1 -1
  38. package/public/pro.html +1 -1
  39. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  40. package/scripts/agent-security-hardening.js +4 -4
  41. package/scripts/async-job-runner.js +84 -24
  42. package/scripts/auto-wire-hooks.js +59 -1
  43. package/scripts/context-manager.js +330 -0
  44. package/scripts/dashboard.js +1 -1
  45. package/scripts/distribution-surfaces.js +12 -0
  46. package/scripts/ensure-repo-bootstrap.js +15 -14
  47. package/scripts/gates-engine.js +96 -10
  48. package/scripts/hook-auto-capture.sh +1 -1
  49. package/scripts/hosted-job-launcher.js +260 -0
  50. package/scripts/managed-dpo-export.js +91 -0
  51. package/scripts/obsidian-export.js +0 -1
  52. package/scripts/operational-integrity.js +50 -7
  53. package/scripts/prove-lancedb.js +62 -4
  54. package/scripts/publish-decision.js +16 -0
  55. package/scripts/self-healing-check.js +6 -1
  56. package/scripts/social-analytics/load-env.js +33 -2
  57. package/scripts/social-analytics/store.js +200 -2
  58. package/scripts/sync-version.js +18 -11
  59. package/scripts/tool-registry.js +37 -0
  60. package/scripts/train_from_feedback.py +0 -4
  61. package/scripts/workflow-sentinel.js +793 -0
  62. package/src/api/server.js +205 -27
  63. /package/scripts/{rlhf_session_start.sh → thumbgate_session_start.sh} +0 -0
@@ -0,0 +1,330 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Context Manager — Unified Context-Augmented Generation (CAG) Orchestrator
6
+ *
7
+ * Single entry point that assembles a normalized context object from:
8
+ * - Session state (primer / handoff)
9
+ * - User profile (role, preferences, agent type)
10
+ * - Relevant lessons (per-action retrieval)
11
+ * - Prevention rules / pre-tool guards
12
+ * - Context pack (ContextFS retrieval)
13
+ * - Code-graph impact (optional, for coding tasks)
14
+ *
15
+ * Implements tiered graceful degradation:
16
+ * Tier 1 (full) — session + lessons + rules + context pack + code-graph
17
+ * Tier 2 (warm) — lessons + rules + context pack (no session)
18
+ * Tier 3 (cold) — prevention rules + global defaults only
19
+ *
20
+ * Role-aware filtering shapes output by agent type and license tier.
21
+ */
22
+
23
+ const {
24
+ ensureContextFs,
25
+ constructContextPack,
26
+ readSessionHandoff,
27
+ recordProvenance,
28
+ } = require('./contextfs');
29
+ const { retrieveRelevantLessons } = require('./lesson-retrieval');
30
+ const { evaluatePretool } = require('./hybrid-feedback-context');
31
+ const { loadProfile } = require('./user-profile');
32
+ const {
33
+ analyzeCodeGraphImpact,
34
+ formatCodeGraphRecallSection,
35
+ } = require('./codegraph-context');
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Agent capability profiles — shapes what context each agent type receives
39
+ // ---------------------------------------------------------------------------
40
+
41
+ const AGENT_PROFILES = {
42
+ claude: {
43
+ maxLessons: 8,
44
+ includeCodeGraph: true,
45
+ includeStructuredRules: true,
46
+ contextBudget: 10000,
47
+ },
48
+ cursor: {
49
+ maxLessons: 5,
50
+ includeCodeGraph: true,
51
+ includeStructuredRules: true,
52
+ contextBudget: 6000,
53
+ },
54
+ forgecode: {
55
+ maxLessons: 5,
56
+ includeCodeGraph: false,
57
+ includeStructuredRules: true,
58
+ contextBudget: 6000,
59
+ },
60
+ codex: {
61
+ maxLessons: 6,
62
+ includeCodeGraph: true,
63
+ includeStructuredRules: true,
64
+ contextBudget: 8000,
65
+ },
66
+ default: {
67
+ maxLessons: 5,
68
+ includeCodeGraph: false,
69
+ includeStructuredRules: true,
70
+ contextBudget: 6000,
71
+ },
72
+ };
73
+
74
+ function getAgentProfile(agentType) {
75
+ const key = String(agentType || 'default').toLowerCase();
76
+ return AGENT_PROFILES[key] || AGENT_PROFILES.default;
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Tier assembly helpers
81
+ // ---------------------------------------------------------------------------
82
+
83
+ function assembleSession() {
84
+ try {
85
+ return readSessionHandoff();
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ function assembleLessons(query, agentProfile, options = {}) {
92
+ try {
93
+ return retrieveRelevantLessons(
94
+ options.toolName || '',
95
+ query,
96
+ { maxResults: agentProfile.maxLessons, feedbackDir: options.feedbackDir },
97
+ );
98
+ } catch {
99
+ return [];
100
+ }
101
+ }
102
+
103
+ function assembleGuards(toolName, toolInput) {
104
+ try {
105
+ return evaluatePretool(toolName || '', toolInput || {});
106
+ } catch {
107
+ return { mode: 'allow', reason: 'guard-unavailable' };
108
+ }
109
+ }
110
+
111
+ function assembleContextPack(query, agentProfile) {
112
+ try {
113
+ ensureContextFs();
114
+ return constructContextPack({
115
+ query,
116
+ maxItems: Math.min(8, Math.ceil(agentProfile.contextBudget / 1000)),
117
+ maxChars: agentProfile.contextBudget,
118
+ });
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ function assembleCodeGraph(query, repoPath, agentProfile) {
125
+ if (!agentProfile.includeCodeGraph) return null;
126
+ try {
127
+ const impact = analyzeCodeGraphImpact({
128
+ intentId: null,
129
+ context: query,
130
+ repoPath,
131
+ });
132
+ return formatCodeGraphRecallSection(impact) || null;
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+
138
+ function assembleUserProfile() {
139
+ try {
140
+ const profile = loadProfile();
141
+ if (!profile || !profile.entries || profile.entries.length === 0) return null;
142
+ return {
143
+ entries: profile.entries,
144
+ charCount: profile.charCount || 0,
145
+ };
146
+ } catch {
147
+ return null;
148
+ }
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Tier classification
153
+ // ---------------------------------------------------------------------------
154
+
155
+ function classifyTier(components) {
156
+ const hasSession = !!components.session;
157
+ const hasLessons = components.lessons && components.lessons.length > 0;
158
+ const hasPack = !!components.contextPack;
159
+
160
+ if (hasSession && (hasLessons || hasPack)) return 'full';
161
+ if (hasLessons || hasPack) return 'warm';
162
+ return 'cold';
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Main orchestrator
167
+ // ---------------------------------------------------------------------------
168
+
169
+ /**
170
+ * Assemble a unified context object for a given query.
171
+ *
172
+ * @param {Object} params
173
+ * @param {string} params.query - Task description / context query
174
+ * @param {string} [params.toolName] - Current tool being invoked (for lesson retrieval)
175
+ * @param {Object} [params.toolInput] - Current tool input (for guard evaluation)
176
+ * @param {string} [params.agentType] - Agent type: claude, cursor, forgecode, codex
177
+ * @param {string} [params.repoPath] - Repo path for code-graph analysis
178
+ * @param {string} [params.feedbackDir] - Override feedback directory
179
+ * @returns {Object} Normalized context object
180
+ */
181
+ function assembleUnifiedContext(params = {}) {
182
+ const {
183
+ query = '',
184
+ toolName,
185
+ toolInput,
186
+ agentType,
187
+ repoPath,
188
+ feedbackDir,
189
+ } = params;
190
+
191
+ const agentProfile = getAgentProfile(agentType);
192
+
193
+ // Assemble all components — each is fault-tolerant
194
+ const session = assembleSession();
195
+ const userProfile = assembleUserProfile();
196
+ const lessons = assembleLessons(query, agentProfile, { toolName, feedbackDir });
197
+ const guards = assembleGuards(toolName, toolInput);
198
+ const contextPack = assembleContextPack(query, agentProfile);
199
+ const codeGraph = assembleCodeGraph(query, repoPath, agentProfile);
200
+
201
+ const components = { session, userProfile, lessons, guards, contextPack, codeGraph };
202
+ const tier = classifyTier(components);
203
+
204
+ const result = {
205
+ tier,
206
+ agentType: agentType || 'default',
207
+ agentProfile: {
208
+ maxLessons: agentProfile.maxLessons,
209
+ contextBudget: agentProfile.contextBudget,
210
+ includeCodeGraph: agentProfile.includeCodeGraph,
211
+ },
212
+ session: session || null,
213
+ userProfile: userProfile || null,
214
+ lessons,
215
+ guards,
216
+ contextPack: contextPack ? {
217
+ packId: contextPack.packId,
218
+ itemCount: Array.isArray(contextPack.items) ? contextPack.items.length : 0,
219
+ items: (contextPack.items || []).slice(0, 5).map((item) => ({
220
+ id: item.id,
221
+ namespace: item.namespace,
222
+ title: item.title,
223
+ tags: item.tags || [],
224
+ score: item.score,
225
+ })),
226
+ visibility: contextPack.visibility || null,
227
+ cached: !!(contextPack.cache && contextPack.cache.hit),
228
+ } : null,
229
+ codeGraph: codeGraph || null,
230
+ assembledAt: new Date().toISOString(),
231
+ };
232
+
233
+ // Record provenance for audit trail
234
+ try {
235
+ recordProvenance({
236
+ type: 'unified_context_assembled',
237
+ tier,
238
+ agentType: result.agentType,
239
+ lessonCount: lessons.length,
240
+ guardDecision: guards.mode || 'allow',
241
+ hasSession: !!session,
242
+ hasUserProfile: !!userProfile,
243
+ hasCodeGraph: !!codeGraph,
244
+ packId: result.contextPack ? result.contextPack.packId : null,
245
+ });
246
+ } catch {
247
+ // Provenance write failure must never break context assembly
248
+ }
249
+
250
+ return result;
251
+ }
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // Formatting for MCP tool response
255
+ // ---------------------------------------------------------------------------
256
+
257
+ function formatUnifiedContext(ctx) {
258
+ const lines = [];
259
+
260
+ lines.push(`## Unified Context (Tier: ${ctx.tier})`);
261
+ lines.push(`Agent: ${ctx.agentType} | Assembled: ${ctx.assembledAt}`);
262
+ lines.push('');
263
+
264
+ // Session
265
+ if (ctx.session) {
266
+ lines.push('### Session');
267
+ if (ctx.session.lastTask) lines.push(`Last task: ${ctx.session.lastTask}`);
268
+ if (ctx.session.nextStep) lines.push(`Next step: ${ctx.session.nextStep}`);
269
+ if (ctx.session.blockers && ctx.session.blockers.length > 0) {
270
+ lines.push(`Blockers: ${ctx.session.blockers.join(', ')}`);
271
+ }
272
+ lines.push('');
273
+ }
274
+
275
+ // User profile
276
+ if (ctx.userProfile) {
277
+ lines.push('### User Profile');
278
+ ctx.userProfile.entries.slice(0, 3).forEach((entry) => {
279
+ lines.push(`- ${entry.slice(0, 120)}`);
280
+ });
281
+ lines.push('');
282
+ }
283
+
284
+ // Guards
285
+ if (ctx.guards && ctx.guards.mode !== 'allow') {
286
+ lines.push(`### Guard: ${ctx.guards.mode.toUpperCase()}`);
287
+ lines.push(ctx.guards.reason || 'No reason provided');
288
+ lines.push('');
289
+ }
290
+
291
+ // Lessons
292
+ if (ctx.lessons && ctx.lessons.length > 0) {
293
+ lines.push(`### Lessons (${ctx.lessons.length})`);
294
+ ctx.lessons.forEach((lesson) => {
295
+ const signal = lesson.signal === 'negative' ? '[-]' : '[+]';
296
+ lines.push(`${signal} ${lesson.title || lesson.id} (score: ${lesson.relevanceScore})`);
297
+ if (lesson.rule) {
298
+ lines.push(` Rule: IF ${lesson.rule.condition || '?'} THEN ${lesson.rule.action || '?'}`);
299
+ }
300
+ });
301
+ lines.push('');
302
+ }
303
+
304
+ // Context pack
305
+ if (ctx.contextPack) {
306
+ lines.push(`### Context Pack (${ctx.contextPack.itemCount} items)`);
307
+ ctx.contextPack.items.forEach((item) => {
308
+ lines.push(`- [${item.namespace}] ${item.title} (score: ${item.score})`);
309
+ });
310
+ if (ctx.contextPack.cached) lines.push('(cached)');
311
+ lines.push('');
312
+ }
313
+
314
+ // Code graph
315
+ if (ctx.codeGraph) {
316
+ lines.push('### Code Graph Impact');
317
+ lines.push(ctx.codeGraph);
318
+ lines.push('');
319
+ }
320
+
321
+ return lines.join('\n');
322
+ }
323
+
324
+ module.exports = {
325
+ assembleUnifiedContext,
326
+ formatUnifiedContext,
327
+ getAgentProfile,
328
+ AGENT_PROFILES,
329
+ classifyTier,
330
+ };
@@ -523,7 +523,7 @@ function computeInstrumentationReadiness(analytics, billing) {
523
523
  const cli = telemetry.cli || {};
524
524
 
525
525
  return {
526
- plausibleConfigured: /\/js\/analytics\.js/.test(landingPage),
526
+ plausibleConfigured: /plausible\.io\/js\/script\.js|\/js\/analytics\.js/.test(landingPage),
527
527
  ga4Configured: Boolean(runtimeConfig.gaMeasurementId),
528
528
  googleSearchConsoleConfigured: Boolean(runtimeConfig.googleSiteVerification),
529
529
  softwareApplicationSchemaPresent: /"@type": "SoftwareApplication"/.test(landingPage),
@@ -6,6 +6,7 @@ const path = require('node:path');
6
6
  const ROOT = path.join(__dirname, '..');
7
7
  const PRODUCTHUNT_URL = 'https://www.producthunt.com/products/thumbgate';
8
8
  const CLAUDE_PLUGIN_LATEST_ASSET_NAME = 'thumbgate-claude-desktop.mcpb';
9
+ const CLAUDE_PLUGIN_NEXT_ASSET_NAME = 'thumbgate-claude-desktop-next.mcpb';
9
10
 
10
11
  function readJson(root, relativePath) {
11
12
  return JSON.parse(fs.readFileSync(path.join(root, relativePath), 'utf8'));
@@ -24,6 +25,14 @@ function getClaudePluginVersionedAssetName(version = getPackageVersion(ROOT)) {
24
25
  return `thumbgate-claude-desktop-v${normalized}.mcpb`;
25
26
  }
26
27
 
28
+ function isPrereleaseVersion(version = getPackageVersion(ROOT)) {
29
+ return /^\d+\.\d+\.\d+-[0-9A-Za-z.-]+$/.test(String(version || '').trim());
30
+ }
31
+
32
+ function getClaudePluginChannelAssetName(version = getPackageVersion(ROOT)) {
33
+ return isPrereleaseVersion(version) ? CLAUDE_PLUGIN_NEXT_ASSET_NAME : CLAUDE_PLUGIN_LATEST_ASSET_NAME;
34
+ }
35
+
27
36
  function getClaudePluginLatestDownloadUrl(root = ROOT) {
28
37
  return `${getRepositoryUrl(root)}/releases/latest/download/${CLAUDE_PLUGIN_LATEST_ASSET_NAME}`;
29
38
  }
@@ -35,10 +44,13 @@ function getClaudePluginVersionedDownloadUrl(root = ROOT, version = getPackageVe
35
44
 
36
45
  module.exports = {
37
46
  CLAUDE_PLUGIN_LATEST_ASSET_NAME,
47
+ CLAUDE_PLUGIN_NEXT_ASSET_NAME,
38
48
  PRODUCTHUNT_URL,
49
+ getClaudePluginChannelAssetName,
39
50
  getClaudePluginLatestDownloadUrl,
40
51
  getClaudePluginVersionedAssetName,
41
52
  getClaudePluginVersionedDownloadUrl,
42
53
  getPackageVersion,
43
54
  getRepositoryUrl,
55
+ isPrereleaseVersion,
44
56
  };
@@ -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 RLHF_ENTRY = {
8
+ const THUMBGATE_ENTRY = {
9
9
  command: 'npx',
10
10
  args: ['-y', 'thumbgate@latest', 'serve'],
11
11
  };
12
- const LEGACY_SERVER_NAMES = ['thumbgate', 'rlhf_feedback_loop'];
13
- const INFO_EXCLUDE_ENTRIES = ['.rlhf/', '.thumbgate/', '.mcp.json'];
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 mergeRlhfEntry(entry = {}) {
40
+ function mergeThumbgateEntry(entry = {}) {
40
41
  return {
41
42
  ...entry,
42
- command: RLHF_ENTRY.command,
43
- args: RLHF_ENTRY.args.slice(),
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.rlhf = mergeRlhfEntry(config.mcpServers.rlhf);
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.rlhf) ||
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.rlhf = mergeRlhfEntry(existing.mcpServers.rlhf);
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 ensureRlhfDir(repoRoot) {
110
- const rlhfDir = path.join(repoRoot, '.rlhf');
111
- if (fs.existsSync(rlhfDir)) {
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(rlhfDir, { recursive: true });
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
- createdRlhfDir: ensureRlhfDir(REPO_ROOT),
122
+ createdThumbgateDir: ensureThumbgateDir(REPO_ROOT),
122
123
  updatedMcpJson: ensureMcpJson(REPO_ROOT),
123
124
  updatedClaudeSettings: ensureClaudeSettings(REPO_ROOT),
124
125
  updatedInfoExclude: ensureInfoExclude(REPO_ROOT),
@@ -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
- recordStat(memoryGuard.gate, 'block');
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: memoryGuard.gate,
1069
- message: memoryGuard.message,
1070
- severity: memoryGuard.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 memoryGuard;
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
- recordStat(memoryGuard.gate, 'block');
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: memoryGuard.gate,
1135
- message: memoryGuard.message,
1136
- severity: memoryGuard.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 memoryGuard;
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/.rlhf}"
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"