thumbgate 0.9.14 → 1.1.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 (64) 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 +41 -1
  11. package/adapters/opencode/opencode.json +1 -1
  12. package/bin/cli.js +18 -3
  13. package/config/mcp-allowlists.json +11 -0
  14. package/openapi/openapi.yaml +105 -0
  15. package/package.json +7 -5
  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 +8 -4
  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/export-hf-dataset.js +293 -0
  48. package/scripts/gates-engine.js +96 -10
  49. package/scripts/hook-auto-capture.sh +1 -1
  50. package/scripts/hosted-job-launcher.js +260 -0
  51. package/scripts/managed-dpo-export.js +91 -0
  52. package/scripts/obsidian-export.js +0 -1
  53. package/scripts/operational-integrity.js +50 -7
  54. package/scripts/prove-lancedb.js +62 -4
  55. package/scripts/publish-decision.js +16 -0
  56. package/scripts/self-healing-check.js +6 -1
  57. package/scripts/social-analytics/load-env.js +33 -2
  58. package/scripts/social-analytics/store.js +200 -2
  59. package/scripts/sync-version.js +18 -11
  60. package/scripts/tool-registry.js +48 -0
  61. package/scripts/train_from_feedback.py +0 -4
  62. package/scripts/workflow-sentinel.js +793 -0
  63. package/src/api/server.js +205 -27
  64. /package/scripts/{rlhf_session_start.sh → thumbgate_session_start.sh} +0 -0
