thumbgate 1.2.0 → 1.4.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 (160) hide show
  1. package/.claude-plugin/README.md +4 -4
  2. package/.claude-plugin/marketplace.json +32 -13
  3. package/.claude-plugin/plugin.json +15 -2
  4. package/.well-known/llms.txt +60 -0
  5. package/.well-known/mcp/server-card.json +1 -1
  6. package/README.md +133 -23
  7. package/adapters/README.md +1 -1
  8. package/adapters/chatgpt/openapi.yaml +168 -0
  9. package/adapters/claude/.mcp.json +2 -2
  10. package/adapters/codex/config.toml +2 -2
  11. package/adapters/mcp/server-stdio.js +85 -2
  12. package/adapters/opencode/opencode.json +1 -1
  13. package/bin/cli.js +215 -19
  14. package/bin/postinstall.js +8 -2
  15. package/config/budget.json +18 -0
  16. package/config/gates/code-edit.json +61 -0
  17. package/config/gates/db-write.json +61 -0
  18. package/config/gates/default.json +154 -3
  19. package/config/gates/deploy.json +61 -0
  20. package/config/github-about.json +2 -1
  21. package/config/merge-quality-checks.json +23 -0
  22. package/config/model-tiers.json +11 -0
  23. package/openapi/openapi.yaml +168 -0
  24. package/package.json +47 -13
  25. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  26. package/plugins/claude-codex-bridge/.mcp.json +1 -1
  27. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
  28. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  29. package/plugins/codex-profile/.mcp.json +1 -1
  30. package/plugins/codex-profile/INSTALL.md +27 -4
  31. package/plugins/codex-profile/README.md +33 -9
  32. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  33. package/plugins/cursor-marketplace/README.md +2 -2
  34. package/plugins/cursor-marketplace/commands/capture-feedback.md +2 -2
  35. package/plugins/cursor-marketplace/rules/feedback-capture.mdc +3 -3
  36. package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +3 -2
  37. package/plugins/opencode-profile/INSTALL.md +1 -1
  38. package/public/blog.html +73 -0
  39. package/public/compare/mem0.html +189 -0
  40. package/public/compare/speclock.html +180 -0
  41. package/public/compare.html +12 -4
  42. package/public/guide.html +5 -5
  43. package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
  44. package/public/guides/codex-cli-guardrails.html +158 -0
  45. package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
  46. package/public/guides/pre-action-gates.html +162 -0
  47. package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
  48. package/public/index.html +169 -70
  49. package/public/learn/ai-agent-persistent-memory.html +1 -0
  50. package/public/lessons.html +334 -17
  51. package/public/llm-context.md +140 -0
  52. package/public/pro.html +24 -22
  53. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  54. package/scripts/access-anomaly-detector.js +1 -1
  55. package/scripts/adk-consolidator.js +1 -5
  56. package/scripts/agent-security-hardening.js +4 -6
  57. package/scripts/agentic-data-pipeline.js +1 -3
  58. package/scripts/async-job-runner.js +1 -5
  59. package/scripts/audit-trail.js +7 -5
  60. package/scripts/background-agent-governance.js +2 -10
  61. package/scripts/billing.js +2 -16
  62. package/scripts/budget-enforcer.js +173 -0
  63. package/scripts/build-codex-plugin.js +152 -0
  64. package/scripts/capture-railway-diagnostics.sh +97 -0
  65. package/scripts/check-congruence.js +133 -15
  66. package/scripts/claude-feedback-sync.js +320 -0
  67. package/scripts/cli-telemetry.js +4 -1
  68. package/scripts/commercial-offer.js +5 -7
  69. package/scripts/content-engine/linkedin-content-generator.js +154 -0
  70. package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
  71. package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
  72. package/scripts/content-engine/reddit-thread-finder.js +154 -0
  73. package/scripts/context-engine.js +21 -6
  74. package/scripts/contextfs.js +33 -44
  75. package/scripts/dashboard.js +104 -0
  76. package/scripts/decision-journal.js +341 -0
  77. package/scripts/delegation-runtime.js +1 -5
  78. package/scripts/distribution-surfaces.js +26 -0
  79. package/scripts/document-intake.js +927 -0
  80. package/scripts/ephemeral-agent-store.js +1 -8
  81. package/scripts/evolution-state.js +1 -5
  82. package/scripts/experiment-tracker.js +1 -5
  83. package/scripts/export-databricks-bundle.js +1 -5
  84. package/scripts/export-hf-dataset.js +1 -5
  85. package/scripts/export-training.js +1 -5
  86. package/scripts/feedback-attribution.js +1 -16
  87. package/scripts/feedback-history-distiller.js +1 -16
  88. package/scripts/feedback-loop.js +17 -5
  89. package/scripts/feedback-root-consolidator.js +2 -21
  90. package/scripts/feedback-session.js +49 -0
  91. package/scripts/feedback-to-rules.js +188 -28
  92. package/scripts/filesystem-search.js +1 -9
  93. package/scripts/fs-utils.js +104 -0
  94. package/scripts/gates-engine.js +149 -4
  95. package/scripts/github-about.js +32 -8
  96. package/scripts/gtm-revenue-loop.js +1 -5
  97. package/scripts/harness-selector.js +148 -0
  98. package/scripts/hosted-job-launcher.js +1 -5
  99. package/scripts/hybrid-feedback-context.js +7 -33
  100. package/scripts/intervention-policy.js +753 -0
  101. package/scripts/lesson-db.js +3 -18
  102. package/scripts/lesson-inference.js +194 -16
  103. package/scripts/lesson-retrieval.js +60 -24
  104. package/scripts/llm-client.js +59 -0
  105. package/scripts/local-model-profile.js +18 -2
  106. package/scripts/managed-lesson-agent.js +183 -0
  107. package/scripts/marketing-experiment.js +8 -22
  108. package/scripts/meta-agent-loop.js +624 -0
  109. package/scripts/metered-billing.js +1 -1
  110. package/scripts/model-tier-router.js +10 -1
  111. package/scripts/money-watcher.js +1 -4
  112. package/scripts/obsidian-export.js +1 -5
  113. package/scripts/operational-integrity.js +369 -34
  114. package/scripts/org-dashboard.js +6 -1
  115. package/scripts/per-step-scoring.js +2 -4
  116. package/scripts/pr-manager.js +201 -19
  117. package/scripts/pro-features.js +3 -2
  118. package/scripts/prompt-dlp.js +3 -3
  119. package/scripts/prove-adapters.js +2 -5
  120. package/scripts/prove-attribution.js +1 -5
  121. package/scripts/prove-automation.js +3 -5
  122. package/scripts/prove-cloudflare-sandbox.js +1 -3
  123. package/scripts/prove-data-pipeline.js +1 -3
  124. package/scripts/prove-intelligence.js +1 -3
  125. package/scripts/prove-lancedb.js +1 -5
  126. package/scripts/prove-local-intelligence.js +1 -3
  127. package/scripts/prove-packaged-runtime.js +326 -0
  128. package/scripts/prove-predictive-insights.js +1 -3
  129. package/scripts/prove-runtime.js +13 -0
  130. package/scripts/prove-training-export.js +1 -3
  131. package/scripts/prove-workflow-contract.js +1 -5
  132. package/scripts/rate-limiter.js +6 -4
  133. package/scripts/reddit-dm-outreach.js +14 -4
  134. package/scripts/schedule-manager.js +3 -5
  135. package/scripts/security-scanner.js +448 -0
  136. package/scripts/self-distill-agent.js +579 -0
  137. package/scripts/semantic-dedup.js +115 -0
  138. package/scripts/skill-exporter.js +1 -3
  139. package/scripts/skill-generator.js +1 -5
  140. package/scripts/social-analytics/engagement-audit.js +1 -18
  141. package/scripts/social-analytics/pollers/linkedin.js +26 -16
  142. package/scripts/social-analytics/publishers/linkedin.js +1 -1
  143. package/scripts/social-analytics/publishers/zernio.js +51 -0
  144. package/scripts/social-pipeline.js +1 -3
  145. package/scripts/social-post-hourly.js +47 -4
  146. package/scripts/statusline-links.js +6 -5
  147. package/scripts/statusline-local-stats.js +2 -0
  148. package/scripts/statusline.sh +38 -7
  149. package/scripts/sync-branch-protection.js +340 -0
  150. package/scripts/tessl-export.js +1 -3
  151. package/scripts/thumbgate-search.js +32 -1
  152. package/scripts/tool-kpi-tracker.js +1 -1
  153. package/scripts/tool-registry.js +108 -4
  154. package/scripts/vector-store.js +1 -5
  155. package/scripts/weekly-auto-post.js +1 -1
  156. package/scripts/workflow-sentinel.js +205 -4
  157. package/skills/thumbgate/SKILL.md +2 -2
  158. package/src/api/server.js +273 -4
  159. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  160. /package/scripts/social-analytics/db/{social-analytics.db-wal → analytics.sqlite} +0 -0
