thumbgate 0.9.10 → 0.9.11
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 +2 -2
- package/.claude-plugin/marketplace.json +4 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +115 -312
- package/adapters/README.md +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +4 -4
- package/adapters/mcp/server-stdio.js +61 -1
- package/adapters/opencode/opencode.json +4 -2
- package/bin/cli.js +156 -8
- package/bin/memory.sh +3 -3
- package/config/e2e-critical-flows.json +4 -0
- package/config/gates/default.json +74 -2
- package/config/github-about.json +1 -1
- package/config/mcp-allowlists.json +27 -0
- package/package.json +22 -5
- package/plugins/amp-skill/INSTALL.md +1 -0
- package/plugins/amp-skill/SKILL.md +1 -0
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +4 -2
- package/plugins/claude-skill/INSTALL.md +1 -0
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +4 -2
- package/plugins/codex-profile/INSTALL.md +1 -1
- package/plugins/codex-profile/README.md +1 -1
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/cursor-marketplace/README.md +3 -3
- package/plugins/cursor-marketplace/mcp.json +3 -1
- package/plugins/cursor-marketplace/scripts/gate-check.sh +15 -5
- package/plugins/gemini-extension/INSTALL.md +3 -3
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/dashboard.html +15 -8
- package/public/index.html +125 -185
- package/public/js/buyer-intent.js +252 -0
- package/public/pro.html +1085 -0
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/adk-consolidator.js +14 -2
- package/scripts/agent-readiness.js +3 -1
- package/scripts/agent-security-hardening.js +4 -4
- package/scripts/auto-promote-gates.js +2 -0
- package/scripts/auto-wire-hooks.js +105 -17
- package/scripts/behavioral-extraction.js +2 -6
- package/scripts/billing.js +107 -3
- package/scripts/budget-guard.js +2 -2
- package/scripts/build-metadata.js +14 -0
- package/scripts/context-engine.js +1 -0
- package/scripts/deploy-policy.js +3 -17
- package/scripts/dpo-optimizer.js +3 -6
- package/scripts/ensure-repo-bootstrap.js +129 -0
- package/scripts/export-dpo-pairs.js +2 -3
- package/scripts/export-kto-pairs.js +3 -4
- package/scripts/export-training.js +8 -6
- package/scripts/feedback-attribution.js +23 -11
- package/scripts/feedback-loop.js +40 -2
- package/scripts/feedback-to-rules.js +2 -1
- package/scripts/filesystem-search.js +3 -2
- package/scripts/gates-engine.js +760 -29
- package/scripts/generate-pretool-hook.sh +0 -0
- package/scripts/gtm-revenue-loop.js +20 -1
- package/scripts/hook-auto-capture.sh +8 -3
- package/scripts/hook-runtime.js +89 -0
- package/scripts/hook-stop-self-score.sh +3 -3
- package/scripts/hook-thumbgate-cache-updater.js +99 -38
- package/scripts/hosted-config.js +4 -16
- package/scripts/hybrid-feedback-context.js +54 -14
- package/scripts/install-mcp.js +13 -0
- package/scripts/intent-router.js +2 -2
- package/scripts/license.js +52 -14
- package/scripts/local-model-profile.js +3 -2
- package/scripts/mcp-config.js +68 -6
- package/scripts/meta-policy.js +4 -8
- package/scripts/money-watcher.js +166 -16
- package/scripts/obsidian-export.js +1 -0
- package/scripts/operational-integrity.js +480 -0
- package/scripts/post-everywhere.js +7 -12
- package/scripts/pr-manager.js +14 -11
- package/scripts/profile-router.js +2 -0
- package/scripts/prompt-dlp.js +1 -0
- package/scripts/publish-decision.js +10 -0
- package/scripts/published-cli.js +34 -0
- package/scripts/risk-scorer.js +3 -2
- package/scripts/rlhf_session_start.sh +32 -0
- package/scripts/skill-quality-tracker.js +3 -5
- package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
- package/scripts/social-analytics/db/social-analytics.db-wal +0 -0
- package/scripts/social-analytics/engagement-audit.js +202 -0
- package/scripts/social-analytics/instagram-thumbgate-post.js +45 -7
- package/scripts/social-analytics/install-growth-automation.js +114 -0
- package/scripts/social-analytics/load-env.js +46 -0
- package/scripts/social-analytics/poll-all.js +3 -18
- package/scripts/social-analytics/pollers/zernio.js +3 -0
- package/scripts/social-analytics/publish-instagram-thumbgate.js +22 -3
- package/scripts/social-analytics/publish-thumbgate-launch.js +316 -0
- package/scripts/social-analytics/publishers/reddit.js +7 -12
- package/scripts/social-analytics/publishers/zernio.js +210 -22
- package/scripts/social-analytics/reconcile-thumbgate-campaign.js +165 -0
- package/scripts/social-analytics/schedule-thumbgate-campaign.js +275 -0
- package/scripts/social-analytics/sync-launch-assets.js +185 -0
- package/scripts/social-post-hourly.js +185 -0
- package/scripts/social-quality-gate.js +119 -3
- package/scripts/social-reply-monitor.js +148 -32
- package/scripts/statusline-cache-path.js +27 -0
- package/scripts/statusline-meta.js +22 -0
- package/scripts/statusline.sh +24 -32
- package/scripts/sync-version.js +11 -3
- package/scripts/test-coverage.js +20 -13
- package/scripts/tool-registry.js +97 -0
- package/scripts/train_from_feedback.py +32 -9
- package/scripts/validate-feedback.js +3 -2
- package/scripts/vector-store.js +2 -3
- package/scripts/verify-obsidian-setup.sh +3 -3
- package/src/api/server.js +281 -33
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_BASE_BRANCH = 'main';
|
|
9
|
+
const DEFAULT_RELEASE_SENSITIVE_GLOBS = [
|
|
10
|
+
'package.json',
|
|
11
|
+
'package-lock.json',
|
|
12
|
+
'server.json',
|
|
13
|
+
'.github/workflows/ci.yml',
|
|
14
|
+
'.github/workflows/publish-*.yml',
|
|
15
|
+
'scripts/publish-decision.js',
|
|
16
|
+
'scripts/pr-manager.js',
|
|
17
|
+
'scripts/gates-engine.js',
|
|
18
|
+
'scripts/tool-registry.js',
|
|
19
|
+
'src/api/server.js',
|
|
20
|
+
'adapters/mcp/server-stdio.js',
|
|
21
|
+
'config/gates/**',
|
|
22
|
+
'config/mcp-allowlists.json',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function normalizePosix(filePath) {
|
|
26
|
+
return String(filePath || '')
|
|
27
|
+
.replace(/\\/g, '/')
|
|
28
|
+
.replace(/^\.\//, '')
|
|
29
|
+
.replace(/^\/+/, '')
|
|
30
|
+
.trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeGlob(glob) {
|
|
34
|
+
return normalizePosix(glob).replace(/\/+$/, '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sanitizeGlobList(globs) {
|
|
38
|
+
if (!Array.isArray(globs)) return [];
|
|
39
|
+
return [...new Set(globs.map((glob) => normalizeGlob(glob)).filter(Boolean))];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function globToRegExp(glob) {
|
|
43
|
+
const normalized = normalizeGlob(glob);
|
|
44
|
+
let pattern = '^';
|
|
45
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
46
|
+
const char = normalized[i];
|
|
47
|
+
const next = normalized[i + 1];
|
|
48
|
+
if (char === '*') {
|
|
49
|
+
if (next === '*') {
|
|
50
|
+
pattern += '.*';
|
|
51
|
+
i += 1;
|
|
52
|
+
} else {
|
|
53
|
+
pattern += '[^/]*';
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if ('\\^$+?.()|{}[]'.includes(char)) {
|
|
58
|
+
pattern += `\\${char}`;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
pattern += char;
|
|
62
|
+
}
|
|
63
|
+
pattern += '$';
|
|
64
|
+
return new RegExp(pattern);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function matchesAnyGlob(filePath, globs) {
|
|
68
|
+
const normalized = sanitizeGlobList(globs);
|
|
69
|
+
if (!filePath || normalized.length === 0) return false;
|
|
70
|
+
return normalized.some((glob) => {
|
|
71
|
+
try {
|
|
72
|
+
return globToRegExp(glob).test(normalizePosix(filePath));
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function runGit(repoPath, args) {
|
|
80
|
+
return execFileSync('git', args, {
|
|
81
|
+
cwd: repoPath,
|
|
82
|
+
encoding: 'utf8',
|
|
83
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
84
|
+
}).trim();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function tryRunGit(repoPath, args) {
|
|
88
|
+
try {
|
|
89
|
+
return runGit(repoPath, args);
|
|
90
|
+
} catch {
|
|
91
|
+
return '';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolveRepoRoot(repoPath = process.cwd()) {
|
|
96
|
+
try {
|
|
97
|
+
return runGit(repoPath, ['rev-parse', '--show-toplevel']);
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function gitRefExists(repoPath, ref) {
|
|
104
|
+
if (!repoPath || !ref) return false;
|
|
105
|
+
try {
|
|
106
|
+
execFileSync('git', ['rev-parse', '--verify', ref], {
|
|
107
|
+
cwd: repoPath,
|
|
108
|
+
stdio: 'ignore',
|
|
109
|
+
});
|
|
110
|
+
return true;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isSafeBranchName(branchName) {
|
|
117
|
+
const normalized = String(branchName || '').trim();
|
|
118
|
+
if (!normalized) return false;
|
|
119
|
+
if (normalized.startsWith('-')) return false;
|
|
120
|
+
if (!/^[A-Za-z0-9._/-]+$/.test(normalized)) return false;
|
|
121
|
+
if (normalized.includes('..') || normalized.includes('//') || normalized.includes('@{')) return false;
|
|
122
|
+
if (normalized.endsWith('.') || normalized.endsWith('/')) return false;
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function fetchBaseBranch(repoPath, baseBranch) {
|
|
127
|
+
if (!repoPath || !isSafeBranchName(baseBranch)) return false;
|
|
128
|
+
// Fetch the remote tracking refs without passing user-controlled branch names to git.
|
|
129
|
+
const result = spawnSync('git', ['fetch', '--no-tags', '--depth=64', 'origin'], {
|
|
130
|
+
cwd: repoPath,
|
|
131
|
+
encoding: 'utf8',
|
|
132
|
+
});
|
|
133
|
+
return result.status === 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function resolveBaseRef(repoPath, baseBranch = DEFAULT_BASE_BRANCH, { fetchIfMissing = false } = {}) {
|
|
137
|
+
if (!isSafeBranchName(baseBranch)) return null;
|
|
138
|
+
const remoteRef = `refs/remotes/origin/${baseBranch}`;
|
|
139
|
+
if (gitRefExists(repoPath, remoteRef)) {
|
|
140
|
+
return `origin/${baseBranch}`;
|
|
141
|
+
}
|
|
142
|
+
if (fetchIfMissing) {
|
|
143
|
+
fetchBaseBranch(repoPath, baseBranch);
|
|
144
|
+
if (gitRefExists(repoPath, remoteRef)) {
|
|
145
|
+
return `origin/${baseBranch}`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (gitRefExists(repoPath, baseBranch)) {
|
|
149
|
+
return baseBranch;
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getCurrentBranch(repoPath) {
|
|
155
|
+
return tryRunGit(repoPath, ['rev-parse', '--abbrev-ref', 'HEAD']) || null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getHeadSha(repoPath) {
|
|
159
|
+
return tryRunGit(repoPath, ['rev-parse', 'HEAD']) || null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function readPackageVersion(repoPath, ref = 'HEAD') {
|
|
163
|
+
try {
|
|
164
|
+
let raw;
|
|
165
|
+
if (ref === 'HEAD') {
|
|
166
|
+
raw = fs.readFileSync(path.join(repoPath, 'package.json'), 'utf8');
|
|
167
|
+
} else {
|
|
168
|
+
raw = runGit(repoPath, ['show', `${ref}:package.json`]);
|
|
169
|
+
}
|
|
170
|
+
return JSON.parse(raw).version || null;
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function parseSemver(version) {
|
|
177
|
+
const match = String(version || '').trim().match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
|
|
178
|
+
if (!match) return null;
|
|
179
|
+
return match.slice(1).map((part) => Number(part));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function compareSemver(left, right) {
|
|
183
|
+
const a = parseSemver(left);
|
|
184
|
+
const b = parseSemver(right);
|
|
185
|
+
if (!a || !b) return null;
|
|
186
|
+
for (let i = 0; i < 3; i += 1) {
|
|
187
|
+
if (a[i] > b[i]) return 1;
|
|
188
|
+
if (a[i] < b[i]) return -1;
|
|
189
|
+
}
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function listChangedFilesAgainstBase(repoPath, baseBranch = DEFAULT_BASE_BRANCH, { fetchIfMissing = false } = {}) {
|
|
194
|
+
const baseRef = resolveBaseRef(repoPath, baseBranch, { fetchIfMissing });
|
|
195
|
+
if (!baseRef) return [];
|
|
196
|
+
const diff = tryRunGit(repoPath, ['diff', '--name-only', `${baseRef}...HEAD`]);
|
|
197
|
+
return diff.split('\n').map((line) => normalizePosix(line)).filter(Boolean);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function findReleaseSensitiveFiles(files, globs = DEFAULT_RELEASE_SENSITIVE_GLOBS) {
|
|
201
|
+
return (Array.isArray(files) ? files : []).filter((filePath) => matchesAnyGlob(filePath, globs));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isHeadReachableFrom(repoPath, ref, commit = 'HEAD') {
|
|
205
|
+
if (!repoPath || !ref) return false;
|
|
206
|
+
const result = spawnSync('git', ['merge-base', '--is-ancestor', commit, ref], {
|
|
207
|
+
cwd: repoPath,
|
|
208
|
+
encoding: 'utf8',
|
|
209
|
+
});
|
|
210
|
+
return result.status === 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function runGh(args) {
|
|
214
|
+
return spawnSync('gh', args, {
|
|
215
|
+
encoding: 'utf8',
|
|
216
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function findOpenPrForBranch({ branchName, runner = runGh } = {}) {
|
|
221
|
+
const normalizedBranch = String(branchName || '').trim();
|
|
222
|
+
if (!normalizedBranch) return null;
|
|
223
|
+
if (!process.env.GH_TOKEN && !process.env.GITHUB_TOKEN) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
const result = runner(['pr', 'list', '--head', normalizedBranch, '--state', 'open', '--json', 'number,state,isDraft,url']);
|
|
227
|
+
if (!result || result.status !== 0) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
const prs = JSON.parse(result.stdout || '[]');
|
|
232
|
+
return Array.isArray(prs) && prs.length > 0 ? prs[0] : null;
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function classifyCommand(command) {
|
|
239
|
+
const text = String(command || '').trim();
|
|
240
|
+
return {
|
|
241
|
+
text,
|
|
242
|
+
isPrCreate: /\bgh\s+pr\s+create\b/i.test(text),
|
|
243
|
+
isPrMerge: /\bgh\s+pr\s+merge\b/i.test(text),
|
|
244
|
+
isPublish: /\b(?:npm|yarn|pnpm)\s+publish\b/i.test(text),
|
|
245
|
+
isReleaseCreate: /\bgh\s+release\s+create\b/i.test(text),
|
|
246
|
+
isTagCreate: /\bgit\s+tag\b/i.test(text),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function buildBlocker(code, message, extra = {}) {
|
|
251
|
+
return { code, message, ...extra };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function evaluateOperationalIntegrity(options = {}) {
|
|
255
|
+
const repoRoot = options.repoPath ? resolveRepoRoot(options.repoPath) : resolveRepoRoot(process.cwd());
|
|
256
|
+
const baseBranch = String(options.baseBranch || DEFAULT_BASE_BRANCH).trim() || DEFAULT_BASE_BRANCH;
|
|
257
|
+
const currentBranch = String(options.currentBranch || (repoRoot ? getCurrentBranch(repoRoot) : '')).trim() || null;
|
|
258
|
+
const baseRef = repoRoot ? resolveBaseRef(repoRoot, baseBranch, { fetchIfMissing: options.fetchBase === true }) : null;
|
|
259
|
+
const changedFiles = Array.isArray(options.changedFiles)
|
|
260
|
+
? options.changedFiles.map((filePath) => normalizePosix(filePath)).filter(Boolean)
|
|
261
|
+
: (repoRoot ? listChangedFilesAgainstBase(repoRoot, baseBranch, { fetchIfMissing: options.fetchBase === true }) : []);
|
|
262
|
+
const releaseSensitiveGlobs = sanitizeGlobList(options.releaseSensitiveGlobs || DEFAULT_RELEASE_SENSITIVE_GLOBS);
|
|
263
|
+
const releaseSensitiveFiles = findReleaseSensitiveFiles(changedFiles, releaseSensitiveGlobs);
|
|
264
|
+
const packageVersion = options.packageVersion !== undefined
|
|
265
|
+
? options.packageVersion
|
|
266
|
+
: (repoRoot ? readPackageVersion(repoRoot, 'HEAD') : null);
|
|
267
|
+
const baseVersion = options.baseVersion !== undefined
|
|
268
|
+
? options.baseVersion
|
|
269
|
+
: (repoRoot && baseRef ? readPackageVersion(repoRoot, baseRef) : null);
|
|
270
|
+
const versionComparison = packageVersion && baseVersion ? compareSemver(packageVersion, baseVersion) : null;
|
|
271
|
+
const headSha = options.headSha || (repoRoot ? getHeadSha(repoRoot) : null);
|
|
272
|
+
const headOnBase = options.headOnBase !== undefined
|
|
273
|
+
? options.headOnBase
|
|
274
|
+
: Boolean(repoRoot && baseRef && headSha && isHeadReachableFrom(repoRoot, baseRef, headSha));
|
|
275
|
+
const branchGovernance = options.branchGovernance && typeof options.branchGovernance === 'object'
|
|
276
|
+
? options.branchGovernance
|
|
277
|
+
: null;
|
|
278
|
+
const openPr = options.openPr !== undefined
|
|
279
|
+
? options.openPr
|
|
280
|
+
: findOpenPrForBranch({ branchName: currentBranch, runner: options.ghRunner || runGh });
|
|
281
|
+
const commandInfo = classifyCommand(options.command || '');
|
|
282
|
+
const blockers = [];
|
|
283
|
+
|
|
284
|
+
const requiresGovernance = commandInfo.isPrCreate || commandInfo.isPrMerge || commandInfo.isPublish || commandInfo.isReleaseCreate || commandInfo.isTagCreate;
|
|
285
|
+
const isPublishLike = commandInfo.isPublish || commandInfo.isReleaseCreate || commandInfo.isTagCreate;
|
|
286
|
+
|
|
287
|
+
if (requiresGovernance && !branchGovernance) {
|
|
288
|
+
blockers.push(buildBlocker(
|
|
289
|
+
'missing_branch_governance',
|
|
290
|
+
'PR, merge, release, and publish actions require explicit branch governance.'
|
|
291
|
+
));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (branchGovernance && branchGovernance.localOnly === true && requiresGovernance) {
|
|
295
|
+
blockers.push(buildBlocker(
|
|
296
|
+
'local_only_branch',
|
|
297
|
+
'This task is marked local-only. PR, merge, release, and publish actions are blocked.'
|
|
298
|
+
));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (commandInfo.isPrMerge && /--admin\b/i.test(commandInfo.text)) {
|
|
302
|
+
blockers.push(buildBlocker(
|
|
303
|
+
'admin_merge_bypass_forbidden',
|
|
304
|
+
'Admin merge bypass is blocked. Use the normal protected-branch flow or merge queue.'
|
|
305
|
+
));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (commandInfo.isPrMerge && branchGovernance && !branchGovernance.prNumber && !branchGovernance.prUrl) {
|
|
309
|
+
blockers.push(buildBlocker(
|
|
310
|
+
'merge_requires_pr_context',
|
|
311
|
+
'Merging requires explicit PR context (prNumber or prUrl) in branch governance.'
|
|
312
|
+
));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (isPublishLike) {
|
|
316
|
+
if (!branchGovernance || !branchGovernance.releaseVersion) {
|
|
317
|
+
blockers.push(buildBlocker(
|
|
318
|
+
'missing_release_version',
|
|
319
|
+
'Release and publish actions require an explicit releaseVersion in branch governance.'
|
|
320
|
+
));
|
|
321
|
+
} else if (packageVersion && branchGovernance.releaseVersion !== packageVersion) {
|
|
322
|
+
blockers.push(buildBlocker(
|
|
323
|
+
'release_version_mismatch',
|
|
324
|
+
`Branch governance expects release version ${branchGovernance.releaseVersion}, but package.json is ${packageVersion}.`
|
|
325
|
+
));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (currentBranch && currentBranch !== baseBranch) {
|
|
329
|
+
blockers.push(buildBlocker(
|
|
330
|
+
'publish_requires_base_branch',
|
|
331
|
+
`Release and publish actions must run from ${baseBranch}, not ${currentBranch}.`
|
|
332
|
+
));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (!headOnBase) {
|
|
336
|
+
blockers.push(buildBlocker(
|
|
337
|
+
'publish_requires_mainline_head',
|
|
338
|
+
`Current HEAD is not reachable from ${baseBranch}. Release and publish actions require a mainline commit.`
|
|
339
|
+
));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (options.requirePrForReleaseSensitive && releaseSensitiveFiles.length > 0 && currentBranch && currentBranch !== baseBranch && !openPr) {
|
|
344
|
+
blockers.push(buildBlocker(
|
|
345
|
+
'release_sensitive_changes_require_pr',
|
|
346
|
+
`Release-sensitive changes on ${currentBranch} require an open pull request before continuing.`,
|
|
347
|
+
{ releaseSensitiveFiles }
|
|
348
|
+
));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (options.requireVersionNotBehindBase && releaseSensitiveFiles.length > 0 && versionComparison !== null && versionComparison < 0) {
|
|
352
|
+
blockers.push(buildBlocker(
|
|
353
|
+
'version_behind_base',
|
|
354
|
+
`package.json version ${packageVersion} is behind ${baseBranch} version ${baseVersion} while release-sensitive files changed.`,
|
|
355
|
+
{ packageVersion, baseVersion }
|
|
356
|
+
));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
ok: blockers.length === 0,
|
|
361
|
+
repoRoot,
|
|
362
|
+
baseBranch,
|
|
363
|
+
baseRef,
|
|
364
|
+
currentBranch,
|
|
365
|
+
headSha,
|
|
366
|
+
headOnBase,
|
|
367
|
+
changedFiles,
|
|
368
|
+
releaseSensitiveFiles,
|
|
369
|
+
packageVersion,
|
|
370
|
+
baseVersion,
|
|
371
|
+
versionComparison,
|
|
372
|
+
branchGovernance,
|
|
373
|
+
openPr,
|
|
374
|
+
blockers,
|
|
375
|
+
commandInfo,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function parseCliArgs(argv = process.argv.slice(2)) {
|
|
380
|
+
const options = {
|
|
381
|
+
json: false,
|
|
382
|
+
ci: false,
|
|
383
|
+
fetchBase: false,
|
|
384
|
+
requirePrForReleaseSensitive: false,
|
|
385
|
+
requireVersionNotBehindBase: false,
|
|
386
|
+
repoPath: process.cwd(),
|
|
387
|
+
baseBranch: DEFAULT_BASE_BRANCH,
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
391
|
+
const arg = argv[i];
|
|
392
|
+
if (arg === '--json') {
|
|
393
|
+
options.json = true;
|
|
394
|
+
} else if (arg === '--ci') {
|
|
395
|
+
options.ci = true;
|
|
396
|
+
options.fetchBase = true;
|
|
397
|
+
options.requirePrForReleaseSensitive = true;
|
|
398
|
+
options.requireVersionNotBehindBase = true;
|
|
399
|
+
} else if (arg === '--fetch-base') {
|
|
400
|
+
options.fetchBase = true;
|
|
401
|
+
} else if (arg === '--require-pr-for-release-sensitive') {
|
|
402
|
+
options.requirePrForReleaseSensitive = true;
|
|
403
|
+
} else if (arg === '--require-version-not-behind-base') {
|
|
404
|
+
options.requireVersionNotBehindBase = true;
|
|
405
|
+
} else if (arg === '--repo-path' && argv[i + 1]) {
|
|
406
|
+
options.repoPath = argv[i + 1];
|
|
407
|
+
i += 1;
|
|
408
|
+
} else if (arg === '--base-branch' && argv[i + 1]) {
|
|
409
|
+
options.baseBranch = argv[i + 1];
|
|
410
|
+
i += 1;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return options;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function runCli(env = process.env, argv = process.argv.slice(2)) {
|
|
418
|
+
const args = parseCliArgs(argv);
|
|
419
|
+
const result = evaluateOperationalIntegrity({
|
|
420
|
+
repoPath: args.repoPath,
|
|
421
|
+
baseBranch: args.baseBranch || env.DEFAULT_BRANCH || DEFAULT_BASE_BRANCH,
|
|
422
|
+
currentBranch: env.GITHUB_REF_NAME || undefined,
|
|
423
|
+
requirePrForReleaseSensitive: args.requirePrForReleaseSensitive,
|
|
424
|
+
requireVersionNotBehindBase: args.requireVersionNotBehindBase,
|
|
425
|
+
fetchBase: args.fetchBase,
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const lines = [];
|
|
429
|
+
lines.push(`Operational integrity: ${result.ok ? 'ok' : 'blocked'}`);
|
|
430
|
+
lines.push(`Base branch: ${result.baseBranch}`);
|
|
431
|
+
lines.push(`Current branch: ${result.currentBranch || 'unknown'}`);
|
|
432
|
+
if (result.packageVersion) {
|
|
433
|
+
lines.push(`package.json version: ${result.packageVersion}`);
|
|
434
|
+
}
|
|
435
|
+
if (result.baseVersion) {
|
|
436
|
+
lines.push(`${result.baseBranch} version: ${result.baseVersion}`);
|
|
437
|
+
}
|
|
438
|
+
if (result.releaseSensitiveFiles.length > 0) {
|
|
439
|
+
lines.push(`Release-sensitive files: ${result.releaseSensitiveFiles.join(', ')}`);
|
|
440
|
+
}
|
|
441
|
+
if (result.openPr && result.openPr.number) {
|
|
442
|
+
lines.push(`Open PR: #${result.openPr.number}${result.openPr.url ? ` ${result.openPr.url}` : ''}`);
|
|
443
|
+
}
|
|
444
|
+
for (const blocker of result.blockers) {
|
|
445
|
+
lines.push(`BLOCKER ${blocker.code}: ${blocker.message}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (args.json) {
|
|
449
|
+
console.log(JSON.stringify(result, null, 2));
|
|
450
|
+
} else {
|
|
451
|
+
console.log(lines.join('\n'));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return result.ok ? 0 : 1;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (require.main === module) {
|
|
458
|
+
process.exitCode = runCli();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
module.exports = {
|
|
462
|
+
DEFAULT_BASE_BRANCH,
|
|
463
|
+
DEFAULT_RELEASE_SENSITIVE_GLOBS,
|
|
464
|
+
classifyCommand,
|
|
465
|
+
compareSemver,
|
|
466
|
+
evaluateOperationalIntegrity,
|
|
467
|
+
findOpenPrForBranch,
|
|
468
|
+
findReleaseSensitiveFiles,
|
|
469
|
+
getCurrentBranch,
|
|
470
|
+
isSafeBranchName,
|
|
471
|
+
listChangedFilesAgainstBase,
|
|
472
|
+
normalizeGlob,
|
|
473
|
+
normalizePosix,
|
|
474
|
+
parseSemver,
|
|
475
|
+
readPackageVersion,
|
|
476
|
+
resolveBaseRef,
|
|
477
|
+
resolveRepoRoot,
|
|
478
|
+
runCli,
|
|
479
|
+
sanitizeGlobList,
|
|
480
|
+
};
|
|
@@ -136,17 +136,9 @@ async function postToReddit(parsed, dryRun) {
|
|
|
136
136
|
const reddit = getPublisher('reddit');
|
|
137
137
|
const postData = await reddit.publishToReddit({ subreddit, title, text: body });
|
|
138
138
|
|
|
139
|
-
//
|
|
139
|
+
// Reddit follow-up comments are manual-review only.
|
|
140
140
|
if (comment && postData.name) {
|
|
141
|
-
console.log('[post-everywhere]
|
|
142
|
-
const token = await reddit.getRedditToken(
|
|
143
|
-
process.env.REDDIT_CLIENT_ID,
|
|
144
|
-
process.env.REDDIT_CLIENT_SECRET,
|
|
145
|
-
process.env.REDDIT_USERNAME,
|
|
146
|
-
process.env.REDDIT_PASSWORD
|
|
147
|
-
);
|
|
148
|
-
const userAgent = process.env.REDDIT_USER_AGENT || `thumbgate/1.0 by ${process.env.REDDIT_USERNAME}`;
|
|
149
|
-
await reddit.submitComment(token, userAgent, { parentId: postData.name, text: comment });
|
|
141
|
+
console.log('[post-everywhere] Reddit follow-up comment skipped; manual review required');
|
|
150
142
|
}
|
|
151
143
|
|
|
152
144
|
return postData;
|
|
@@ -216,8 +208,11 @@ async function postEverywhere(filePath, { platforms, dryRun } = {}) {
|
|
|
216
208
|
}
|
|
217
209
|
console.log('[post-everywhere] Quality gate: PASSED');
|
|
218
210
|
|
|
219
|
-
// Determine which platforms to post to
|
|
220
|
-
|
|
211
|
+
// Determine which platforms to post to.
|
|
212
|
+
// Default excludes devto — high-volume Dev.to posting is counterproductive (0 engagement on 427 posts).
|
|
213
|
+
// Use --platforms=devto explicitly for monthly cross-posts only.
|
|
214
|
+
const DEFAULT_PLATFORMS = ['reddit', 'x', 'linkedin'];
|
|
215
|
+
const targetPlatforms = platforms || (parsed.platform ? [parsed.platform] : DEFAULT_PLATFORMS);
|
|
221
216
|
|
|
222
217
|
// Preserve original body/comment so each platform gets a fresh UTM tag
|
|
223
218
|
const originalBody = parsed.body;
|
package/scripts/pr-manager.js
CHANGED
|
@@ -165,11 +165,9 @@ async function resolveBlockers(pr, runner = runGh) {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
// 5. Ready to Merge
|
|
168
|
-
if (pr.mergeStateStatus === 'CLEAN'
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return { status: 'ready' };
|
|
172
|
-
}
|
|
168
|
+
if (pr.mergeStateStatus === 'CLEAN' && pr.mergeable === 'MERGEABLE') {
|
|
169
|
+
console.log('[PR Manager] SUCCESS: PR is ready for protected autonomous merge.');
|
|
170
|
+
return { status: 'ready' };
|
|
173
171
|
}
|
|
174
172
|
|
|
175
173
|
return { status: 'pending', reason: 'unknown_state' };
|
|
@@ -179,14 +177,17 @@ async function resolveBlockers(pr, runner = runGh) {
|
|
|
179
177
|
* Perform autonomous merge
|
|
180
178
|
*/
|
|
181
179
|
function performMerge(prNumber, runner = runGh) {
|
|
182
|
-
|
|
183
|
-
|
|
180
|
+
const args = ['pr', 'merge', prNumber.toString(), '--squash', '--delete-branch', '--auto'];
|
|
181
|
+
console.log(`[PR Manager] Initiating protected squash merge for PR #${prNumber}...`);
|
|
182
|
+
const result = runner(args);
|
|
184
183
|
if (result.status === 0) {
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
const output = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
185
|
+
const mode = /merge queue|queued|auto-merge/i.test(output) ? 'queued_or_auto' : 'merged';
|
|
186
|
+
console.log(`[PR Manager] Merge accepted for PR #${prNumber} (${mode}).`);
|
|
187
|
+
return { ok: true, mode, args };
|
|
187
188
|
} else {
|
|
188
189
|
console.error(`[PR Manager] Merge failed: ${formatGhError(result)}`);
|
|
189
|
-
return false;
|
|
190
|
+
return { ok: false, mode: 'failed', args, error: formatGhError(result) };
|
|
190
191
|
}
|
|
191
192
|
}
|
|
192
193
|
|
|
@@ -202,7 +203,9 @@ async function managePrs(prNumber = '', runner = runGh) {
|
|
|
202
203
|
for (const pr of prs) {
|
|
203
204
|
const outcome = await resolveBlockers(pr, runner);
|
|
204
205
|
if (outcome.status === 'ready') {
|
|
205
|
-
|
|
206
|
+
const mergeResult = performMerge(pr.number, runner);
|
|
207
|
+
outcome.mergeRequested = mergeResult.ok;
|
|
208
|
+
outcome.mergeMode = mergeResult.mode;
|
|
206
209
|
}
|
|
207
210
|
|
|
208
211
|
results.push({
|
package/scripts/prompt-dlp.js
CHANGED
|
@@ -97,6 +97,7 @@ const KNOWN_GATED_TOOLS = new Set([
|
|
|
97
97
|
'Bash', 'Edit', 'Write', 'Read', 'Glob', 'Grep',
|
|
98
98
|
'capture_feedback', 'recall', 'search_lessons', 'prevention_rules',
|
|
99
99
|
'feedback_stats', 'construct_context_pack', 'evaluate_context_pack',
|
|
100
|
+
'set_task_scope', 'get_scope_state', 'approve_protected_action',
|
|
100
101
|
]);
|
|
101
102
|
|
|
102
103
|
const SHADOW_LOG_FILE = 'shadow-actions.jsonl';
|
|
@@ -9,6 +9,8 @@ function decidePublishPlan(options) {
|
|
|
9
9
|
const currentSha = String(options.currentSha || '').trim();
|
|
10
10
|
const tagSha = String(options.tagSha || '').trim();
|
|
11
11
|
const version = String(options.version || '').trim();
|
|
12
|
+
const currentBranch = String(options.currentBranch || '').trim();
|
|
13
|
+
const defaultBranch = String(options.defaultBranch || '').trim();
|
|
12
14
|
const published = normalizeBoolean(options.published);
|
|
13
15
|
const tagExists = normalizeBoolean(options.tagExists);
|
|
14
16
|
const tagMatchesCurrentCommit = tagExists && tagSha === currentSha;
|
|
@@ -21,6 +23,12 @@ function decidePublishPlan(options) {
|
|
|
21
23
|
throw new Error('CURRENT_SHA is required.');
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
if (currentBranch && defaultBranch && currentBranch !== defaultBranch) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Refusing to publish from ${currentBranch}. Publish workflow must run from ${defaultBranch}.`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
if (published && !tagExists) {
|
|
25
33
|
throw new Error(
|
|
26
34
|
`Version ${version} is already published on npm but has no remote tag. Recover from the original release commit or bump the version.`
|
|
@@ -106,6 +114,8 @@ function runCli(env = process.env) {
|
|
|
106
114
|
const plan = decidePublishPlan({
|
|
107
115
|
version: env.VERSION,
|
|
108
116
|
currentSha: env.CURRENT_SHA || env.GITHUB_SHA,
|
|
117
|
+
currentBranch: env.CURRENT_BRANCH || env.GITHUB_REF_NAME,
|
|
118
|
+
defaultBranch: env.DEFAULT_BRANCH,
|
|
109
119
|
published: env.NPM_PUBLISHED,
|
|
110
120
|
tagExists: env.TAG_EXISTS,
|
|
111
121
|
tagSha: env.TAG_SHA,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execFileSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
function publishedCliArgs(pkgVersion, commandArgs = []) {
|
|
9
|
+
return ['--yes', '--package', `thumbgate@${pkgVersion}`, 'thumbgate', ...commandArgs];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function runPublishedCli(pkgVersion, commandArgs = [], options = {}) {
|
|
13
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'thumbgate-published-cli-'));
|
|
14
|
+
try {
|
|
15
|
+
return execFileSync('npx', publishedCliArgs(pkgVersion, commandArgs), {
|
|
16
|
+
encoding: 'utf8',
|
|
17
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
18
|
+
timeout: options.timeout || 8000,
|
|
19
|
+
cwd: tmpDir,
|
|
20
|
+
});
|
|
21
|
+
} finally {
|
|
22
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function runPublishedCliHelp(pkgVersion, options = {}) {
|
|
27
|
+
return runPublishedCli(pkgVersion, ['help'], options);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
publishedCliArgs,
|
|
32
|
+
runPublishedCli,
|
|
33
|
+
runPublishedCliHelp,
|
|
34
|
+
};
|
package/scripts/risk-scorer.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const { resolveFeedbackDir: resolveSharedFeedbackDir } = require('./feedback-paths');
|
|
6
7
|
|
|
7
8
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
8
|
-
const DEFAULT_FEEDBACK_DIR =
|
|
9
|
+
const DEFAULT_FEEDBACK_DIR = resolveSharedFeedbackDir();
|
|
9
10
|
const DEFAULT_MODEL_PATH = path.join(DEFAULT_FEEDBACK_DIR, 'risk-model.json');
|
|
10
11
|
const DEFAULT_SEQUENCE_PATH = path.join(DEFAULT_FEEDBACK_DIR, 'feedback-sequences.jsonl');
|
|
11
12
|
|
|
@@ -29,7 +30,7 @@ const SAFETY_WORD_RE = /\b(budget|path|guardrail|safe|security|risk)\b/i;
|
|
|
29
30
|
const SUCCESS_WORD_RE = /\b(pass|worked|fixed|success|verified)\b/i;
|
|
30
31
|
|
|
31
32
|
function resolveFeedbackDir(feedbackDir) {
|
|
32
|
-
return feedbackDir
|
|
33
|
+
return resolveSharedFeedbackDir({ feedbackDir });
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
function readJSONL(filePath) {
|