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.
Files changed (70) 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 +6 -3
  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 +53 -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 +29 -5
  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 +62 -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/feedback-history-distiller.js +7 -1
  48. package/scripts/feedback-loop.js +10 -4
  49. package/scripts/feedback-paths.js +142 -10
  50. package/scripts/feedback-root-consolidator.js +18 -4
  51. package/scripts/gates-engine.js +96 -10
  52. package/scripts/hook-auto-capture.sh +1 -1
  53. package/scripts/hosted-job-launcher.js +260 -0
  54. package/scripts/managed-dpo-export.js +91 -0
  55. package/scripts/obsidian-export.js +0 -1
  56. package/scripts/operational-integrity.js +50 -7
  57. package/scripts/post-everywhere.js +10 -0
  58. package/scripts/prove-lancedb.js +62 -4
  59. package/scripts/publish-decision.js +16 -0
  60. package/scripts/self-healing-check.js +6 -1
  61. package/scripts/seo-gsd.js +217 -4
  62. package/scripts/social-analytics/load-env.js +33 -2
  63. package/scripts/social-analytics/store.js +200 -2
  64. package/scripts/statusline-cache-path.js +9 -6
  65. package/scripts/sync-version.js +18 -11
  66. package/scripts/tool-registry.js +37 -0
  67. package/scripts/train_from_feedback.py +0 -4
  68. package/scripts/workflow-sentinel.js +793 -0
  69. package/src/api/server.js +297 -38
  70. /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 match.slice(1).map((part) => Number(part));
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
- for (let i = 0; i < 3; i += 1) {
187
- if (a[i] > b[i]) return 1;
188
- if (a[i] < b[i]) return -1;
189
- }
190
- return 0;
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}`);
@@ -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 { upsertFeedback, searchSimilar } = require('./vector-store');
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 { upsertFeedback: upsert2, searchSimilar: search2 } = require('./vector-store');
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: 15 * 60_000 },
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,