@@ -2,16 +2,12 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const { ensureDir } = require('./fs-utils');
5
6
 
6
7
  // ---------------------------------------------------------------------------
7
8
  // Helpers
8
9
  // ---------------------------------------------------------------------------
9
10
 
10
- function ensureDir(dirPath) {
11
- if (!fs.existsSync(dirPath)) {
12
- fs.mkdirSync(dirPath, { recursive: true });
13
- }
14
- }
15
11
 
16
12
  function readJSONL(filePath) {
17
13
  if (!fs.existsSync(filePath)) return [];
@@ -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) {
@@ -260,13 +572,24 @@ function runGh(args) {
260
572
  });
261
573
  }
262
574
 
263
- function findOpenPrForBranch({ branchName, runner = runGh } = {}) {
575
+ function resolveGitHubRepository(env = process.env) {
576
+ const repository = String(env.GITHUB_REPOSITORY || '').trim();
577
+ return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository) ? repository : null;
578
+ }
579
+
580
+ function findOpenPrForBranch({ branchName, runner = runGh, env = process.env } = {}) {
264
581
  const normalizedBranch = String(branchName || '').trim();
265
582
  if (!normalizedBranch) return null;
266
- if (!process.env.GH_TOKEN && !process.env.GITHUB_TOKEN) {
583
+ if (!env.GH_TOKEN && !env.GITHUB_TOKEN) {
267
584
  return null;
268
585
  }
269
- const result = runner(['pr', 'list', '--head', normalizedBranch, '--state', 'open', '--json', 'number,state,isDraft,url']);
586
+ const args = ['pr', 'list'];
587
+ const repository = resolveGitHubRepository(env);
588
+ if (repository) {
589
+ args.push('--repo', repository);
590
+ }
591
+ args.push('--head', normalizedBranch, '--state', 'open', '--json', 'number,state,isDraft,url');
592
+ const result = runner(args);
270
593
  if (!result || result.status !== 0) {
271
594
  return null;
272
595
  }
@@ -304,6 +627,7 @@ function evaluateOperationalIntegrity(options = {}) {
304
627
  : (repoRoot ? listChangedFilesAgainstBase(repoRoot, baseBranch, { fetchIfMissing: options.fetchBase === true }) : []);
305
628
  const releaseSensitiveGlobs = sanitizeGlobList(options.releaseSensitiveGlobs || DEFAULT_RELEASE_SENSITIVE_GLOBS);
306
629
  const releaseSensitiveFiles = findReleaseSensitiveFiles(changedFiles, releaseSensitiveGlobs);
630
+ const hasReleaseSensitiveFiles = releaseSensitiveFiles.length > 0;
307
631
  const packageVersion = options.packageVersion !== undefined
308
632
  ? options.packageVersion
309
633
  : (repoRoot ? readPackageVersion(repoRoot, 'HEAD') : null);
@@ -383,7 +707,7 @@ function evaluateOperationalIntegrity(options = {}) {
383
707
  }
384
708
  }
385
709
 
386
- if (options.requirePrForReleaseSensitive && releaseSensitiveFiles.length > 0 && currentBranch && currentBranch !== baseBranch && !openPr) {
710
+ if (options.requirePrForReleaseSensitive && hasReleaseSensitiveFiles && currentBranch && currentBranch !== baseBranch && !openPr) {
387
711
  blockers.push(buildBlocker(
388
712
  'release_sensitive_changes_require_pr',
389
713
  `Release-sensitive changes on ${currentBranch} require an open pull request before continuing.`,
@@ -391,7 +715,7 @@ function evaluateOperationalIntegrity(options = {}) {
391
715
  ));
392
716
  }
393
717
 
394
- if (options.requireVersionNotBehindBase && releaseSensitiveFiles.length > 0 && versionComparison !== null && versionComparison < 0) {
718
+ if (options.requireVersionNotBehindBase && hasReleaseSensitiveFiles && versionComparison !== null && versionComparison < 0) {
395
719
  blockers.push(buildBlocker(
396
720
  'version_behind_base',
397
721
  `package.json version ${packageVersion} is behind ${baseBranch} version ${baseVersion} while release-sensitive files changed.`,
@@ -474,6 +798,8 @@ function runCli(env = process.env, argv = process.argv.slice(2)) {
474
798
  });
475
799
 
476
800
  const lines = [];
801
+ const hasReleaseSensitiveFiles = Array.isArray(result.releaseSensitiveFiles) && result.releaseSensitiveFiles.length > 0;
802
+ const openPrNumber = result.openPr?.number;
477
803
  lines.push(`Operational integrity: ${result.ok ? 'ok' : 'blocked'}`);
478
804
  lines.push(`Base branch: ${result.baseBranch}`);
479
805
  lines.push(`Current branch: ${result.currentBranch || 'unknown'}`);
@@ -483,11 +809,12 @@ function runCli(env = process.env, argv = process.argv.slice(2)) {
483
809
  if (result.baseVersion) {
484
810
  lines.push(`${result.baseBranch} version: ${result.baseVersion}`);
485
811
  }
486
- if (result.releaseSensitiveFiles.length > 0) {
812
+ if (hasReleaseSensitiveFiles) {
487
813
  lines.push(`Release-sensitive files: ${result.releaseSensitiveFiles.join(', ')}`);
488
814
  }
489
- if (result.openPr && result.openPr.number) {
490
- lines.push(`Open PR: #${result.openPr.number}${result.openPr.url ? ` ${result.openPr.url}` : ''}`);
815
+ if (openPrNumber) {
816
+ const openPrSuffix = result.openPr?.url ? ` ${result.openPr.url}` : '';
817
+ lines.push(`Open PR: #${openPrNumber}${openPrSuffix}`);
491
818
  }
492
819
  for (const blocker of result.blockers) {
493
820
  lines.push(`BLOCKER ${blocker.code}: ${blocker.message}`);
@@ -502,7 +829,7 @@ function runCli(env = process.env, argv = process.argv.slice(2)) {
502
829
  return result.ok ? 0 : 1;
503
830
  }
504
831
 
505
- if (require.main === module) {
832
+ if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
506
833
  process.exitCode = runCli();
507
834
  }
508
835
 
@@ -515,14 +842,22 @@ module.exports = {
515
842
  findOpenPrForBranch,
516
843
  findReleaseSensitiveFiles,
517
844
  getCurrentBranch,
845
+ getGitBinary,
846
+ assertSafeGitObjectId,
847
+ gitVerifyRef,
518
848
  isSafeBranchName,
849
+ isSafeGitObjectId,
850
+ isSafeGitRevision,
851
+ isHeadReachableFrom,
519
852
  listChangedFilesAgainstBase,
520
853
  normalizeGlob,
521
854
  normalizePosix,
522
855
  parseSemver,
523
856
  readPackageVersion,
857
+ resolveGitBinary,
524
858
  resolveBaseRef,
525
859
  resolveCiBranchName,
860
+ resolveGitHubRepository,
526
861
  resolveRepoRoot,
527
862
  runCli,
528
863
  sanitizeGlobList,
@@ -19,6 +19,11 @@ const path = require('path');
19
19
  const { resolveFeedbackDir } = require('./feedback-paths');
20
20
  const { readAuditLog, auditStats, skillAdherence } = require('./audit-trail');
21
21
  const { isProTier } = require('./rate-limiter');
22
+ const {
23
+ PRO_MONTHLY_PAYMENT_LINK,
24
+ PRO_PRICE_LABEL,
25
+ TEAM_PRICE_LABEL,
26
+ } = require('./commercial-offer');
22
27
 
23
28
  // ---------------------------------------------------------------------------
24
29
  // Agent Registry
@@ -181,7 +186,7 @@ function generateOrgDashboard(opts = {}) {
181
186
  };
182
187
 
183
188
  if (!pro) {
184
- summary.upgradeMessage = 'Upgrade to Pro for full org visibility all agents, all gates, all history. https://thumbgate-production.up.railway.app/checkout/pro';
189
+ summary.upgradeMessage = `Pro checkout: ${PRO_PRICE_LABEL}${PRO_MONTHLY_PAYMENT_LINK} | Team: ${TEAM_PRICE_LABEL} after workflow qualification.`;
185
190
  }
186
191
 
187
192
  return summary;
@@ -15,11 +15,9 @@
15
15
  const fs = require('fs');
16
16
  const path = require('path');
17
17
  const { resolveFeedbackDir } = require('./feedback-paths');
18
+ const { ensureParentDir, readJsonl } = require('./fs-utils');
18
19
 
19
20
  function getFeedbackDir() { return resolveFeedbackDir(); }
20
- function ensureDir(fp) { const d = path.dirname(fp); if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true }); }
21
- function readJsonl(fp) { if (!fs.existsSync(fp)) return []; const raw = fs.readFileSync(fp, 'utf-8').trim(); if (!raw) return []; return raw.split('\n').map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); }
22
-
23
21
  const SCORES_FILE = 'step-scores.jsonl';
24
22
  function getScoresPath() { return path.join(getFeedbackDir(), SCORES_FILE); }
25
23
 
@@ -62,7 +60,7 @@ function scoreStep(auditEntry) {
62
60
  function scoreAuditTrail(auditEntries) {
63
61
  const scores = auditEntries.map(scoreStep);
64
62
  const scoresPath = getScoresPath();
65
- ensureDir(scoresPath);
63
+ ensureParentDir(scoresPath);
66
64
  for (const s of scores) fs.appendFileSync(scoresPath, JSON.stringify(s) + '\n');
67
65
  return { scored: scores.length, scores };
68
66
  }