thumbgate 1.2.0 → 1.3.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/README.md +4 -4
- 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 +35 -14
- 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 +2 -2
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +20 -11
- package/config/github-about.json +1 -1
- package/config/model-tiers.json +11 -0
- package/package.json +8 -6
- 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/cursor-marketplace/README.md +2 -2
- package/plugins/cursor-marketplace/commands/capture-feedback.md +2 -2
- package/plugins/cursor-marketplace/rules/feedback-capture.mdc +3 -3
- package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +3 -2
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/compare.html +4 -4
- package/public/guide.html +4 -4
- package/public/index.html +51 -38
- package/public/learn/ai-agent-persistent-memory.html +1 -0
- package/public/lessons.html +325 -17
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/audit-trail.js +6 -0
- package/scripts/capture-railway-diagnostics.sh +97 -0
- package/scripts/check-congruence.js +1 -1
- package/scripts/claude-feedback-sync.js +320 -0
- package/scripts/cli-telemetry.js +4 -1
- package/scripts/contextfs.js +32 -23
- package/scripts/dashboard.js +84 -0
- package/scripts/feedback-loop.js +16 -0
- package/scripts/intervention-policy.js +696 -0
- package/scripts/local-model-profile.js +18 -2
- package/scripts/model-tier-router.js +10 -1
- package/scripts/operational-integrity.js +354 -31
- package/scripts/prove-adapters.js +1 -0
- package/scripts/prove-automation.js +2 -2
- package/scripts/prove-packaged-runtime.js +260 -0
- package/scripts/prove-runtime.js +13 -0
- package/scripts/rate-limiter.js +3 -3
- package/scripts/statusline-local-stats.js +2 -0
- package/scripts/statusline.sh +166 -11
- package/scripts/tool-registry.js +2 -2
- package/scripts/workflow-sentinel.js +114 -4
- package/skills/thumbgate/SKILL.md +1 -1
|
@@ -22,6 +22,17 @@ const MODEL_ROLES = {
|
|
|
22
22
|
vlm: 'gemini-2.5-flash',
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
// GLM 5.1 open-source model IDs for self-hosted local inference.
|
|
26
|
+
// Activate by setting THUMBGATE_LOCAL_MODEL_FAMILY=glm-z1 (or any glm-* variant).
|
|
27
|
+
// Each role can still be overridden via THUMBGATE_MODEL_ROLE_<ROLE>.
|
|
28
|
+
const GLM_MODEL_ROLES = {
|
|
29
|
+
normal: 'glm-z1-9b',
|
|
30
|
+
thinking: 'glm-z1-32b',
|
|
31
|
+
critique: 'glm-z1-9b',
|
|
32
|
+
compaction: 'glm-4-9b',
|
|
33
|
+
vlm: 'glm-4v-9b',
|
|
34
|
+
};
|
|
35
|
+
|
|
25
36
|
const VALID_MODEL_ROLES = Object.keys(MODEL_ROLES);
|
|
26
37
|
|
|
27
38
|
const EMBEDDING_PROFILES = {
|
|
@@ -323,8 +334,12 @@ function resolveModelRole(role, env) {
|
|
|
323
334
|
throw new Error(`Unknown model role: '${normalized}'. Valid roles: ${VALID_MODEL_ROLES.join(', ')}`);
|
|
324
335
|
}
|
|
325
336
|
const envKey = `THUMBGATE_MODEL_ROLE_${normalized.toUpperCase()}`;
|
|
326
|
-
const
|
|
327
|
-
|
|
337
|
+
const modelFamily = resolveModelFamily(e);
|
|
338
|
+
const isLocalGlm = modelFamily.startsWith('glm');
|
|
339
|
+
const provider = isLocalGlm ? 'local' : 'gemini';
|
|
340
|
+
const defaultModel = isLocalGlm ? (GLM_MODEL_ROLES[normalized] || MODEL_ROLES[normalized]) : MODEL_ROLES[normalized];
|
|
341
|
+
const model = (e[envKey] && String(e[envKey]).trim()) || defaultModel;
|
|
342
|
+
return { role: normalized, model, provider, envKey };
|
|
328
343
|
}
|
|
329
344
|
|
|
330
345
|
function buildModelFitReport(options = {}) {
|
|
@@ -361,6 +376,7 @@ module.exports = {
|
|
|
361
376
|
DEFAULT_EMBED_MODEL,
|
|
362
377
|
DEFAULT_FEEDBACK_DIR,
|
|
363
378
|
EMBEDDING_PROFILES,
|
|
379
|
+
GLM_MODEL_ROLES,
|
|
364
380
|
INDEXCACHE_SERVER_ENGINES,
|
|
365
381
|
LONG_CONTEXT_TAGS,
|
|
366
382
|
LONG_CONTEXT_TASK_TYPES,
|
|
@@ -30,6 +30,8 @@ const TIERS = {
|
|
|
30
30
|
nano: { label: 'nano', costMultiplier: 0.1, maxContext: 32000 },
|
|
31
31
|
mini: { label: 'mini', costMultiplier: 0.4, maxContext: 200000 },
|
|
32
32
|
frontier: { label: 'frontier', costMultiplier: 1.0, maxContext: 1000000 },
|
|
33
|
+
// Self-hosted open-source frontier (e.g. GLM 5.1). Zero marginal cost.
|
|
34
|
+
localFrontier: { label: 'local-frontier', costMultiplier: 0.0, maxContext: 1000000 },
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
// ---------------------------------------------------------------------------
|
|
@@ -268,8 +270,15 @@ function recommendExecutionPlan(task = {}, env = process.env) {
|
|
|
268
270
|
const classification = classifyTask(task);
|
|
269
271
|
const inference = recommendInferenceBackend(task, env);
|
|
270
272
|
|
|
273
|
+
// When a local GLM backend is active, frontier tasks run at zero cost.
|
|
274
|
+
const isLocalGlm = inference.backend.providerMode === 'local'
|
|
275
|
+
&& inference.backend.modelFamily.startsWith('glm');
|
|
276
|
+
const effectiveTier = isLocalGlm && classification.tier === 'frontier'
|
|
277
|
+
? 'localFrontier'
|
|
278
|
+
: classification.tier;
|
|
279
|
+
|
|
271
280
|
return {
|
|
272
|
-
tier:
|
|
281
|
+
tier: effectiveTier,
|
|
273
282
|
escalated: classification.escalated,
|
|
274
283
|
tierReason: classification.reason,
|
|
275
284
|
backendId: inference.backend.id,
|
|
@@ -6,6 +6,12 @@ const path = require('path');
|
|
|
6
6
|
const { execFileSync, spawnSync } = require('child_process');
|
|
7
7
|
|
|
8
8
|
const DEFAULT_BASE_BRANCH = 'main';
|
|
9
|
+
const FIXED_GIT_BIN_CANDIDATES = [
|
|
10
|
+
'/usr/bin/git',
|
|
11
|
+
'/opt/homebrew/bin/git',
|
|
12
|
+
'/usr/local/bin/git',
|
|
13
|
+
];
|
|
14
|
+
const SEMVER_PATTERN = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/;
|
|
9
15
|
const DEFAULT_RELEASE_SENSITIVE_GLOBS = [
|
|
10
16
|
'package.json',
|
|
11
17
|
'package-lock.json',
|
|
@@ -22,6 +28,55 @@ const DEFAULT_RELEASE_SENSITIVE_GLOBS = [
|
|
|
22
28
|
'config/mcp-allowlists.json',
|
|
23
29
|
];
|
|
24
30
|
|
|
31
|
+
function canExecuteBinary(candidate) {
|
|
32
|
+
const normalized = String(candidate || '').trim();
|
|
33
|
+
if (!normalized) return false;
|
|
34
|
+
if (normalized.includes(path.sep)) {
|
|
35
|
+
try {
|
|
36
|
+
fs.accessSync(normalized, fs.constants.X_OK);
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const probe = spawnSync(normalized, ['--version'], {
|
|
44
|
+
encoding: 'utf8',
|
|
45
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
46
|
+
});
|
|
47
|
+
return !probe.error && probe.status === 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveGitBinary(options = {}) {
|
|
51
|
+
const env = options.env || process.env;
|
|
52
|
+
const allowPathLookup = options.allowPathLookup !== false;
|
|
53
|
+
const configuredCandidates = Array.isArray(options.candidates) && options.candidates.length > 0
|
|
54
|
+
? options.candidates
|
|
55
|
+
: [
|
|
56
|
+
env.THUMBGATE_GIT_BIN,
|
|
57
|
+
...FIXED_GIT_BIN_CANDIDATES,
|
|
58
|
+
allowPathLookup ? 'git' : null,
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
for (const candidate of configuredCandidates) {
|
|
62
|
+
if (!canExecuteBinary(candidate)) continue;
|
|
63
|
+
return String(candidate).trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getGitBinary(options = {}) {
|
|
70
|
+
const gitBin = GIT_BIN || resolveGitBinary(options);
|
|
71
|
+
const required = options.required !== false;
|
|
72
|
+
if (!gitBin && required) {
|
|
73
|
+
throw new Error('Git executable is unavailable in this runtime');
|
|
74
|
+
}
|
|
75
|
+
return gitBin;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const GIT_BIN = resolveGitBinary();
|
|
79
|
+
|
|
25
80
|
function normalizePosix(filePath) {
|
|
26
81
|
return String(filePath || '')
|
|
27
82
|
.replace(/\\/g, '/')
|
|
@@ -76,25 +131,270 @@ function matchesAnyGlob(filePath, globs) {
|
|
|
76
131
|
});
|
|
77
132
|
}
|
|
78
133
|
|
|
79
|
-
function
|
|
80
|
-
|
|
134
|
+
function isSafeGitRevision(revision) {
|
|
135
|
+
const normalized = String(revision || '').trim();
|
|
136
|
+
if (!normalized) return false;
|
|
137
|
+
if (normalized.startsWith('-')) return false;
|
|
138
|
+
if (normalized.includes('..') || normalized.includes('//') || normalized.includes('@{')) return false;
|
|
139
|
+
if (normalized.endsWith('.') || normalized.endsWith('/')) return false;
|
|
140
|
+
if (!/^[A-Za-z0-9._/-]+$/.test(normalized)) return false;
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isSafeGitObjectId(objectId) {
|
|
145
|
+
return /^[0-9a-f]{40}$/i.test(String(objectId || '').trim());
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function assertSafeGitObjectId(objectId, label = 'object id') {
|
|
149
|
+
const normalized = String(objectId || '').trim().toLowerCase();
|
|
150
|
+
if (!isSafeGitObjectId(normalized)) {
|
|
151
|
+
throw new Error(`Unsafe git ${label}: ${objectId}`);
|
|
152
|
+
}
|
|
153
|
+
return normalized;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function assertSafeGitRevision(revision, label = 'revision') {
|
|
157
|
+
const normalized = String(revision || '').trim();
|
|
158
|
+
if (!isSafeGitRevision(normalized)) {
|
|
159
|
+
throw new Error(`Unsafe git ${label}: ${revision}`);
|
|
160
|
+
}
|
|
161
|
+
return normalized;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function resolveGitDirEntry(repoRoot, gitEntryPath) {
|
|
165
|
+
let stat;
|
|
166
|
+
try {
|
|
167
|
+
stat = fs.statSync(gitEntryPath);
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (stat.isDirectory()) {
|
|
173
|
+
return gitEntryPath;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!stat.isFile()) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const pointer = fs.readFileSync(gitEntryPath, 'utf8').trim();
|
|
181
|
+
const separatorIndex = pointer.indexOf(':');
|
|
182
|
+
if (separatorIndex < 0 || pointer.slice(0, separatorIndex).trim().toLowerCase() !== 'gitdir') {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const gitDirValue = pointer.slice(separatorIndex + 1).trim();
|
|
187
|
+
if (!gitDirValue) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return path.resolve(repoRoot, gitDirValue);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function findGitRepoMetadata(repoPath) {
|
|
195
|
+
let currentPath = path.resolve(repoPath || process.cwd());
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
if (!fs.statSync(currentPath).isDirectory()) {
|
|
199
|
+
currentPath = path.dirname(currentPath);
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
currentPath = path.dirname(currentPath);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
while (true) {
|
|
206
|
+
const gitDir = resolveGitDirEntry(currentPath, path.join(currentPath, '.git'));
|
|
207
|
+
if (gitDir) {
|
|
208
|
+
return { repoRoot: currentPath, gitDir };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const parentPath = path.dirname(currentPath);
|
|
212
|
+
if (parentPath === currentPath) {
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
currentPath = parentPath;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
throw new Error(`Not a git repository: ${repoPath}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function gitShowTopLevel(repoPath) {
|
|
222
|
+
return findGitRepoMetadata(repoPath).repoRoot;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function gitDirPath(repoPath) {
|
|
226
|
+
return findGitRepoMetadata(repoPath).gitDir;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function readGitRefFile(gitDir, refName) {
|
|
230
|
+
const refSegments = refName?.split('/').filter(Boolean);
|
|
231
|
+
if (!Array.isArray(refSegments) || refSegments.length === 0) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const refPath = path.join(gitDir, ...refSegments);
|
|
236
|
+
try {
|
|
237
|
+
const value = fs.readFileSync(refPath, 'utf8').trim();
|
|
238
|
+
return isSafeGitObjectId(value) ? assertSafeGitObjectId(value) : null;
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function readPackedGitRef(gitDir, refName) {
|
|
245
|
+
try {
|
|
246
|
+
const packedRefs = fs.readFileSync(path.join(gitDir, 'packed-refs'), 'utf8');
|
|
247
|
+
for (const line of packedRefs.split('\n')) {
|
|
248
|
+
const trimmed = line.trim();
|
|
249
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('^')) continue;
|
|
250
|
+
const [objectId, name] = trimmed.split(/\s+/, 2);
|
|
251
|
+
if (name === refName && isSafeGitObjectId(objectId)) {
|
|
252
|
+
return assertSafeGitObjectId(objectId);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function readGitRefSha(gitDir, refName) {
|
|
262
|
+
if (!refName?.startsWith('refs/')) return null;
|
|
263
|
+
return readGitRefFile(gitDir, refName) || readPackedGitRef(gitDir, refName);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function readGitHeadState(repoPath) {
|
|
267
|
+
const gitDir = gitDirPath(repoPath);
|
|
268
|
+
const headValue = fs.readFileSync(path.join(gitDir, 'HEAD'), 'utf8').trim();
|
|
269
|
+
|
|
270
|
+
if (headValue.startsWith('ref:')) {
|
|
271
|
+
const refName = headValue.slice(4).trim();
|
|
272
|
+
return {
|
|
273
|
+
refName,
|
|
274
|
+
objectId: readGitRefSha(gitDir, refName),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (isSafeGitObjectId(headValue)) {
|
|
279
|
+
return {
|
|
280
|
+
refName: null,
|
|
281
|
+
objectId: assertSafeGitObjectId(headValue, 'HEAD sha'),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
refName: null,
|
|
287
|
+
objectId: null,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function resolveGitRevisionToCommitSha(repoPath, revision) {
|
|
292
|
+
const safeRevision = assertSafeGitRevision(revision, 'revision');
|
|
293
|
+
if (safeRevision === 'HEAD') {
|
|
294
|
+
return gitHeadSha(repoPath);
|
|
295
|
+
}
|
|
296
|
+
if (isSafeGitObjectId(safeRevision)) {
|
|
297
|
+
return safeRevision.toLowerCase();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const gitDir = path.resolve(repoPath, gitDirPath(repoPath));
|
|
301
|
+
if (safeRevision.startsWith('refs/')) {
|
|
302
|
+
return readGitRefSha(gitDir, safeRevision);
|
|
303
|
+
}
|
|
304
|
+
if (safeRevision.startsWith('origin/')) {
|
|
305
|
+
return readGitRefSha(gitDir, `refs/remotes/${safeRevision}`);
|
|
306
|
+
}
|
|
307
|
+
return readGitRefSha(gitDir, `refs/heads/${safeRevision}`) || readGitRefSha(gitDir, `refs/remotes/origin/${safeRevision}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function gitVerifyRef(repoPath, ref) {
|
|
311
|
+
const commitSha = resolveGitRevisionToCommitSha(repoPath, ref);
|
|
312
|
+
if (!commitSha) {
|
|
313
|
+
throw new Error(`Unknown git ref: ${ref}`);
|
|
314
|
+
}
|
|
315
|
+
return assertSafeGitObjectId(commitSha, 'commit sha');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function gitCurrentBranch(repoPath) {
|
|
319
|
+
const headState = readGitHeadState(repoPath);
|
|
320
|
+
if (headState.refName?.startsWith('refs/heads/')) {
|
|
321
|
+
return headState.refName.slice('refs/heads/'.length);
|
|
322
|
+
}
|
|
323
|
+
if (headState.refName?.startsWith('refs/remotes/')) {
|
|
324
|
+
return headState.refName.slice('refs/remotes/'.length);
|
|
325
|
+
}
|
|
326
|
+
if (headState.objectId) {
|
|
327
|
+
return 'HEAD';
|
|
328
|
+
}
|
|
329
|
+
throw new Error('Unable to resolve current branch');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function gitHeadSha(repoPath) {
|
|
333
|
+
const headState = readGitHeadState(repoPath);
|
|
334
|
+
if (headState.objectId) {
|
|
335
|
+
return headState.objectId;
|
|
336
|
+
}
|
|
337
|
+
throw new Error('Unable to resolve HEAD commit');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function assertSafeRepoRelativePath(filePath, label = 'path') {
|
|
341
|
+
const normalized = normalizePosix(filePath);
|
|
342
|
+
if (!normalized || normalized.startsWith('-') || normalized.startsWith('.git')) {
|
|
343
|
+
throw new Error(`Unsafe repo-relative ${label}: ${filePath}`);
|
|
344
|
+
}
|
|
345
|
+
if (normalized.includes('..') || normalized.includes('//')) {
|
|
346
|
+
throw new Error(`Unsafe repo-relative ${label}: ${filePath}`);
|
|
347
|
+
}
|
|
348
|
+
return normalized;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function gitReadBlobAtCommit(repoPath, commitSha, filePath) {
|
|
352
|
+
const safeCommitSha = assertSafeGitObjectId(commitSha, 'commit sha');
|
|
353
|
+
const safeFilePath = assertSafeRepoRelativePath(filePath, 'file path');
|
|
354
|
+
const treeEntry = execFileSync(getGitBinary(), ['ls-tree', safeCommitSha, '--', safeFilePath], {
|
|
355
|
+
cwd: repoPath,
|
|
356
|
+
encoding: 'utf8',
|
|
357
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
358
|
+
}).trim();
|
|
359
|
+
const match = /^\d+\s+blob\s+([0-9a-f]{40})\t/.exec(treeEntry);
|
|
360
|
+
if (!match?.[1]) {
|
|
361
|
+
throw new Error(`Unable to resolve blob for ${safeFilePath} at ${safeCommitSha}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const blobSha = assertSafeGitObjectId(match[1], 'blob sha');
|
|
365
|
+
return execFileSync(getGitBinary(), ['cat-file', 'blob', blobSha], {
|
|
366
|
+
cwd: repoPath,
|
|
367
|
+
encoding: 'utf8',
|
|
368
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function gitShowPackageJsonAtRef(repoPath, ref) {
|
|
373
|
+
const safeCommitSha = assertSafeGitObjectId(gitVerifyRef(repoPath, ref), 'commit sha');
|
|
374
|
+
return gitReadBlobAtCommit(repoPath, safeCommitSha, 'package.json').trim();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function gitDiffNameOnlyAgainstBase(repoPath, baseRef) {
|
|
378
|
+
const safeBaseCommitSha = assertSafeGitObjectId(gitVerifyRef(repoPath, baseRef), 'base commit sha');
|
|
379
|
+
return execFileSync(getGitBinary(), ['diff', '--name-only', `${safeBaseCommitSha}...HEAD`, '--'], {
|
|
81
380
|
cwd: repoPath,
|
|
82
381
|
encoding: 'utf8',
|
|
83
382
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
84
383
|
}).trim();
|
|
85
384
|
}
|
|
86
385
|
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
386
|
+
function gitMergeBaseIsAncestor(repoPath, commit, ref) {
|
|
387
|
+
const safeCommitSha = assertSafeGitObjectId(gitVerifyRef(repoPath, commit), 'ancestor commit sha');
|
|
388
|
+
const safeRefCommitSha = assertSafeGitObjectId(gitVerifyRef(repoPath, ref), 'descendant commit sha');
|
|
389
|
+
return spawnSync(getGitBinary(), ['merge-base', '--is-ancestor', safeCommitSha, safeRefCommitSha], {
|
|
390
|
+
cwd: repoPath,
|
|
391
|
+
encoding: 'utf8',
|
|
392
|
+
});
|
|
93
393
|
}
|
|
94
394
|
|
|
95
395
|
function resolveRepoRoot(repoPath = process.cwd()) {
|
|
96
396
|
try {
|
|
97
|
-
return
|
|
397
|
+
return gitShowTopLevel(repoPath);
|
|
98
398
|
} catch {
|
|
99
399
|
return null;
|
|
100
400
|
}
|
|
@@ -103,10 +403,7 @@ function resolveRepoRoot(repoPath = process.cwd()) {
|
|
|
103
403
|
function gitRefExists(repoPath, ref) {
|
|
104
404
|
if (!repoPath || !ref) return false;
|
|
105
405
|
try {
|
|
106
|
-
|
|
107
|
-
cwd: repoPath,
|
|
108
|
-
stdio: 'ignore',
|
|
109
|
-
});
|
|
406
|
+
gitVerifyRef(repoPath, ref);
|
|
110
407
|
return true;
|
|
111
408
|
} catch {
|
|
112
409
|
return false;
|
|
@@ -125,8 +422,10 @@ function isSafeBranchName(branchName) {
|
|
|
125
422
|
|
|
126
423
|
function fetchBaseBranch(repoPath, baseBranch) {
|
|
127
424
|
if (!repoPath || !isSafeBranchName(baseBranch)) return false;
|
|
425
|
+
const gitBin = getGitBinary({ required: false });
|
|
426
|
+
if (!gitBin) return false;
|
|
128
427
|
// Fetch the remote tracking refs without passing user-controlled branch names to git.
|
|
129
|
-
const result = spawnSync(
|
|
428
|
+
const result = spawnSync(gitBin, ['fetch', '--no-tags', '--depth=64', 'origin'], {
|
|
130
429
|
cwd: repoPath,
|
|
131
430
|
encoding: 'utf8',
|
|
132
431
|
});
|
|
@@ -152,11 +451,19 @@ function resolveBaseRef(repoPath, baseBranch = DEFAULT_BASE_BRANCH, { fetchIfMis
|
|
|
152
451
|
}
|
|
153
452
|
|
|
154
453
|
function getCurrentBranch(repoPath) {
|
|
155
|
-
|
|
454
|
+
try {
|
|
455
|
+
return gitCurrentBranch(repoPath) || null;
|
|
456
|
+
} catch {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
156
459
|
}
|
|
157
460
|
|
|
158
461
|
function getHeadSha(repoPath) {
|
|
159
|
-
|
|
462
|
+
try {
|
|
463
|
+
return gitHeadSha(repoPath) || null;
|
|
464
|
+
} catch {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
160
467
|
}
|
|
161
468
|
|
|
162
469
|
function readPackageVersion(repoPath, ref = 'HEAD') {
|
|
@@ -165,7 +472,7 @@ function readPackageVersion(repoPath, ref = 'HEAD') {
|
|
|
165
472
|
if (ref === 'HEAD') {
|
|
166
473
|
raw = fs.readFileSync(path.join(repoPath, 'package.json'), 'utf8');
|
|
167
474
|
} else {
|
|
168
|
-
raw =
|
|
475
|
+
raw = gitShowPackageJsonAtRef(repoPath, ref);
|
|
169
476
|
}
|
|
170
477
|
return JSON.parse(raw).version || null;
|
|
171
478
|
} catch {
|
|
@@ -174,7 +481,7 @@ function readPackageVersion(repoPath, ref = 'HEAD') {
|
|
|
174
481
|
}
|
|
175
482
|
|
|
176
483
|
function parseSemver(version) {
|
|
177
|
-
const match = String(version || '').trim()
|
|
484
|
+
const match = SEMVER_PATTERN.exec(String(version || '').trim());
|
|
178
485
|
if (!match) return null;
|
|
179
486
|
return {
|
|
180
487
|
major: Number(match[1]),
|
|
@@ -236,8 +543,12 @@ function compareSemver(left, right) {
|
|
|
236
543
|
function listChangedFilesAgainstBase(repoPath, baseBranch = DEFAULT_BASE_BRANCH, { fetchIfMissing = false } = {}) {
|
|
237
544
|
const baseRef = resolveBaseRef(repoPath, baseBranch, { fetchIfMissing });
|
|
238
545
|
if (!baseRef) return [];
|
|
239
|
-
|
|
240
|
-
|
|
546
|
+
try {
|
|
547
|
+
const diff = gitDiffNameOnlyAgainstBase(repoPath, baseRef);
|
|
548
|
+
return diff.split('\n').map((line) => normalizePosix(line)).filter(Boolean);
|
|
549
|
+
} catch {
|
|
550
|
+
return [];
|
|
551
|
+
}
|
|
241
552
|
}
|
|
242
553
|
|
|
243
554
|
function findReleaseSensitiveFiles(files, globs = DEFAULT_RELEASE_SENSITIVE_GLOBS) {
|
|
@@ -246,11 +557,12 @@ function findReleaseSensitiveFiles(files, globs = DEFAULT_RELEASE_SENSITIVE_GLOB
|
|
|
246
557
|
|
|
247
558
|
function isHeadReachableFrom(repoPath, ref, commit = 'HEAD') {
|
|
248
559
|
if (!repoPath || !ref) return false;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
|
|
560
|
+
try {
|
|
561
|
+
const result = gitMergeBaseIsAncestor(repoPath, commit, ref);
|
|
562
|
+
return result.status === 0;
|
|
563
|
+
} catch {
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
254
566
|
}
|
|
255
567
|
|
|
256
568
|
function runGh(args) {
|
|
@@ -304,6 +616,7 @@ function evaluateOperationalIntegrity(options = {}) {
|
|
|
304
616
|
: (repoRoot ? listChangedFilesAgainstBase(repoRoot, baseBranch, { fetchIfMissing: options.fetchBase === true }) : []);
|
|
305
617
|
const releaseSensitiveGlobs = sanitizeGlobList(options.releaseSensitiveGlobs || DEFAULT_RELEASE_SENSITIVE_GLOBS);
|
|
306
618
|
const releaseSensitiveFiles = findReleaseSensitiveFiles(changedFiles, releaseSensitiveGlobs);
|
|
619
|
+
const hasReleaseSensitiveFiles = releaseSensitiveFiles.length > 0;
|
|
307
620
|
const packageVersion = options.packageVersion !== undefined
|
|
308
621
|
? options.packageVersion
|
|
309
622
|
: (repoRoot ? readPackageVersion(repoRoot, 'HEAD') : null);
|
|
@@ -383,7 +696,7 @@ function evaluateOperationalIntegrity(options = {}) {
|
|
|
383
696
|
}
|
|
384
697
|
}
|
|
385
698
|
|
|
386
|
-
if (options.requirePrForReleaseSensitive &&
|
|
699
|
+
if (options.requirePrForReleaseSensitive && hasReleaseSensitiveFiles && currentBranch && currentBranch !== baseBranch && !openPr) {
|
|
387
700
|
blockers.push(buildBlocker(
|
|
388
701
|
'release_sensitive_changes_require_pr',
|
|
389
702
|
`Release-sensitive changes on ${currentBranch} require an open pull request before continuing.`,
|
|
@@ -391,7 +704,7 @@ function evaluateOperationalIntegrity(options = {}) {
|
|
|
391
704
|
));
|
|
392
705
|
}
|
|
393
706
|
|
|
394
|
-
if (options.requireVersionNotBehindBase &&
|
|
707
|
+
if (options.requireVersionNotBehindBase && hasReleaseSensitiveFiles && versionComparison !== null && versionComparison < 0) {
|
|
395
708
|
blockers.push(buildBlocker(
|
|
396
709
|
'version_behind_base',
|
|
397
710
|
`package.json version ${packageVersion} is behind ${baseBranch} version ${baseVersion} while release-sensitive files changed.`,
|
|
@@ -474,6 +787,8 @@ function runCli(env = process.env, argv = process.argv.slice(2)) {
|
|
|
474
787
|
});
|
|
475
788
|
|
|
476
789
|
const lines = [];
|
|
790
|
+
const hasReleaseSensitiveFiles = Array.isArray(result.releaseSensitiveFiles) && result.releaseSensitiveFiles.length > 0;
|
|
791
|
+
const openPrNumber = result.openPr?.number;
|
|
477
792
|
lines.push(`Operational integrity: ${result.ok ? 'ok' : 'blocked'}`);
|
|
478
793
|
lines.push(`Base branch: ${result.baseBranch}`);
|
|
479
794
|
lines.push(`Current branch: ${result.currentBranch || 'unknown'}`);
|
|
@@ -483,11 +798,12 @@ function runCli(env = process.env, argv = process.argv.slice(2)) {
|
|
|
483
798
|
if (result.baseVersion) {
|
|
484
799
|
lines.push(`${result.baseBranch} version: ${result.baseVersion}`);
|
|
485
800
|
}
|
|
486
|
-
if (
|
|
801
|
+
if (hasReleaseSensitiveFiles) {
|
|
487
802
|
lines.push(`Release-sensitive files: ${result.releaseSensitiveFiles.join(', ')}`);
|
|
488
803
|
}
|
|
489
|
-
if (
|
|
490
|
-
|
|
804
|
+
if (openPrNumber) {
|
|
805
|
+
const openPrSuffix = result.openPr?.url ? ` ${result.openPr.url}` : '';
|
|
806
|
+
lines.push(`Open PR: #${openPrNumber}${openPrSuffix}`);
|
|
491
807
|
}
|
|
492
808
|
for (const blocker of result.blockers) {
|
|
493
809
|
lines.push(`BLOCKER ${blocker.code}: ${blocker.message}`);
|
|
@@ -502,7 +818,7 @@ function runCli(env = process.env, argv = process.argv.slice(2)) {
|
|
|
502
818
|
return result.ok ? 0 : 1;
|
|
503
819
|
}
|
|
504
820
|
|
|
505
|
-
if (
|
|
821
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
|
|
506
822
|
process.exitCode = runCli();
|
|
507
823
|
}
|
|
508
824
|
|
|
@@ -515,12 +831,19 @@ module.exports = {
|
|
|
515
831
|
findOpenPrForBranch,
|
|
516
832
|
findReleaseSensitiveFiles,
|
|
517
833
|
getCurrentBranch,
|
|
834
|
+
getGitBinary,
|
|
835
|
+
assertSafeGitObjectId,
|
|
836
|
+
gitVerifyRef,
|
|
518
837
|
isSafeBranchName,
|
|
838
|
+
isSafeGitObjectId,
|
|
839
|
+
isSafeGitRevision,
|
|
840
|
+
isHeadReachableFrom,
|
|
519
841
|
listChangedFilesAgainstBase,
|
|
520
842
|
normalizeGlob,
|
|
521
843
|
normalizePosix,
|
|
522
844
|
parseSemver,
|
|
523
845
|
readPackageVersion,
|
|
846
|
+
resolveGitBinary,
|
|
524
847
|
resolveBaseRef,
|
|
525
848
|
resolveCiBranchName,
|
|
526
849
|
resolveRepoRoot,
|
|
@@ -41,6 +41,7 @@ function initGitRepo() {
|
|
|
41
41
|
execFileSync('git', ['init', '-b', 'main'], { cwd: repoPath, stdio: 'ignore' });
|
|
42
42
|
execFileSync('git', ['config', 'user.name', 'ThumbGate Proof'], { cwd: repoPath, stdio: 'ignore' });
|
|
43
43
|
execFileSync('git', ['config', 'user.email', 'proof@example.com'], { cwd: repoPath, stdio: 'ignore' });
|
|
44
|
+
execFileSync('git', ['config', 'commit.gpgsign', 'false'], { cwd: repoPath, stdio: 'ignore' });
|
|
44
45
|
fs.writeFileSync(path.join(repoPath, 'README.md'), '# proof repo\n');
|
|
45
46
|
execFileSync('git', ['add', 'README.md'], { cwd: repoPath, stdio: 'ignore' });
|
|
46
47
|
execFileSync('git', ['commit', '-m', 'init'], { cwd: repoPath, stdio: 'ignore' });
|
|
@@ -17,7 +17,7 @@ const { startServer } = require('../src/api/server');
|
|
|
17
17
|
const { handleRequest } = require('../adapters/mcp/server-stdio');
|
|
18
18
|
const { collectHealthReport } = require('./self-healing-check');
|
|
19
19
|
const { runSelfHeal } = require('./self-heal');
|
|
20
|
-
const {
|
|
20
|
+
const { getContextFsRoot, NAMESPACES } = require('./contextfs');
|
|
21
21
|
const { traceForProofCheck, aggregateTraces } = require('./code-reasoning');
|
|
22
22
|
const { runVerificationLoop } = require('./verification-loop');
|
|
23
23
|
const { run: runGateCheck } = require('./gates-engine');
|
|
@@ -513,7 +513,7 @@ async function runAutomationProof(options = {}) {
|
|
|
513
513
|
// 19) semantic cache hit on equivalent query
|
|
514
514
|
{
|
|
515
515
|
currentCheck = 'context.semantic_cache.hit.first';
|
|
516
|
-
fs.rmSync(path.join(
|
|
516
|
+
fs.rmSync(path.join(getContextFsRoot(), NAMESPACES.provenance, 'semantic-cache.jsonl'), { force: true });
|
|
517
517
|
const first = await fetchWithRetry(`${baseUrl}/v1/context/construct`, {
|
|
518
518
|
method: 'POST',
|
|
519
519
|
headers: {
|