thumbgate 1.0.0 → 1.2.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 +16 -5
- package/adapters/README.md +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +19 -7
- package/adapters/opencode/opencode.json +1 -1
- package/config/github-about.json +1 -1
- package/config/mcp-allowlists.json +1 -0
- package/package.json +22 -11
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- 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/compare.html +302 -0
- package/public/index.html +41 -11
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/ai-search-visibility.js +142 -0
- package/scripts/changeset-check.js +372 -0
- package/scripts/check-congruence.js +7 -4
- package/scripts/computer-use-firewall.js +45 -15
- package/scripts/docker-sandbox-planner.js +208 -0
- package/scripts/export-hf-dataset.js +293 -0
- package/scripts/github-about.js +56 -0
- package/scripts/operational-integrity.js +7 -1
- package/scripts/published-cli.js +10 -1
- package/scripts/statusline-links.js +238 -0
- package/scripts/statusline.sh +39 -4
- package/scripts/sync-github-about.js +7 -4
- package/scripts/tool-registry.js +11 -0
- package/scripts/workflow-sentinel.js +83 -35
- package/src/api/server.js +12 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const { classifyCommand } = require('./operational-integrity');
|
|
7
|
+
|
|
8
|
+
const HIGH_RISK_ACTION_TYPES = new Set([
|
|
9
|
+
'shell.exec',
|
|
10
|
+
'file.delete',
|
|
11
|
+
'upload',
|
|
12
|
+
'message.send',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
function normalizeText(value) {
|
|
16
|
+
if (value === undefined || value === null) return '';
|
|
17
|
+
return String(value).trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeStringArray(values = []) {
|
|
21
|
+
if (!Array.isArray(values)) return [];
|
|
22
|
+
return Array.from(new Set(
|
|
23
|
+
values
|
|
24
|
+
.map((value) => normalizeText(value))
|
|
25
|
+
.filter(Boolean),
|
|
26
|
+
));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeRiskBand(value) {
|
|
30
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
31
|
+
if (['very_high', 'high', 'medium', 'low'].includes(normalized)) {
|
|
32
|
+
return normalized;
|
|
33
|
+
}
|
|
34
|
+
if (normalized === 'critical') return 'very_high';
|
|
35
|
+
return 'low';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function quoteShellArg(value) {
|
|
39
|
+
return `'${String(value).replaceAll('\'', String.raw`'\''`)}'`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildNetworkPolicy(input = {}) {
|
|
43
|
+
const allowedHosts = normalizeStringArray(input.allowedHosts || input.egressAllowlist);
|
|
44
|
+
if (input.requiresNetwork !== true) {
|
|
45
|
+
return {
|
|
46
|
+
mode: 'deny_all',
|
|
47
|
+
allowedHosts: [],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
mode: allowedHosts.length > 0 ? 'allow_list' : 'egress_enabled',
|
|
52
|
+
allowedHosts,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildLaunchers(workspacePath) {
|
|
57
|
+
const suffix = workspacePath ? ` shell ${quoteShellArg(workspacePath)}` : ' shell';
|
|
58
|
+
return {
|
|
59
|
+
standalone: `sbx run${suffix}`,
|
|
60
|
+
dockerDesktop: `docker sandbox run${suffix}`,
|
|
61
|
+
followUp: workspacePath
|
|
62
|
+
? [
|
|
63
|
+
'sbx list',
|
|
64
|
+
'docker sandbox ls',
|
|
65
|
+
]
|
|
66
|
+
: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildSummary(shouldSandbox, recommendation) {
|
|
71
|
+
if (!shouldSandbox) {
|
|
72
|
+
return 'Current action can stay on the normal local execution path.';
|
|
73
|
+
}
|
|
74
|
+
if (recommendation === 'required') {
|
|
75
|
+
return 'Route this action into Docker Sandboxes before retrying so the run happens inside a disposable microVM instead of on the host.';
|
|
76
|
+
}
|
|
77
|
+
return 'Prefer Docker Sandboxes for this action to reduce host blast radius while keeping local autonomy.';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildWhy({
|
|
81
|
+
recommendation,
|
|
82
|
+
command,
|
|
83
|
+
riskBand,
|
|
84
|
+
actionType,
|
|
85
|
+
affectedFiles,
|
|
86
|
+
}) {
|
|
87
|
+
const lines = [];
|
|
88
|
+
if (recommendation === 'required') {
|
|
89
|
+
lines.push('The predicted action is destructive or release-sensitive enough to justify host isolation.');
|
|
90
|
+
} else if (recommendation === 'recommended') {
|
|
91
|
+
lines.push('The predicted action is high-risk enough that isolated execution meaningfully reduces host blast radius.');
|
|
92
|
+
} else {
|
|
93
|
+
lines.push('The current action does not need a dedicated Docker sandbox boundary.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (command && /\brm\s+-rf\b/i.test(command)) {
|
|
97
|
+
lines.push('Recursive delete commands are safer when the filesystem boundary lives inside a disposable microVM.');
|
|
98
|
+
}
|
|
99
|
+
if (command && /\bgit\s+push\b.*(?:--force|-f)\b/i.test(command)) {
|
|
100
|
+
lines.push('Force-push flows should run in an isolated lane so host credentials and unrelated state stay out of scope.');
|
|
101
|
+
}
|
|
102
|
+
if (command && /\b(?:gh\s+pr\s+(?:create|merge)|npm\s+publish|yarn\s+publish|pnpm\s+publish)\b/i.test(command)) {
|
|
103
|
+
lines.push('PR, merge, and publish flows are governance-sensitive and benefit from a disposable execution boundary.');
|
|
104
|
+
}
|
|
105
|
+
if (HIGH_RISK_ACTION_TYPES.has(actionType)) {
|
|
106
|
+
lines.push(`Action type ${actionType} is in the high-risk set for local execution.`);
|
|
107
|
+
}
|
|
108
|
+
if (riskBand === 'very_high' || riskBand === 'high') {
|
|
109
|
+
lines.push(`Risk band ${riskBand} predicts elevated blast radius on the local host.`);
|
|
110
|
+
}
|
|
111
|
+
if (affectedFiles.length >= 4) {
|
|
112
|
+
lines.push(`The change touches ${affectedFiles.length} files, so host isolation improves recovery if the run goes sideways.`);
|
|
113
|
+
}
|
|
114
|
+
return lines;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildDockerSandboxPlan(input = {}) {
|
|
118
|
+
const toolName = normalizeText(input.toolName);
|
|
119
|
+
const actionType = normalizeText(input.actionType)
|
|
120
|
+
|| (toolName === 'Bash' ? 'shell.exec' : '');
|
|
121
|
+
const command = normalizeText(input.command);
|
|
122
|
+
const repoPath = normalizeText(input.repoPath);
|
|
123
|
+
const workspacePath = repoPath ? path.resolve(repoPath) : null;
|
|
124
|
+
const affectedFiles = normalizeStringArray(input.affectedFiles || input.changedFiles || input.files);
|
|
125
|
+
const riskBand = normalizeRiskBand(input.riskBand || input.band);
|
|
126
|
+
const riskScore = Number.isFinite(Number(input.riskScore))
|
|
127
|
+
? Number(Number(input.riskScore).toFixed(4))
|
|
128
|
+
: null;
|
|
129
|
+
const commandInfo = classifyCommand(command);
|
|
130
|
+
const destructiveCommand = /\brm\s+-rf\b/i.test(command)
|
|
131
|
+
|| /\bgit\s+push\b.*(?:--force|-f)\b/i.test(command)
|
|
132
|
+
|| /\bgh\s+pr\s+merge\b.*--admin\b/i.test(command);
|
|
133
|
+
const governedCommand = Boolean(
|
|
134
|
+
commandInfo.isPrCreate
|
|
135
|
+
|| commandInfo.isPrMerge
|
|
136
|
+
|| commandInfo.isPublish
|
|
137
|
+
|| commandInfo.isReleaseCreate
|
|
138
|
+
|| commandInfo.isTagCreate
|
|
139
|
+
);
|
|
140
|
+
const highRiskAction = HIGH_RISK_ACTION_TYPES.has(actionType)
|
|
141
|
+
|| destructiveCommand
|
|
142
|
+
|| governedCommand
|
|
143
|
+
|| riskBand === 'high'
|
|
144
|
+
|| riskBand === 'very_high';
|
|
145
|
+
|
|
146
|
+
let recommendation = 'not_needed';
|
|
147
|
+
if (destructiveCommand || commandInfo.isPublish || commandInfo.isReleaseCreate || actionType === 'upload' || actionType === 'message.send') {
|
|
148
|
+
recommendation = 'required';
|
|
149
|
+
} else if (highRiskAction || affectedFiles.length >= 4) {
|
|
150
|
+
recommendation = 'recommended';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const shouldSandbox = recommendation !== 'not_needed';
|
|
154
|
+
const networkPolicy = buildNetworkPolicy({
|
|
155
|
+
requiresNetwork: input.requiresNetwork === true || governedCommand || commandInfo.isPublish || actionType === 'upload' || actionType === 'message.send',
|
|
156
|
+
allowedHosts: input.allowedHosts,
|
|
157
|
+
egressAllowlist: input.egressAllowlist,
|
|
158
|
+
});
|
|
159
|
+
const launchers = buildLaunchers(workspacePath);
|
|
160
|
+
const summary = buildSummary(shouldSandbox, recommendation);
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
plannerVersion: 'docker-sandbox-plan-v1',
|
|
164
|
+
shouldSandbox,
|
|
165
|
+
recommendation,
|
|
166
|
+
summary,
|
|
167
|
+
sandboxKind: shouldSandbox ? 'docker_microvm' : 'host',
|
|
168
|
+
workspacePath,
|
|
169
|
+
actionType: actionType || null,
|
|
170
|
+
riskBand,
|
|
171
|
+
riskScore,
|
|
172
|
+
command: command || null,
|
|
173
|
+
affectedFiles,
|
|
174
|
+
networkPolicy,
|
|
175
|
+
launchers,
|
|
176
|
+
claims: shouldSandbox ? {
|
|
177
|
+
isolationBoundary: 'microvm',
|
|
178
|
+
hostAccess: 'bounded_outside_host',
|
|
179
|
+
dockerDaemon: 'private_inside_sandbox',
|
|
180
|
+
workspaceStrategy: workspacePath ? 'directory_sync' : 'ephemeral',
|
|
181
|
+
} : null,
|
|
182
|
+
why: buildWhy({
|
|
183
|
+
recommendation,
|
|
184
|
+
command,
|
|
185
|
+
riskBand,
|
|
186
|
+
actionType,
|
|
187
|
+
affectedFiles,
|
|
188
|
+
}),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = {
|
|
193
|
+
HIGH_RISK_ACTION_TYPES,
|
|
194
|
+
buildDockerSandboxPlan,
|
|
195
|
+
buildLaunchers,
|
|
196
|
+
buildNetworkPolicy,
|
|
197
|
+
buildSummary,
|
|
198
|
+
normalizeRiskBand,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
if (require.main === module) {
|
|
202
|
+
const plan = buildDockerSandboxPlan({
|
|
203
|
+
toolName: process.argv[2] || 'Bash',
|
|
204
|
+
command: process.argv.slice(3).join(' '),
|
|
205
|
+
repoPath: process.cwd(),
|
|
206
|
+
});
|
|
207
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
208
|
+
}
|
|
@@ -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
|
+
};
|
package/scripts/github-about.js
CHANGED
|
@@ -12,6 +12,8 @@ const ROOT = path.join(__dirname, '..');
|
|
|
12
12
|
const CONFIG_RELATIVE_PATH = path.join('config', 'github-about.json');
|
|
13
13
|
const LEGACY_REPOSITORY_URL = 'https://github.com/IgorGanapolsky/thumbgate';
|
|
14
14
|
const GITHUB_API_BASE_URL = 'https://api.github.com';
|
|
15
|
+
const DEFAULT_VERIFY_ATTEMPTS = 5;
|
|
16
|
+
const DEFAULT_VERIFY_DELAY_MS = 2000;
|
|
15
17
|
|
|
16
18
|
function readText(root, relativePath) {
|
|
17
19
|
return fs.readFileSync(path.join(root, relativePath), 'utf8');
|
|
@@ -296,6 +298,57 @@ async function fetchLiveGitHubAbout(options = {}) {
|
|
|
296
298
|
};
|
|
297
299
|
}
|
|
298
300
|
|
|
301
|
+
function normalizePositiveInteger(value, fallback) {
|
|
302
|
+
const parsed = Number.parseInt(value, 10);
|
|
303
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function sleep(delayMs) {
|
|
307
|
+
return new Promise((resolve) => {
|
|
308
|
+
setTimeout(resolve, delayMs);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function verifyLiveGitHubAbout(options = {}) {
|
|
313
|
+
const root = options.root || ROOT;
|
|
314
|
+
const expected = options.expected || loadGitHubAboutConfig(root);
|
|
315
|
+
const repo = normalizeText(options.repo) || expected.repo;
|
|
316
|
+
const label = options.label || `Live GitHub About (${repo})`;
|
|
317
|
+
const attempts = normalizePositiveInteger(options.attempts, DEFAULT_VERIFY_ATTEMPTS);
|
|
318
|
+
const delayMs = normalizePositiveInteger(options.delayMs, DEFAULT_VERIFY_DELAY_MS);
|
|
319
|
+
const fetcher = typeof options.fetcher === 'function' ? options.fetcher : fetchLiveGitHubAbout;
|
|
320
|
+
const sleeper = typeof options.sleep === 'function' ? options.sleep : sleep;
|
|
321
|
+
let actual = null;
|
|
322
|
+
let errors = [];
|
|
323
|
+
|
|
324
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
325
|
+
actual = await fetcher({
|
|
326
|
+
root,
|
|
327
|
+
repo,
|
|
328
|
+
token: options.token,
|
|
329
|
+
});
|
|
330
|
+
errors = compareGitHubAbout(expected, actual, label);
|
|
331
|
+
if (errors.length === 0) {
|
|
332
|
+
return {
|
|
333
|
+
ok: true,
|
|
334
|
+
actual,
|
|
335
|
+
attemptsUsed: attempt,
|
|
336
|
+
errors: [],
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
if (attempt < attempts) {
|
|
340
|
+
await sleeper(delayMs * attempt);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
ok: false,
|
|
346
|
+
actual,
|
|
347
|
+
attemptsUsed: attempts,
|
|
348
|
+
errors,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
299
352
|
async function updateLiveGitHubAbout(options = {}) {
|
|
300
353
|
const about = loadGitHubAboutConfig(options.root || ROOT);
|
|
301
354
|
const repo = normalizeText(options.repo) || about.repo;
|
|
@@ -334,6 +387,8 @@ async function updateLiveGitHubAbout(options = {}) {
|
|
|
334
387
|
}
|
|
335
388
|
|
|
336
389
|
module.exports = {
|
|
390
|
+
DEFAULT_VERIFY_ATTEMPTS,
|
|
391
|
+
DEFAULT_VERIFY_DELAY_MS,
|
|
337
392
|
LEGACY_REPOSITORY_URL,
|
|
338
393
|
buildCanonicalRepoUrls,
|
|
339
394
|
collectLocalGitHubAboutErrors,
|
|
@@ -347,4 +402,5 @@ module.exports = {
|
|
|
347
402
|
normalizeTopics,
|
|
348
403
|
normalizeUrl,
|
|
349
404
|
updateLiveGitHubAbout,
|
|
405
|
+
verifyLiveGitHubAbout,
|
|
350
406
|
};
|
|
@@ -457,12 +457,17 @@ function parseCliArgs(argv = process.argv.slice(2)) {
|
|
|
457
457
|
return options;
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
+
function resolveCiBranchName(env = process.env) {
|
|
461
|
+
const branchName = String(env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME || '').trim();
|
|
462
|
+
return branchName || undefined;
|
|
463
|
+
}
|
|
464
|
+
|
|
460
465
|
function runCli(env = process.env, argv = process.argv.slice(2)) {
|
|
461
466
|
const args = parseCliArgs(argv);
|
|
462
467
|
const result = evaluateOperationalIntegrity({
|
|
463
468
|
repoPath: args.repoPath,
|
|
464
469
|
baseBranch: args.baseBranch || env.DEFAULT_BRANCH || DEFAULT_BASE_BRANCH,
|
|
465
|
-
currentBranch: env
|
|
470
|
+
currentBranch: resolveCiBranchName(env),
|
|
466
471
|
requirePrForReleaseSensitive: args.requirePrForReleaseSensitive,
|
|
467
472
|
requireVersionNotBehindBase: args.requireVersionNotBehindBase,
|
|
468
473
|
fetchBase: args.fetchBase,
|
|
@@ -517,6 +522,7 @@ module.exports = {
|
|
|
517
522
|
parseSemver,
|
|
518
523
|
readPackageVersion,
|
|
519
524
|
resolveBaseRef,
|
|
525
|
+
resolveCiBranchName,
|
|
520
526
|
resolveRepoRoot,
|
|
521
527
|
runCli,
|
|
522
528
|
sanitizeGlobList,
|
package/scripts/published-cli.js
CHANGED
|
@@ -13,6 +13,10 @@ function runtimePrefixDir(prefixDir) {
|
|
|
13
13
|
return prefixDir || path.join(os.homedir(), '.thumbgate', 'runtime');
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function installedRuntimeBin(prefixDir) {
|
|
17
|
+
return path.join(runtimePrefixDir(prefixDir), 'node_modules', '.bin', 'thumbgate');
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
function publishedCliArgs(pkgVersion, commandArgs = [], options = {}) {
|
|
17
21
|
return [
|
|
18
22
|
'exec',
|
|
@@ -29,7 +33,11 @@ function publishedCliArgs(pkgVersion, commandArgs = [], options = {}) {
|
|
|
29
33
|
|
|
30
34
|
function publishedCliShellCommand(pkgVersion, commandArgs = [], options = {}) {
|
|
31
35
|
const prefixDir = runtimePrefixDir(options.prefixDir);
|
|
32
|
-
|
|
36
|
+
const runtimeBin = installedRuntimeBin(prefixDir);
|
|
37
|
+
const escapedArgs = commandArgs.map(shellQuote).join(' ');
|
|
38
|
+
const fastPath = `[ -x ${shellQuote(runtimeBin)} ] && exec ${shellQuote(runtimeBin)}${escapedArgs ? ` ${escapedArgs}` : ''}`;
|
|
39
|
+
const installPath = `mkdir -p ${shellQuote(prefixDir)} && exec npm ${publishedCliArgs(pkgVersion, commandArgs, { prefixDir }).map(shellQuote).join(' ')}`;
|
|
40
|
+
return `${fastPath} || ${installPath}`;
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
function runPublishedCli(pkgVersion, commandArgs = [], options = {}) {
|
|
@@ -55,6 +63,7 @@ function runPublishedCliHelp(pkgVersion, options = {}) {
|
|
|
55
63
|
module.exports = {
|
|
56
64
|
publishedCliArgs,
|
|
57
65
|
publishedCliShellCommand,
|
|
66
|
+
installedRuntimeBin,
|
|
58
67
|
runtimePrefixDir,
|
|
59
68
|
runPublishedCli,
|
|
60
69
|
runPublishedCliHelp,
|