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
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const runner = require('./async-job-runner');
|
|
8
|
+
const { buildHarnessJob } = require('./natural-language-harness');
|
|
9
|
+
|
|
10
|
+
const RUNNER_SCRIPT_PATH = path.join(__dirname, 'async-job-runner.js');
|
|
11
|
+
const MANAGED_DPO_EXPORT_SCRIPT_PATH = path.join(__dirname, 'managed-dpo-export.js');
|
|
12
|
+
const BACKGROUND_LAUNCH_MODE = 'background';
|
|
13
|
+
const INLINE_LAUNCH_MODE = 'inline';
|
|
14
|
+
const IDLE_JOB_STATUSES = new Set(['queued', 'paused', 'resume_requested']);
|
|
15
|
+
|
|
16
|
+
function nowIso() {
|
|
17
|
+
return new Date().toISOString();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ensureDir(dirPath) {
|
|
21
|
+
if (!fs.existsSync(dirPath)) {
|
|
22
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function shellQuote(value) {
|
|
27
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createHostedJobId(prefix = 'job') {
|
|
31
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getStatePath(jobId) {
|
|
35
|
+
return runner.getJobRuntimePaths(jobId).statePath;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeStateFile(jobId, state) {
|
|
39
|
+
const statePath = getStatePath(jobId);
|
|
40
|
+
ensureDir(path.dirname(statePath));
|
|
41
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
42
|
+
return state;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function updateIdleJobState(jobId, updater) {
|
|
46
|
+
const state = runner.readJobState(jobId);
|
|
47
|
+
if (!state) {
|
|
48
|
+
const error = new Error(`No persisted state found for job ${jobId}`);
|
|
49
|
+
error.statusCode = 404;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!IDLE_JOB_STATUSES.has(state.status)) {
|
|
54
|
+
const error = new Error(`Job ${jobId} is not idle; current status is ${state.status}`);
|
|
55
|
+
error.statusCode = 409;
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return writeStateFile(jobId, updater({ ...state }));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function writeJobFile(jobId, jobSpec) {
|
|
63
|
+
const { jobDir } = runner.getJobRuntimePaths(jobId);
|
|
64
|
+
ensureDir(jobDir);
|
|
65
|
+
const jobFilePath = path.join(jobDir, 'job.json');
|
|
66
|
+
fs.writeFileSync(jobFilePath, JSON.stringify(jobSpec, null, 2) + '\n', 'utf8');
|
|
67
|
+
return jobFilePath;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function runInlineJob(args) {
|
|
71
|
+
if (args.runFile) {
|
|
72
|
+
runner.runJobFromFile(args.runFile);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (args.resumeJobId) {
|
|
77
|
+
runner.resumeJob(args.resumeJobId);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
throw new Error('Unsupported inline hosted job launch');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function launchRunner(args, options = {}) {
|
|
85
|
+
const launchMode = options.launchMode || process.env.THUMBGATE_HOSTED_JOB_LAUNCH_MODE || BACKGROUND_LAUNCH_MODE;
|
|
86
|
+
if (launchMode === INLINE_LAUNCH_MODE) {
|
|
87
|
+
runInlineJob(args);
|
|
88
|
+
return {
|
|
89
|
+
launchMode,
|
|
90
|
+
pid: process.pid,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const runnerArgs = [];
|
|
95
|
+
if (args.runFile) {
|
|
96
|
+
runnerArgs.push(`--run-file=${args.runFile}`);
|
|
97
|
+
} else if (args.resumeJobId) {
|
|
98
|
+
runnerArgs.push(`--resume=${args.resumeJobId}`);
|
|
99
|
+
} else {
|
|
100
|
+
throw new Error('Hosted job launch requires runFile or resumeJobId');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const child = spawn(process.execPath, [RUNNER_SCRIPT_PATH, ...runnerArgs], {
|
|
104
|
+
cwd: options.cwd || process.cwd(),
|
|
105
|
+
env: process.env,
|
|
106
|
+
detached: true,
|
|
107
|
+
stdio: 'ignore',
|
|
108
|
+
});
|
|
109
|
+
child.unref();
|
|
110
|
+
return {
|
|
111
|
+
launchMode,
|
|
112
|
+
pid: child.pid,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function prepareManagedJob(jobSpec, options = {}) {
|
|
117
|
+
const jobId = options.jobId || jobSpec.id || createHostedJobId(options.jobPrefix || 'job');
|
|
118
|
+
const finalSpec = {
|
|
119
|
+
...jobSpec,
|
|
120
|
+
id: jobId,
|
|
121
|
+
};
|
|
122
|
+
const jobFilePath = writeJobFile(jobId, finalSpec);
|
|
123
|
+
const queuedState = runner.queueJob({
|
|
124
|
+
...finalSpec,
|
|
125
|
+
jobFilePath,
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
jobId,
|
|
129
|
+
jobFilePath,
|
|
130
|
+
state: queuedState,
|
|
131
|
+
jobSpec: {
|
|
132
|
+
...finalSpec,
|
|
133
|
+
jobFilePath,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function launchManagedJob(jobSpec, options = {}) {
|
|
139
|
+
const prepared = prepareManagedJob(jobSpec, options);
|
|
140
|
+
const launch = launchRunner({ runFile: prepared.jobFilePath }, options);
|
|
141
|
+
return {
|
|
142
|
+
jobId: prepared.jobId,
|
|
143
|
+
jobFilePath: prepared.jobFilePath,
|
|
144
|
+
launchMode: launch.launchMode,
|
|
145
|
+
pid: launch.pid || null,
|
|
146
|
+
state: runner.readJobState(prepared.jobId) || prepared.state,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function buildManagedDpoExportJob(params = {}) {
|
|
151
|
+
const command = [
|
|
152
|
+
shellQuote(process.execPath),
|
|
153
|
+
shellQuote(MANAGED_DPO_EXPORT_SCRIPT_PATH),
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
if (params.inputPath) {
|
|
157
|
+
command.push('--inputPath', shellQuote(params.inputPath));
|
|
158
|
+
} else if (params.memoryLogPath) {
|
|
159
|
+
command.push('--memoryLogPath', shellQuote(params.memoryLogPath));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (params.outputPath) {
|
|
163
|
+
command.push('--outputPath', shellQuote(params.outputPath));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
tags: ['hosted-job', 'dpo-export'],
|
|
168
|
+
skill: 'hosted-dpo-export',
|
|
169
|
+
autoImprove: false,
|
|
170
|
+
verificationMode: 'none',
|
|
171
|
+
recordFeedback: false,
|
|
172
|
+
stages: [
|
|
173
|
+
{
|
|
174
|
+
name: 'export_dpo_pairs',
|
|
175
|
+
command: command.join(' '),
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function launchDpoExportJob(params = {}, options = {}) {
|
|
182
|
+
return launchManagedJob(buildManagedDpoExportJob(params), {
|
|
183
|
+
...options,
|
|
184
|
+
jobPrefix: 'dpo_export',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function launchHarnessJob(identifier, inputs = {}, options = {}) {
|
|
189
|
+
const jobId = options.jobId || createHostedJobId('harness');
|
|
190
|
+
const jobSpec = buildHarnessJob(identifier, inputs, {
|
|
191
|
+
jobId,
|
|
192
|
+
skill: options.skill,
|
|
193
|
+
partnerProfile: options.partnerProfile,
|
|
194
|
+
autoImprove: options.autoImprove,
|
|
195
|
+
});
|
|
196
|
+
return launchManagedJob(jobSpec, {
|
|
197
|
+
...options,
|
|
198
|
+
jobId,
|
|
199
|
+
jobPrefix: 'harness',
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function resumeHostedJob(jobId, options = {}) {
|
|
204
|
+
const state = runner.readJobState(jobId);
|
|
205
|
+
if (!state) {
|
|
206
|
+
const error = new Error(`No persisted state found for job ${jobId}`);
|
|
207
|
+
error.statusCode = 404;
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (['completed', 'failed', 'cancelled'].includes(state.status)) {
|
|
212
|
+
const error = new Error(`Job ${jobId} is already ${state.status}`);
|
|
213
|
+
error.statusCode = 409;
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const launch = launchRunner({ resumeJobId: jobId }, options);
|
|
218
|
+
return {
|
|
219
|
+
jobId,
|
|
220
|
+
launchMode: launch.launchMode,
|
|
221
|
+
pid: launch.pid || null,
|
|
222
|
+
state: runner.readJobState(jobId) || state,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function pauseQueuedJob(jobId, metadata = {}) {
|
|
227
|
+
runner.clearJobControl(jobId);
|
|
228
|
+
return updateIdleJobState(jobId, (state) => ({
|
|
229
|
+
...state,
|
|
230
|
+
status: 'paused',
|
|
231
|
+
updatedAt: nowIso(),
|
|
232
|
+
pausedAt: nowIso(),
|
|
233
|
+
stopReason: metadata && metadata.reason ? metadata.reason : 'pause_requested',
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function cancelQueuedJob(jobId, metadata = {}) {
|
|
238
|
+
runner.clearJobControl(jobId);
|
|
239
|
+
return updateIdleJobState(jobId, (state) => ({
|
|
240
|
+
...state,
|
|
241
|
+
status: 'cancelled',
|
|
242
|
+
updatedAt: nowIso(),
|
|
243
|
+
endedAt: nowIso(),
|
|
244
|
+
stopReason: metadata && metadata.reason ? metadata.reason : 'cancel_requested',
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
module.exports = {
|
|
249
|
+
BACKGROUND_LAUNCH_MODE,
|
|
250
|
+
INLINE_LAUNCH_MODE,
|
|
251
|
+
buildManagedDpoExportJob,
|
|
252
|
+
cancelQueuedJob,
|
|
253
|
+
createHostedJobId,
|
|
254
|
+
launchDpoExportJob,
|
|
255
|
+
launchHarnessJob,
|
|
256
|
+
launchManagedJob,
|
|
257
|
+
pauseQueuedJob,
|
|
258
|
+
prepareManagedJob,
|
|
259
|
+
resumeHostedJob,
|
|
260
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
readJSONL,
|
|
9
|
+
exportDpoFromMemories,
|
|
10
|
+
DEFAULT_LOCAL_MEMORY_LOG,
|
|
11
|
+
} = require('./export-dpo-pairs');
|
|
12
|
+
|
|
13
|
+
function parseArgs(argv) {
|
|
14
|
+
const args = {};
|
|
15
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
16
|
+
const token = argv[index];
|
|
17
|
+
if (!token.startsWith('--')) continue;
|
|
18
|
+
|
|
19
|
+
const trimmed = token.slice(2);
|
|
20
|
+
const separatorIndex = trimmed.indexOf('=');
|
|
21
|
+
if (separatorIndex !== -1) {
|
|
22
|
+
const key = trimmed.slice(0, separatorIndex);
|
|
23
|
+
args[key] = trimmed.slice(separatorIndex + 1);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const next = argv[index + 1];
|
|
28
|
+
if (next && !next.startsWith('--')) {
|
|
29
|
+
args[trimmed] = next;
|
|
30
|
+
index += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
args[trimmed] = true;
|
|
35
|
+
}
|
|
36
|
+
if (args['input-path'] && !args.inputPath) args.inputPath = args['input-path'];
|
|
37
|
+
if (args['memory-log-path'] && !args.memoryLogPath) args.memoryLogPath = args['memory-log-path'];
|
|
38
|
+
if (args['output-path'] && !args.outputPath) args.outputPath = args['output-path'];
|
|
39
|
+
return args;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function loadMemories(args) {
|
|
43
|
+
if (args.inputPath) {
|
|
44
|
+
const raw = fs.readFileSync(path.resolve(args.inputPath), 'utf8');
|
|
45
|
+
const parsed = JSON.parse(raw);
|
|
46
|
+
return Array.isArray(parsed) ? parsed : parsed.memories || [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const memoryLogPath = args.memoryLogPath
|
|
50
|
+
? path.resolve(args.memoryLogPath)
|
|
51
|
+
: DEFAULT_LOCAL_MEMORY_LOG;
|
|
52
|
+
return readJSONL(memoryLogPath);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function run(argv = process.argv.slice(2)) {
|
|
56
|
+
const args = parseArgs(argv);
|
|
57
|
+
const memories = loadMemories(args);
|
|
58
|
+
const result = exportDpoFromMemories(memories);
|
|
59
|
+
|
|
60
|
+
let outputPath = null;
|
|
61
|
+
if (args.outputPath) {
|
|
62
|
+
outputPath = path.resolve(args.outputPath);
|
|
63
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
64
|
+
fs.writeFileSync(outputPath, result.jsonl, 'utf8');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const summary = {
|
|
68
|
+
pairs: result.pairs.length,
|
|
69
|
+
errors: result.errors.length,
|
|
70
|
+
learnings: result.learnings.length,
|
|
71
|
+
unpairedErrors: result.unpairedErrors.length,
|
|
72
|
+
unpairedLearnings: result.unpairedLearnings.length,
|
|
73
|
+
outputPath,
|
|
74
|
+
};
|
|
75
|
+
process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
|
|
76
|
+
return summary;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (require.main === module) {
|
|
80
|
+
try {
|
|
81
|
+
run();
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error(error && error.message ? error.message : 'managed DPO export failed');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
parseArgs,
|
|
90
|
+
run,
|
|
91
|
+
};
|
|
@@ -346,7 +346,6 @@ function exportGates(configPath, outputDir) {
|
|
|
346
346
|
// Read auto-promoted gates if present (check common locations)
|
|
347
347
|
const autoGatePaths = [
|
|
348
348
|
path.join(path.dirname(configPath), '..', '.thumbgate', 'auto-promoted-gates.json'),
|
|
349
|
-
path.join(path.dirname(configPath), '..', '.rlhf', 'auto-promoted-gates.json'),
|
|
350
349
|
path.join(path.dirname(configPath), '..', '.claude', 'memory', 'feedback', 'auto-promoted-gates.json'),
|
|
351
350
|
];
|
|
352
351
|
for (const agPath of autoGatePaths) {
|
|
@@ -174,20 +174,63 @@ function readPackageVersion(repoPath, ref = 'HEAD') {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
function parseSemver(version) {
|
|
177
|
-
const match = String(version || '').trim().match(/^(\d+)\.(\d+)\.(\d+)(
|
|
177
|
+
const match = String(version || '').trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/);
|
|
178
178
|
if (!match) return null;
|
|
179
|
-
return
|
|
179
|
+
return {
|
|
180
|
+
major: Number(match[1]),
|
|
181
|
+
minor: Number(match[2]),
|
|
182
|
+
patch: Number(match[3]),
|
|
183
|
+
prerelease: match[4] ? match[4].split('.').filter(Boolean) : [],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isNumericIdentifier(value) {
|
|
188
|
+
return /^\d+$/.test(String(value || ''));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function comparePrerelease(left = [], right = []) {
|
|
192
|
+
const maxLength = Math.max(left.length, right.length);
|
|
193
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
194
|
+
const leftIdentifier = left[index];
|
|
195
|
+
const rightIdentifier = right[index];
|
|
196
|
+
|
|
197
|
+
if (leftIdentifier === undefined) return -1;
|
|
198
|
+
if (rightIdentifier === undefined) return 1;
|
|
199
|
+
if (leftIdentifier === rightIdentifier) continue;
|
|
200
|
+
|
|
201
|
+
const leftIsNumeric = isNumericIdentifier(leftIdentifier);
|
|
202
|
+
const rightIsNumeric = isNumericIdentifier(rightIdentifier);
|
|
203
|
+
|
|
204
|
+
if (leftIsNumeric && rightIsNumeric) {
|
|
205
|
+
const leftValue = Number(leftIdentifier);
|
|
206
|
+
const rightValue = Number(rightIdentifier);
|
|
207
|
+
if (leftValue > rightValue) return 1;
|
|
208
|
+
if (leftValue < rightValue) return -1;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (leftIsNumeric !== rightIsNumeric) {
|
|
213
|
+
return leftIsNumeric ? -1 : 1;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const lexical = String(leftIdentifier).localeCompare(String(rightIdentifier));
|
|
217
|
+
if (lexical !== 0) return lexical > 0 ? 1 : -1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return 0;
|
|
180
221
|
}
|
|
181
222
|
|
|
182
223
|
function compareSemver(left, right) {
|
|
183
224
|
const a = parseSemver(left);
|
|
184
225
|
const b = parseSemver(right);
|
|
185
226
|
if (!a || !b) return null;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return
|
|
227
|
+
if (a.major !== b.major) return a.major > b.major ? 1 : -1;
|
|
228
|
+
if (a.minor !== b.minor) return a.minor > b.minor ? 1 : -1;
|
|
229
|
+
if (a.patch !== b.patch) return a.patch > b.patch ? 1 : -1;
|
|
230
|
+
if (a.prerelease.length === 0 && b.prerelease.length === 0) return 0;
|
|
231
|
+
if (a.prerelease.length === 0) return 1;
|
|
232
|
+
if (b.prerelease.length === 0) return -1;
|
|
233
|
+
return comparePrerelease(a.prerelease, b.prerelease);
|
|
191
234
|
}
|
|
192
235
|
|
|
193
236
|
function listChangedFilesAgainstBase(repoPath, baseBranch = DEFAULT_BASE_BRANCH, { fetchIfMissing = false } = {}) {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
const fs = require('fs');
|
|
26
26
|
const path = require('path');
|
|
27
27
|
const { tagUrlsInText } = require('./social-analytics/utm');
|
|
28
|
+
const { isDuplicate, recordPost } = require('./social-analytics/publishers/zernio');
|
|
28
29
|
|
|
29
30
|
// ---------------------------------------------------------------------------
|
|
30
31
|
// Publisher imports (lazy — only loaded when needed)
|
|
@@ -259,9 +260,18 @@ async function postEverywhere(filePath, { platforms, dryRun } = {}) {
|
|
|
259
260
|
continue;
|
|
260
261
|
}
|
|
261
262
|
|
|
263
|
+
// Dedup guard: skip platforms where identical content was posted in last 24h
|
|
264
|
+
const dedupContent = [parsed.title, parsed.body].filter(Boolean).join('\n');
|
|
265
|
+
if (!dryRun && isDuplicate(dedupContent, platform)) {
|
|
266
|
+
console.log(`[post-everywhere] ${platform}: SKIPPED — duplicate content within 24h`);
|
|
267
|
+
results[platform] = { skipped: true, reason: 'duplicate_content_24h' };
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
262
271
|
try {
|
|
263
272
|
console.log(`\n[post-everywhere] Posting to ${platform}...`);
|
|
264
273
|
results[platform] = await dispatcher(parsed, dryRun);
|
|
274
|
+
if (!dryRun) recordPost(dedupContent, platform);
|
|
265
275
|
console.log(`[post-everywhere] ${platform}: OK`);
|
|
266
276
|
} catch (err) {
|
|
267
277
|
console.error(`[post-everywhere] ${platform}: FAILED — ${err.message}`);
|
package/scripts/prove-lancedb.js
CHANGED
|
@@ -21,6 +21,48 @@ const { escapeMarkdownTableCell } = require('./markdown-escape');
|
|
|
21
21
|
const ROOT = path.join(__dirname, '..');
|
|
22
22
|
const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf-8'));
|
|
23
23
|
|
|
24
|
+
function hasInstalledLanceDB() {
|
|
25
|
+
try {
|
|
26
|
+
require.resolve('@lancedb/lancedb');
|
|
27
|
+
return true;
|
|
28
|
+
} catch (_) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createInMemoryLanceLoader() {
|
|
34
|
+
const tables = new Map();
|
|
35
|
+
return async () => ({
|
|
36
|
+
connect: async () => ({
|
|
37
|
+
tableNames: async () => [...tables.keys()],
|
|
38
|
+
openTable: async (name) => {
|
|
39
|
+
const rows = tables.get(name) || [];
|
|
40
|
+
return {
|
|
41
|
+
add: async (records) => {
|
|
42
|
+
rows.push(...records);
|
|
43
|
+
tables.set(name, rows);
|
|
44
|
+
},
|
|
45
|
+
search: () => ({
|
|
46
|
+
limit: (limit) => ({
|
|
47
|
+
toArray: async () => rows.slice(0, limit),
|
|
48
|
+
}),
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
createTable: async (name, records) => {
|
|
53
|
+
tables.set(name, [...records]);
|
|
54
|
+
return {
|
|
55
|
+
add: async (more) => {
|
|
56
|
+
const rows = tables.get(name) || [];
|
|
57
|
+
rows.push(...more);
|
|
58
|
+
tables.set(name, rows);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
24
66
|
function ensureDir(dirPath) {
|
|
25
67
|
if (!fs.existsSync(dirPath)) {
|
|
26
68
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
@@ -33,6 +75,8 @@ function status(condition) {
|
|
|
33
75
|
|
|
34
76
|
async function runProof(options = {}) {
|
|
35
77
|
const proofDir = options.proofDir || process.env.THUMBGATE_PROOF_DIR || path.join(ROOT, 'proof');
|
|
78
|
+
const lanceInstalled = hasInstalledLanceDB();
|
|
79
|
+
const inMemoryLanceLoader = createInMemoryLanceLoader();
|
|
36
80
|
const report = {
|
|
37
81
|
phase: '04-lancedb-vector-storage',
|
|
38
82
|
generated: new Date().toISOString(),
|
|
@@ -60,7 +104,11 @@ async function runProof(options = {}) {
|
|
|
60
104
|
process.env.THUMBGATE_FEEDBACK_DIR = tmpDir;
|
|
61
105
|
process.env.THUMBGATE_VECTOR_STUB_EMBED = 'true';
|
|
62
106
|
|
|
63
|
-
const
|
|
107
|
+
const vectorStore = require('./vector-store');
|
|
108
|
+
if (!lanceInstalled) {
|
|
109
|
+
vectorStore.setLanceLoaderForTests(inMemoryLanceLoader);
|
|
110
|
+
}
|
|
111
|
+
const { upsertFeedback, searchSimilar } = vectorStore;
|
|
64
112
|
|
|
65
113
|
const event = {
|
|
66
114
|
id: 'proof-vec01',
|
|
@@ -84,7 +132,10 @@ async function runProof(options = {}) {
|
|
|
84
132
|
vec01Evidence =
|
|
85
133
|
`lancedb dir created at ${lanceDir}. ` +
|
|
86
134
|
`upsertFeedback() resolved, searchSimilar() returned ${results.length} result(s) ` +
|
|
87
|
-
`including proof-vec01. Table name: thumbgate_memories
|
|
135
|
+
`including proof-vec01. Table name: thumbgate_memories. ` +
|
|
136
|
+
(lanceInstalled
|
|
137
|
+
? 'Proof used installed @lancedb/lancedb runtime.'
|
|
138
|
+
: 'Proof used an in-memory LanceDB-compatible loader because @lancedb/lancedb is not installed in this environment.');
|
|
88
139
|
} else if (dirExists) {
|
|
89
140
|
vec01Status = 'fail';
|
|
90
141
|
vec01Evidence = `lancedb dir exists but searchSimilar() did not return proof-vec01. Got: ${JSON.stringify(results.map((r) => r.id))}`;
|
|
@@ -186,7 +237,11 @@ async function runProof(options = {}) {
|
|
|
186
237
|
process.env.THUMBGATE_FEEDBACK_DIR = tmpDir;
|
|
187
238
|
process.env.THUMBGATE_VECTOR_STUB_EMBED = 'true';
|
|
188
239
|
|
|
189
|
-
const
|
|
240
|
+
const vectorStore = require('./vector-store');
|
|
241
|
+
if (!lanceInstalled) {
|
|
242
|
+
vectorStore.setLanceLoaderForTests(inMemoryLanceLoader);
|
|
243
|
+
}
|
|
244
|
+
const { upsertFeedback: upsert2, searchSimilar: search2 } = vectorStore;
|
|
190
245
|
|
|
191
246
|
// Upsert a second distinct record
|
|
192
247
|
await upsert2({
|
|
@@ -209,7 +264,10 @@ async function runProof(options = {}) {
|
|
|
209
264
|
`proof-vec01 present: ${hasVec01}. proof-vec04-b present: ${hasVec04b}. ` +
|
|
210
265
|
`API: searchSimilar(queryText, limit=10) returns vector-ranked rows from thumbgate_memories table. ` +
|
|
211
266
|
`Note: stub embed (THUMBGATE_VECTOR_STUB_EMBED=true) returns identical 384-dim unit vectors — ` +
|
|
212
|
-
`ranking is insertion-order with stub, cosine similarity with real ONNX model
|
|
267
|
+
`ranking is insertion-order with stub, cosine similarity with real ONNX model. ` +
|
|
268
|
+
(lanceInstalled
|
|
269
|
+
? 'Vector table access used installed @lancedb/lancedb runtime.'
|
|
270
|
+
: 'Vector table access used an in-memory LanceDB-compatible loader because @lancedb/lancedb is not installed in this environment.');
|
|
213
271
|
} else {
|
|
214
272
|
vec04Status = 'fail';
|
|
215
273
|
vec04Evidence = `searchSimilar() returned 0 results after 2 upserts. Expected >= 1.`;
|
|
@@ -5,6 +5,14 @@ function normalizeBoolean(value) {
|
|
|
5
5
|
return String(value).trim().toLowerCase() === 'true';
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
function isPrereleaseVersion(version) {
|
|
9
|
+
return /^\d+\.\d+\.\d+-[0-9A-Za-z.-]+$/.test(String(version || '').trim());
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getNpmTag(version) {
|
|
13
|
+
return isPrereleaseVersion(version) ? 'next' : 'latest';
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
function decidePublishPlan(options) {
|
|
9
17
|
const currentSha = String(options.currentSha || '').trim();
|
|
10
18
|
const tagSha = String(options.tagSha || '').trim();
|
|
@@ -14,6 +22,7 @@ function decidePublishPlan(options) {
|
|
|
14
22
|
const published = normalizeBoolean(options.published);
|
|
15
23
|
const tagExists = normalizeBoolean(options.tagExists);
|
|
16
24
|
const tagMatchesCurrentCommit = tagExists && tagSha === currentSha;
|
|
25
|
+
const npmTag = getNpmTag(version);
|
|
17
26
|
|
|
18
27
|
if (!version) {
|
|
19
28
|
throw new Error('VERSION is required.');
|
|
@@ -51,6 +60,7 @@ function decidePublishPlan(options) {
|
|
|
51
60
|
reason: `Version ${version} is new. Create tag v${version}, publish to npm, and create a GitHub Release.`,
|
|
52
61
|
createTag: true,
|
|
53
62
|
publishNpm: true,
|
|
63
|
+
npmTag,
|
|
54
64
|
ensureRelease: true,
|
|
55
65
|
skipPublish: false,
|
|
56
66
|
tagMatchesCurrentCommit: false,
|
|
@@ -63,6 +73,7 @@ function decidePublishPlan(options) {
|
|
|
63
73
|
reason: `Tag v${version} already points at ${currentSha}. Resume npm publish without recreating the tag.`,
|
|
64
74
|
createTag: false,
|
|
65
75
|
publishNpm: true,
|
|
76
|
+
npmTag,
|
|
66
77
|
ensureRelease: true,
|
|
67
78
|
skipPublish: false,
|
|
68
79
|
tagMatchesCurrentCommit: true,
|
|
@@ -75,6 +86,7 @@ function decidePublishPlan(options) {
|
|
|
75
86
|
reason: `Version ${version} is already published from the current commit ${currentSha}.`,
|
|
76
87
|
createTag: false,
|
|
77
88
|
publishNpm: false,
|
|
89
|
+
npmTag,
|
|
78
90
|
ensureRelease: true,
|
|
79
91
|
skipPublish: true,
|
|
80
92
|
tagMatchesCurrentCommit: true,
|
|
@@ -86,6 +98,7 @@ function decidePublishPlan(options) {
|
|
|
86
98
|
reason: `Version ${version} is already published from commit ${tagSha}. Skip npm publish for this merge because package version did not change.`,
|
|
87
99
|
createTag: false,
|
|
88
100
|
publishNpm: false,
|
|
101
|
+
npmTag,
|
|
89
102
|
ensureRelease: false,
|
|
90
103
|
skipPublish: true,
|
|
91
104
|
tagMatchesCurrentCommit: false,
|
|
@@ -102,6 +115,7 @@ function writeGithubOutputs(plan, outputPath) {
|
|
|
102
115
|
`reason=${plan.reason}`,
|
|
103
116
|
`create_tag=${String(plan.createTag)}`,
|
|
104
117
|
`publish_npm=${String(plan.publishNpm)}`,
|
|
118
|
+
`npm_tag=${plan.npmTag}`,
|
|
105
119
|
`ensure_release=${String(plan.ensureRelease)}`,
|
|
106
120
|
`skip_publish=${String(plan.skipPublish)}`,
|
|
107
121
|
`tag_matches_current_commit=${String(plan.tagMatchesCurrentCommit)}`,
|
|
@@ -137,6 +151,8 @@ if (require.main === module) {
|
|
|
137
151
|
|
|
138
152
|
module.exports = {
|
|
139
153
|
decidePublishPlan,
|
|
154
|
+
getNpmTag,
|
|
155
|
+
isPrereleaseVersion,
|
|
140
156
|
normalizeBoolean,
|
|
141
157
|
runCli,
|
|
142
158
|
writeGithubOutputs,
|
|
@@ -8,10 +8,14 @@ const { appendDiagnosticRecord } = require('./feedback-loop');
|
|
|
8
8
|
|
|
9
9
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
10
10
|
const DEFAULT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
|
11
|
+
const DEFAULT_TESTS_TIMEOUT_MS = Number.parseInt(
|
|
12
|
+
process.env.THUMBGATE_SELF_HEAL_TEST_TIMEOUT_MS || '',
|
|
13
|
+
10,
|
|
14
|
+
) || 60 * 60_000;
|
|
11
15
|
|
|
12
16
|
const DEFAULT_CHECKS = [
|
|
13
17
|
{ name: 'budget_status', command: ['npm', 'run', 'budget:status'], timeoutMs: 60_000 },
|
|
14
|
-
{ name: 'tests', command: ['npm', 'test'], timeoutMs:
|
|
18
|
+
{ name: 'tests', command: ['npm', 'test'], timeoutMs: DEFAULT_TESTS_TIMEOUT_MS },
|
|
15
19
|
{ name: 'prove_adapters', command: ['npm', 'run', 'prove:adapters'], timeoutMs: 10 * 60_000, useTempProofDir: true },
|
|
16
20
|
{ name: 'prove_automation', command: ['npm', 'run', 'prove:automation'], timeoutMs: 10 * 60_000, useTempProofDir: true },
|
|
17
21
|
{ name: 'prove_data_pipeline', command: ['npm', 'run', 'prove:data-pipeline'], timeoutMs: 10 * 60_000, useTempProofDir: true },
|
|
@@ -177,6 +181,7 @@ function runCli() {
|
|
|
177
181
|
|
|
178
182
|
module.exports = {
|
|
179
183
|
DEFAULT_CHECKS,
|
|
184
|
+
DEFAULT_TESTS_TIMEOUT_MS,
|
|
180
185
|
DEFAULT_MAX_BUFFER_BYTES,
|
|
181
186
|
runCommand,
|
|
182
187
|
collectHealthReport,
|