@@ -0,0 +1,293 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * HuggingFace Dataset Exporter
6
+ *
7
+ * Exports ThumbGate agent traces as a HuggingFace-compatible dataset in two formats:
8
+ *
9
+ * 1. Agent Traces (traces split) — raw feedback entries with tool calls, signals,
10
+ * context, and outcomes. Matches the "share your agent traces" initiative.
11
+ *
12
+ * 2. DPO Preferences (preferences split) — chosen/rejected preference pairs
13
+ * derived from error→learning memory promotion. Ready for DPO/RLHF training.
14
+ *
15
+ * Output: Parquet-compatible JSONL files + dataset_info.json (HF Dataset Card metadata).
16
+ *
17
+ * HuggingFace Datasets format:
18
+ * dataset_dir/
19
+ * dataset_info.json — metadata, features schema, splits
20
+ * traces.jsonl — agent trace rows
21
+ * preferences.jsonl — DPO preference pair rows
22
+ */
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+ const { resolveFeedbackDir } = require('./feedback-paths');
27
+ const { exportDpoFromMemories } = require('./export-dpo-pairs');
28
+ const { getProvenance } = require('./contextfs');
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Helpers
32
+ // ---------------------------------------------------------------------------
33
+
34
+ function readJSONL(filePath) {
35
+ if (!fs.existsSync(filePath)) return [];
36
+ const raw = fs.readFileSync(filePath, 'utf-8').trim();
37
+ if (!raw) return [];
38
+ return raw
39
+ .split('\n')
40
+ .map((line) => {
41
+ try { return JSON.parse(line); } catch { return null; }
42
+ })
43
+ .filter(Boolean);
44
+ }
45
+
46
+ function ensureDir(dirPath) {
47
+ if (!fs.existsSync(dirPath)) {
48
+ fs.mkdirSync(dirPath, { recursive: true });
49
+ }
50
+ }
51
+
52
+ function writeJSONL(filePath, rows) {
53
+ const content = rows.map((row) => JSON.stringify(row)).join('\n');
54
+ fs.writeFileSync(filePath, content ? `${content}\n` : '');
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // PII / path redaction
59
+ // ---------------------------------------------------------------------------
60
+
61
+ function redactPaths(text) {
62
+ if (!text || typeof text !== 'string') return text || '';
63
+ return text
64
+ .replace(/\/Users\/[^\s/]+/g, '/Users/redacted')
65
+ .replace(/\/home\/[^\s/]+/g, '/home/redacted')
66
+ .replace(/C:\\Users\\[^\s\\]+/g, 'C:\\Users\\redacted');
67
+ }
68
+
69
+ function redactEntry(obj) {
70
+ if (!obj || typeof obj !== 'object') return obj;
71
+ const out = {};
72
+ for (const [key, value] of Object.entries(obj)) {
73
+ if (typeof value === 'string') {
74
+ out[key] = redactPaths(value);
75
+ } else if (Array.isArray(value)) {
76
+ out[key] = value.map((v) => (typeof v === 'string' ? redactPaths(v) : v));
77
+ } else {
78
+ out[key] = value;
79
+ }
80
+ }
81
+ return out;
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Trace row builder — converts feedback-log entries to HF trace rows
86
+ // ---------------------------------------------------------------------------
87
+
88
+ function buildTraceRow(entry, index) {
89
+ return {
90
+ trace_id: entry.id || `trace_${index}`,
91
+ timestamp: entry.timestamp || null,
92
+ signal: entry.signal || entry.feedback || 'unknown',
93
+ tool_name: entry.toolName || entry.actionType || 'unknown',
94
+ context: redactPaths(entry.context || ''),
95
+ what_worked: redactPaths(entry.whatWorked || ''),
96
+ what_went_wrong: redactPaths(entry.whatWentWrong || ''),
97
+ what_to_change: redactPaths(entry.whatToChange || ''),
98
+ tags: Array.isArray(entry.tags) ? entry.tags : [],
99
+ failure_type: entry.failureType || null,
100
+ source: 'thumbgate',
101
+ };
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Preference row builder — converts DPO pairs to HF preference rows
106
+ // ---------------------------------------------------------------------------
107
+
108
+ function buildPreferenceRow(pair, index) {
109
+ return {
110
+ pair_id: `pref_${index}`,
111
+ prompt: redactPaths(pair.prompt || ''),
112
+ chosen: redactPaths(pair.chosen || ''),
113
+ rejected: redactPaths(pair.rejected || ''),
114
+ match_score: pair.metadata ? pair.metadata.matchScore : null,
115
+ matched_keys: pair.metadata ? pair.metadata.matchedKeys || [] : [],
116
+ rubric_delta: pair.metadata && pair.metadata.rubric
117
+ ? pair.metadata.rubric.weightedDelta
118
+ : null,
119
+ source: 'thumbgate',
120
+ };
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Dataset info (HuggingFace Dataset Card metadata)
125
+ // ---------------------------------------------------------------------------
126
+
127
+ function buildDatasetInfo({ traceCount, preferenceCount, exportedAt }) {
128
+ return {
129
+ dataset_info: {
130
+ description: 'Agent traces and DPO preference pairs from ThumbGate — pre-action gates for AI coding agents. Contains real-world tool call feedback, failure patterns, and learned corrections.',
131
+ citation: '',
132
+ homepage: 'https://github.com/IgorGanapolsky/ThumbGate',
133
+ license: 'MIT',
134
+ features: {
135
+ traces: {
136
+ trace_id: { dtype: 'string' },
137
+ timestamp: { dtype: 'string' },
138
+ signal: { dtype: 'string' },
139
+ tool_name: { dtype: 'string' },
140
+ context: { dtype: 'string' },
141
+ what_worked: { dtype: 'string' },
142
+ what_went_wrong: { dtype: 'string' },
143
+ what_to_change: { dtype: 'string' },
144
+ tags: { dtype: 'list', inner: { dtype: 'string' } },
145
+ failure_type: { dtype: 'string' },
146
+ source: { dtype: 'string' },
147
+ },
148
+ preferences: {
149
+ pair_id: { dtype: 'string' },
150
+ prompt: { dtype: 'string' },
151
+ chosen: { dtype: 'string' },
152
+ rejected: { dtype: 'string' },
153
+ match_score: { dtype: 'float32' },
154
+ matched_keys: { dtype: 'list', inner: { dtype: 'string' } },
155
+ rubric_delta: { dtype: 'float32' },
156
+ source: { dtype: 'string' },
157
+ },
158
+ },
159
+ splits: {
160
+ traces: { num_examples: traceCount },
161
+ preferences: { num_examples: preferenceCount },
162
+ },
163
+ },
164
+ exported_at: exportedAt,
165
+ exporter: 'thumbgate/export-hf-dataset',
166
+ version: '1.0.0',
167
+ };
168
+ }
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // Main export function
172
+ // ---------------------------------------------------------------------------
173
+
174
+ /**
175
+ * Export ThumbGate data as a HuggingFace-compatible dataset.
176
+ *
177
+ * @param {Object} options
178
+ * @param {string} [options.outputDir] - Directory to write dataset files
179
+ * @param {string} [options.feedbackDir] - Override feedback data directory
180
+ * @param {boolean} [options.includeProvenance] - Include provenance events in traces
181
+ * @returns {Object} Export summary
182
+ */
183
+ function exportHfDataset(options = {}) {
184
+ const feedbackDir = options.feedbackDir || resolveFeedbackDir();
185
+ const outputDir = options.outputDir || path.join(feedbackDir, 'hf-dataset');
186
+ const includeProvenance = options.includeProvenance !== false;
187
+
188
+ ensureDir(outputDir);
189
+
190
+ // --- Traces split ---
191
+ const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
192
+ const feedbackEntries = readJSONL(feedbackLogPath);
193
+ const traceRows = feedbackEntries.map((entry, i) => buildTraceRow(redactEntry(entry), i));
194
+
195
+ // Optionally append provenance events as traces
196
+ if (includeProvenance) {
197
+ try {
198
+ const provenanceEvents = getProvenance(200);
199
+ for (const evt of provenanceEvents) {
200
+ traceRows.push({
201
+ trace_id: evt.id || `prov_${traceRows.length}`,
202
+ timestamp: evt.timestamp || null,
203
+ signal: 'provenance',
204
+ tool_name: evt.type || 'context_assembly',
205
+ context: redactPaths(JSON.stringify(evt).slice(0, 500)),
206
+ what_worked: '',
207
+ what_went_wrong: '',
208
+ what_to_change: '',
209
+ tags: ['provenance'],
210
+ failure_type: null,
211
+ source: 'thumbgate',
212
+ });
213
+ }
214
+ } catch {
215
+ // Provenance read failure should not break export
216
+ }
217
+ }
218
+
219
+ writeJSONL(path.join(outputDir, 'traces.jsonl'), traceRows);
220
+
221
+ // --- Preferences split ---
222
+ const memoryLogPath = path.join(feedbackDir, 'memory-log.jsonl');
223
+ const memories = readJSONL(memoryLogPath);
224
+ let preferenceRows = [];
225
+
226
+ if (memories.length > 0) {
227
+ try {
228
+ const dpoResult = exportDpoFromMemories(memories);
229
+ preferenceRows = dpoResult.pairs.map((pair, i) => buildPreferenceRow(pair, i));
230
+ } catch {
231
+ // DPO export failure should not break the traces export
232
+ }
233
+ }
234
+
235
+ writeJSONL(path.join(outputDir, 'preferences.jsonl'), preferenceRows);
236
+
237
+ // --- Dataset info ---
238
+ const exportedAt = new Date().toISOString();
239
+ const info = buildDatasetInfo({
240
+ traceCount: traceRows.length,
241
+ preferenceCount: preferenceRows.length,
242
+ exportedAt,
243
+ });
244
+ fs.writeFileSync(
245
+ path.join(outputDir, 'dataset_info.json'),
246
+ JSON.stringify(info, null, 2) + '\n',
247
+ );
248
+
249
+ return {
250
+ outputDir,
251
+ traceCount: traceRows.length,
252
+ preferenceCount: preferenceRows.length,
253
+ files: ['traces.jsonl', 'preferences.jsonl', 'dataset_info.json'],
254
+ exportedAt,
255
+ };
256
+ }
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // CLI
260
+ // ---------------------------------------------------------------------------
261
+
262
+ function main() {
263
+ const args = {};
264
+ process.argv.slice(2).forEach((arg) => {
265
+ if (!arg.startsWith('--')) return;
266
+ const [key, ...rest] = arg.slice(2).split('=');
267
+ args[key] = rest.length ? rest.join('=') : true;
268
+ });
269
+
270
+ const result = exportHfDataset({
271
+ outputDir: args.output || undefined,
272
+ includeProvenance: args.provenance !== 'false',
273
+ });
274
+
275
+ console.log(`Exported HuggingFace dataset to ${result.outputDir}`);
276
+ console.log(` Traces: ${result.traceCount}`);
277
+ console.log(` Preferences: ${result.preferenceCount}`);
278
+ console.log(` Files: ${result.files.join(', ')}`);
279
+ }
280
+
281
+ if (require.main === module) {
282
+ main();
283
+ }
284
+
285
+ module.exports = {
286
+ exportHfDataset,
287
+ buildTraceRow,
288
+ buildPreferenceRow,
289
+ buildDatasetInfo,
290
+ redactPaths,
291
+ redactEntry,
292
+ readJSONL,
293
+ };
@@ -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"