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.
- package/.claude-plugin/README.md +4 -4
- package/.claude-plugin/marketplace.json +32 -13
- package/.claude-plugin/plugin.json +15 -2
- package/.well-known/llms.txt +60 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +133 -23
- package/adapters/README.md +1 -1
- package/adapters/chatgpt/openapi.yaml +168 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +85 -2
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +215 -19
- package/bin/postinstall.js +8 -2
- package/config/budget.json +18 -0
- package/config/gates/code-edit.json +61 -0
- package/config/gates/db-write.json +61 -0
- package/config/gates/default.json +154 -3
- package/config/gates/deploy.json +61 -0
- package/config/github-about.json +2 -1
- package/config/merge-quality-checks.json +23 -0
- package/config/model-tiers.json +11 -0
- package/openapi/openapi.yaml +168 -0
- package/package.json +47 -13
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +1 -1
- package/plugins/codex-profile/INSTALL.md +27 -4
- package/plugins/codex-profile/README.md +33 -9
- 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/blog.html +73 -0
- package/public/compare/mem0.html +189 -0
- package/public/compare/speclock.html +180 -0
- package/public/compare.html +12 -4
- package/public/guide.html +5 -5
- package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
- package/public/guides/codex-cli-guardrails.html +158 -0
- package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
- package/public/guides/pre-action-gates.html +162 -0
- package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
- package/public/index.html +169 -70
- package/public/learn/ai-agent-persistent-memory.html +1 -0
- package/public/lessons.html +334 -17
- package/public/llm-context.md +140 -0
- package/public/pro.html +24 -22
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/access-anomaly-detector.js +1 -1
- package/scripts/adk-consolidator.js +1 -5
- package/scripts/agent-security-hardening.js +4 -6
- package/scripts/agentic-data-pipeline.js +1 -3
- package/scripts/async-job-runner.js +1 -5
- package/scripts/audit-trail.js +7 -5
- package/scripts/background-agent-governance.js +2 -10
- package/scripts/billing.js +2 -16
- package/scripts/budget-enforcer.js +173 -0
- package/scripts/build-codex-plugin.js +152 -0
- package/scripts/capture-railway-diagnostics.sh +97 -0
- package/scripts/check-congruence.js +133 -15
- package/scripts/claude-feedback-sync.js +320 -0
- package/scripts/cli-telemetry.js +4 -1
- package/scripts/commercial-offer.js +5 -7
- package/scripts/content-engine/linkedin-content-generator.js +154 -0
- package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
- package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
- package/scripts/content-engine/reddit-thread-finder.js +154 -0
- package/scripts/context-engine.js +21 -6
- package/scripts/contextfs.js +33 -44
- package/scripts/dashboard.js +104 -0
- package/scripts/decision-journal.js +341 -0
- package/scripts/delegation-runtime.js +1 -5
- package/scripts/distribution-surfaces.js +26 -0
- package/scripts/document-intake.js +927 -0
- package/scripts/ephemeral-agent-store.js +1 -8
- package/scripts/evolution-state.js +1 -5
- package/scripts/experiment-tracker.js +1 -5
- package/scripts/export-databricks-bundle.js +1 -5
- package/scripts/export-hf-dataset.js +1 -5
- package/scripts/export-training.js +1 -5
- package/scripts/feedback-attribution.js +1 -16
- package/scripts/feedback-history-distiller.js +1 -16
- package/scripts/feedback-loop.js +17 -5
- package/scripts/feedback-root-consolidator.js +2 -21
- package/scripts/feedback-session.js +49 -0
- package/scripts/feedback-to-rules.js +188 -28
- package/scripts/filesystem-search.js +1 -9
- package/scripts/fs-utils.js +104 -0
- package/scripts/gates-engine.js +149 -4
- package/scripts/github-about.js +32 -8
- package/scripts/gtm-revenue-loop.js +1 -5
- package/scripts/harness-selector.js +148 -0
- package/scripts/hosted-job-launcher.js +1 -5
- package/scripts/hybrid-feedback-context.js +7 -33
- package/scripts/intervention-policy.js +753 -0
- package/scripts/lesson-db.js +3 -18
- package/scripts/lesson-inference.js +194 -16
- package/scripts/lesson-retrieval.js +60 -24
- package/scripts/llm-client.js +59 -0
- package/scripts/local-model-profile.js +18 -2
- package/scripts/managed-lesson-agent.js +183 -0
- package/scripts/marketing-experiment.js +8 -22
- package/scripts/meta-agent-loop.js +624 -0
- package/scripts/metered-billing.js +1 -1
- package/scripts/model-tier-router.js +10 -1
- package/scripts/money-watcher.js +1 -4
- package/scripts/obsidian-export.js +1 -5
- package/scripts/operational-integrity.js +369 -34
- package/scripts/org-dashboard.js +6 -1
- package/scripts/per-step-scoring.js +2 -4
- package/scripts/pr-manager.js +201 -19
- package/scripts/pro-features.js +3 -2
- package/scripts/prompt-dlp.js +3 -3
- package/scripts/prove-adapters.js +2 -5
- package/scripts/prove-attribution.js +1 -5
- package/scripts/prove-automation.js +3 -5
- package/scripts/prove-cloudflare-sandbox.js +1 -3
- package/scripts/prove-data-pipeline.js +1 -3
- package/scripts/prove-intelligence.js +1 -3
- package/scripts/prove-lancedb.js +1 -5
- package/scripts/prove-local-intelligence.js +1 -3
- package/scripts/prove-packaged-runtime.js +326 -0
- package/scripts/prove-predictive-insights.js +1 -3
- package/scripts/prove-runtime.js +13 -0
- package/scripts/prove-training-export.js +1 -3
- package/scripts/prove-workflow-contract.js +1 -5
- package/scripts/rate-limiter.js +6 -4
- package/scripts/reddit-dm-outreach.js +14 -4
- package/scripts/schedule-manager.js +3 -5
- package/scripts/security-scanner.js +448 -0
- package/scripts/self-distill-agent.js +579 -0
- package/scripts/semantic-dedup.js +115 -0
- package/scripts/skill-exporter.js +1 -3
- package/scripts/skill-generator.js +1 -5
- package/scripts/social-analytics/engagement-audit.js +1 -18
- package/scripts/social-analytics/pollers/linkedin.js +26 -16
- package/scripts/social-analytics/publishers/linkedin.js +1 -1
- package/scripts/social-analytics/publishers/zernio.js +51 -0
- package/scripts/social-pipeline.js +1 -3
- package/scripts/social-post-hourly.js +47 -4
- package/scripts/statusline-links.js +6 -5
- package/scripts/statusline-local-stats.js +2 -0
- package/scripts/statusline.sh +38 -7
- package/scripts/sync-branch-protection.js +340 -0
- package/scripts/tessl-export.js +1 -3
- package/scripts/thumbgate-search.js +32 -1
- package/scripts/tool-kpi-tracker.js +1 -1
- package/scripts/tool-registry.js +108 -4
- package/scripts/vector-store.js +1 -5
- package/scripts/weekly-auto-post.js +1 -1
- package/scripts/workflow-sentinel.js +205 -4
- package/skills/thumbgate/SKILL.md +2 -2
- package/src/api/server.js +273 -4
- package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
- /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
|
|
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) {
|
|
@@ -260,13 +572,24 @@ function runGh(args) {
|
|
|
260
572
|
});
|
|
261
573
|
}
|
|
262
574
|
|
|
263
|
-
function
|
|
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 (!
|
|
583
|
+
if (!env.GH_TOKEN && !env.GITHUB_TOKEN) {
|
|
267
584
|
return null;
|
|
268
585
|
}
|
|
269
|
-
const
|
|
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 &&
|
|
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 &&
|
|
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 (
|
|
812
|
+
if (hasReleaseSensitiveFiles) {
|
|
487
813
|
lines.push(`Release-sensitive files: ${result.releaseSensitiveFiles.join(', ')}`);
|
|
488
814
|
}
|
|
489
|
-
if (
|
|
490
|
-
|
|
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 (
|
|
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,
|
package/scripts/org-dashboard.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
}
|