thumbgate 1.1.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.
Files changed (63) hide show
  1. package/.claude-plugin/README.md +4 -4
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +48 -16
  6. package/adapters/README.md +1 -1
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/codex/config.toml +2 -2
  9. package/adapters/mcp/server-stdio.js +11 -8
  10. package/adapters/opencode/opencode.json +1 -1
  11. package/bin/cli.js +20 -11
  12. package/config/github-about.json +1 -1
  13. package/config/model-tiers.json +11 -0
  14. package/package.json +22 -11
  15. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  16. package/plugins/claude-codex-bridge/.mcp.json +1 -1
  17. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  18. package/plugins/codex-profile/.mcp.json +1 -1
  19. package/plugins/codex-profile/INSTALL.md +1 -1
  20. package/plugins/codex-profile/README.md +1 -1
  21. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  22. package/plugins/cursor-marketplace/README.md +2 -2
  23. package/plugins/cursor-marketplace/commands/capture-feedback.md +2 -2
  24. package/plugins/cursor-marketplace/rules/feedback-capture.mdc +3 -3
  25. package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +3 -2
  26. package/plugins/opencode-profile/INSTALL.md +1 -1
  27. package/public/compare.html +302 -0
  28. package/public/guide.html +4 -4
  29. package/public/index.html +77 -38
  30. package/public/learn/ai-agent-persistent-memory.html +1 -0
  31. package/public/lessons.html +325 -17
  32. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  33. package/scripts/ai-search-visibility.js +142 -0
  34. package/scripts/audit-trail.js +6 -0
  35. package/scripts/capture-railway-diagnostics.sh +97 -0
  36. package/scripts/changeset-check.js +372 -0
  37. package/scripts/check-congruence.js +8 -5
  38. package/scripts/claude-feedback-sync.js +320 -0
  39. package/scripts/cli-telemetry.js +4 -1
  40. package/scripts/computer-use-firewall.js +45 -15
  41. package/scripts/contextfs.js +32 -23
  42. package/scripts/dashboard.js +84 -0
  43. package/scripts/docker-sandbox-planner.js +208 -0
  44. package/scripts/feedback-loop.js +16 -0
  45. package/scripts/github-about.js +56 -0
  46. package/scripts/intervention-policy.js +696 -0
  47. package/scripts/local-model-profile.js +18 -2
  48. package/scripts/model-tier-router.js +10 -1
  49. package/scripts/operational-integrity.js +361 -32
  50. package/scripts/prove-adapters.js +1 -0
  51. package/scripts/prove-automation.js +2 -2
  52. package/scripts/prove-packaged-runtime.js +260 -0
  53. package/scripts/prove-runtime.js +13 -0
  54. package/scripts/published-cli.js +10 -1
  55. package/scripts/rate-limiter.js +3 -3
  56. package/scripts/statusline-links.js +238 -0
  57. package/scripts/statusline-local-stats.js +2 -0
  58. package/scripts/statusline.sh +200 -10
  59. package/scripts/sync-github-about.js +7 -4
  60. package/scripts/tool-registry.js +2 -2
  61. package/scripts/workflow-sentinel.js +197 -39
  62. package/skills/thumbgate/SKILL.md +1 -1
  63. package/src/api/server.js +12 -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 model = (e[envKey] && String(e[envKey]).trim()) || MODEL_ROLES[normalized];
327
- return { role: normalized, model, provider: 'gemini', envKey };
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: classification.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 runGit(repoPath, args) {
80
- return execFileSync('git', args, {
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 tryRunGit(repoPath, args) {
88
- try {
89
- return runGit(repoPath, args);
90
- } catch {
91
- return '';
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 runGit(repoPath, ['rev-parse', '--show-toplevel']);
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
- execFileSync('git', ['rev-parse', '--verify', ref], {
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('git', ['fetch', '--no-tags', '--depth=64', 'origin'], {
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
- return tryRunGit(repoPath, ['rev-parse', '--abbrev-ref', 'HEAD']) || null;
454
+ try {
455
+ return gitCurrentBranch(repoPath) || null;
456
+ } catch {
457
+ return null;
458
+ }
156
459
  }
157
460
 
158
461
  function getHeadSha(repoPath) {
159
- return tryRunGit(repoPath, ['rev-parse', 'HEAD']) || null;
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 = runGit(repoPath, ['show', `${ref}:package.json`]);
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().match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/);
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
- const diff = tryRunGit(repoPath, ['diff', '--name-only', `${baseRef}...HEAD`]);
240
- return diff.split('\n').map((line) => normalizePosix(line)).filter(Boolean);
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
- const result = spawnSync('git', ['merge-base', '--is-ancestor', commit, ref], {
250
- cwd: repoPath,
251
- encoding: 'utf8',
252
- });
253
- return result.status === 0;
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 && releaseSensitiveFiles.length > 0 && currentBranch && currentBranch !== baseBranch && !openPr) {
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 && releaseSensitiveFiles.length > 0 && versionComparison !== null && versionComparison < 0) {
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.`,
@@ -457,18 +770,25 @@ function parseCliArgs(argv = process.argv.slice(2)) {
457
770
  return options;
458
771
  }
459
772
 
773
+ function resolveCiBranchName(env = process.env) {
774
+ const branchName = String(env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME || '').trim();
775
+ return branchName || undefined;
776
+ }
777
+
460
778
  function runCli(env = process.env, argv = process.argv.slice(2)) {
461
779
  const args = parseCliArgs(argv);
462
780
  const result = evaluateOperationalIntegrity({
463
781
  repoPath: args.repoPath,
464
782
  baseBranch: args.baseBranch || env.DEFAULT_BRANCH || DEFAULT_BASE_BRANCH,
465
- currentBranch: env.GITHUB_REF_NAME || undefined,
783
+ currentBranch: resolveCiBranchName(env),
466
784
  requirePrForReleaseSensitive: args.requirePrForReleaseSensitive,
467
785
  requireVersionNotBehindBase: args.requireVersionNotBehindBase,
468
786
  fetchBase: args.fetchBase,
469
787
  });
470
788
 
471
789
  const lines = [];
790
+ const hasReleaseSensitiveFiles = Array.isArray(result.releaseSensitiveFiles) && result.releaseSensitiveFiles.length > 0;
791
+ const openPrNumber = result.openPr?.number;
472
792
  lines.push(`Operational integrity: ${result.ok ? 'ok' : 'blocked'}`);
473
793
  lines.push(`Base branch: ${result.baseBranch}`);
474
794
  lines.push(`Current branch: ${result.currentBranch || 'unknown'}`);
@@ -478,11 +798,12 @@ function runCli(env = process.env, argv = process.argv.slice(2)) {
478
798
  if (result.baseVersion) {
479
799
  lines.push(`${result.baseBranch} version: ${result.baseVersion}`);
480
800
  }
481
- if (result.releaseSensitiveFiles.length > 0) {
801
+ if (hasReleaseSensitiveFiles) {
482
802
  lines.push(`Release-sensitive files: ${result.releaseSensitiveFiles.join(', ')}`);
483
803
  }
484
- if (result.openPr && result.openPr.number) {
485
- lines.push(`Open PR: #${result.openPr.number}${result.openPr.url ? ` ${result.openPr.url}` : ''}`);
804
+ if (openPrNumber) {
805
+ const openPrSuffix = result.openPr?.url ? ` ${result.openPr.url}` : '';
806
+ lines.push(`Open PR: #${openPrNumber}${openPrSuffix}`);
486
807
  }
487
808
  for (const blocker of result.blockers) {
488
809
  lines.push(`BLOCKER ${blocker.code}: ${blocker.message}`);
@@ -497,7 +818,7 @@ function runCli(env = process.env, argv = process.argv.slice(2)) {
497
818
  return result.ok ? 0 : 1;
498
819
  }
499
820
 
500
- if (require.main === module) {
821
+ if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
501
822
  process.exitCode = runCli();
502
823
  }
503
824
 
@@ -510,13 +831,21 @@ module.exports = {
510
831
  findOpenPrForBranch,
511
832
  findReleaseSensitiveFiles,
512
833
  getCurrentBranch,
834
+ getGitBinary,
835
+ assertSafeGitObjectId,
836
+ gitVerifyRef,
513
837
  isSafeBranchName,
838
+ isSafeGitObjectId,
839
+ isSafeGitRevision,
840
+ isHeadReachableFrom,
514
841
  listChangedFilesAgainstBase,
515
842
  normalizeGlob,
516
843
  normalizePosix,
517
844
  parseSemver,
518
845
  readPackageVersion,
846
+ resolveGitBinary,
519
847
  resolveBaseRef,
848
+ resolveCiBranchName,
520
849
  resolveRepoRoot,
521
850
  runCli,
522
851
  sanitizeGlobList,
@@ -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 { CONTEXTFS_ROOT, NAMESPACES } = require('./contextfs');
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(CONTEXTFS_ROOT, NAMESPACES.provenance, 'semantic-cache.jsonl'), { force: true });
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: {