thumbgate 1.28.4 → 1.29.1
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/plugin.json +1 -1
- package/.well-known/llms.txt +18 -10
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +5 -2
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +8 -8
- package/bin/postinstall.js +4 -13
- package/config/github-about.json +5 -4
- package/config/post-deploy-marketing-pages.json +6 -6
- package/docs/integrations/grafana/README.md +109 -0
- package/docs/integrations/grafana/thumbgate-revenue-evidence-dashboard.json +1930 -0
- package/openapi/openapi.yaml +240 -5
- package/package.json +59 -19
- package/public/agent-manager.html +10 -11
- package/public/agents-cost-savings.html +2 -2
- package/public/assets/brand/thumbgate-logo-transparent.svg +6 -11
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +11 -10
- package/public/assets/brand/thumbgate-mark.svg +10 -11
- package/public/blog/inside-your-boundary.html +114 -0
- package/public/blog/process-over-outcome-gates.html +119 -0
- package/public/blog.html +296 -402
- package/public/brand/thumbgate-mark.svg +5 -9
- package/public/codex-enterprise.html +2 -2
- package/public/compare.html +12 -3
- package/public/diagnostic.html +79 -29
- package/public/guide.html +4 -4
- package/public/index.html +1088 -2098
- package/public/install.html +3 -3
- package/public/js/buyer-intent.js +33 -18
- package/public/numbers.html +2 -2
- package/public/pricing.html +268 -408
- package/public/pro.html +4 -4
- package/scripts/billing.js +456 -126
- package/scripts/buyer-paths.js +102 -0
- package/scripts/cli-feedback.js +2 -2
- package/scripts/commercial-offer.js +18 -10
- package/scripts/external-customer-audit.js +881 -0
- package/scripts/feedback-loop.js +26 -0
- package/scripts/gates-engine.js +123 -1
- package/scripts/grafana-revenue-evidence.js +856 -0
- package/scripts/jsonl-window.js +89 -0
- package/scripts/lesson-embedding-index.js +3 -7
- package/scripts/meta-agent-loop.js +20 -2
- package/scripts/observability-env.js +139 -0
- package/scripts/observability-setup.js +55 -0
- package/scripts/plausible-domain-config.js +4 -0
- package/scripts/provider-live-evidence.js +1290 -0
- package/scripts/provider-payment-reconciler.js +442 -0
- package/scripts/provider-revenue-evidence.js +249 -0
- package/scripts/rate-limiter.js +1 -5
- package/scripts/revenue-action-eligibility.js +414 -0
- package/scripts/revenue-evidence-remediation.js +694 -0
- package/scripts/revenue-offer-system.js +709 -0
- package/scripts/sales-pipeline.js +1117 -0
- package/scripts/seo-gsd.js +8 -4
- package/scripts/stripe-credentials.js +37 -0
- package/scripts/stripe-revenue-catalog-audit.js +363 -0
- package/scripts/stripe-revenue-catalog.js +164 -0
- package/scripts/telemetry-analytics.js +23 -3
- package/scripts/vector-store.js +83 -7
- package/scripts/workflow-intake-queue.js +483 -0
- package/src/api/server.js +521 -114
package/scripts/feedback-loop.js
CHANGED
|
@@ -1701,6 +1701,32 @@ function captureFeedback(params) {
|
|
|
1701
1701
|
|
|
1702
1702
|
const _captureMs = Date.now() - _captureStart;
|
|
1703
1703
|
|
|
1704
|
+
// Funnel activation: first non-audit accepted feedback marks product usage.
|
|
1705
|
+
// Acquisition is init; activation is "operator actually used the feedback loop."
|
|
1706
|
+
if (!isAuditEntry) {
|
|
1707
|
+
try {
|
|
1708
|
+
const markerPath = path.join(FEEDBACK_DIR, 'activation-recorded.json');
|
|
1709
|
+
if (!fs.existsSync(markerPath)) {
|
|
1710
|
+
const { appendFunnelEvent } = require('./billing');
|
|
1711
|
+
appendFunnelEvent({
|
|
1712
|
+
stage: 'activation',
|
|
1713
|
+
event: 'first_feedback_capture',
|
|
1714
|
+
installId: feedbackEvent.installId || process.env.THUMBGATE_INSTALL_ID || null,
|
|
1715
|
+
evidence: feedbackEvent.id,
|
|
1716
|
+
metadata: {
|
|
1717
|
+
signal,
|
|
1718
|
+
source: 'capture_feedback',
|
|
1719
|
+
},
|
|
1720
|
+
});
|
|
1721
|
+
fs.writeFileSync(markerPath, JSON.stringify({
|
|
1722
|
+
at: new Date().toISOString(),
|
|
1723
|
+
feedbackId: feedbackEvent.id,
|
|
1724
|
+
signal,
|
|
1725
|
+
}), 'utf8');
|
|
1726
|
+
}
|
|
1727
|
+
} catch { /* activation funnel is best-effort */ }
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1704
1730
|
// Auto-open feedback session for follow-up capture
|
|
1705
1731
|
let feedbackSession = null;
|
|
1706
1732
|
try {
|
package/scripts/gates-engine.js
CHANGED
|
@@ -149,6 +149,20 @@ const UNCONDITIONAL_HARD_FLOOR_GATE_IDS = new Set([
|
|
|
149
149
|
'slopsquat-guard',
|
|
150
150
|
...SELF_PROTECT_HARD_FLOOR_GATE_IDS,
|
|
151
151
|
]);
|
|
152
|
+
// Issue #2782 (reported by Andy Martin, 2026-07-08): after the free-tier daily
|
|
153
|
+
// block cap is hit, applyDailyBlockCap() downgraded EVERY config-declared
|
|
154
|
+
// "block" gate to a warning — including these catastrophic, effectively
|
|
155
|
+
// irreversible commands — with no THUMBGATE_STRICT_ENFORCEMENT check at all.
|
|
156
|
+
// A free-tier user past their daily cap could have `rm -rf ~`, a force push,
|
|
157
|
+
// `git reset --hard`, or `git clean -f` silently allowed through. These four
|
|
158
|
+
// map directly to CLAUDE.md's own hard-block list and must never be subject
|
|
159
|
+
// to the daily cap discount, regardless of tier or strict-mode setting.
|
|
160
|
+
const CATASTROPHIC_DECLARATIVE_GATE_IDS = new Set([
|
|
161
|
+
'force-push',
|
|
162
|
+
'git-reset-hard',
|
|
163
|
+
'git-clean-force',
|
|
164
|
+
'rm-rf-home-or-root',
|
|
165
|
+
]);
|
|
152
166
|
const SELF_PROTECT_CONFIG_TARGET_PATTERN = /(?:^|\/)(?:config\/gates\/|config\/(?:budget|enforcement|mcp-allowlists)\.json$|\.thumbgate\/config\.json$|thumbgate\.json$)/i;
|
|
153
167
|
const SELF_PROTECT_HOOK_TARGET_PATTERN = /(?:^|\/)(?:\.claude\/settings(?:\.local)?\.json|\.codex\/config\.toml|scripts\/hook-[^/]+\.(?:js|sh))$/i;
|
|
154
168
|
const SELF_PROTECT_CONFIG_COMMAND_PATTERN = /(?:config\/gates\/|config\/(?:budget|enforcement|mcp-allowlists)\.json\b|\.thumbgate\/config\.json\b|thumbgate\.json\b)/i;
|
|
@@ -772,6 +786,11 @@ function incrementTodayBlockCount() {
|
|
|
772
786
|
* a deny result to a warn with an upgrade CTA. Returns null if no cap applies.
|
|
773
787
|
*/
|
|
774
788
|
function applyDailyBlockCap(denyResult) {
|
|
789
|
+
// Catastrophic/irreversible commands (force-push, git reset --hard, git
|
|
790
|
+
// clean -f, rm -rf on home/root) never get the free-tier daily-cap
|
|
791
|
+
// discount — see issue #2782. Checked first, before any tier/CI shortcut,
|
|
792
|
+
// so nothing can accidentally exempt a catastrophic gate from this floor.
|
|
793
|
+
if (denyResult && CATASTROPHIC_DECLARATIVE_GATE_IDS.has(denyResult.gate)) return null;
|
|
775
794
|
// Pro, trial, CI, and THUMBGATE_NO_RATE_LIMIT users are uncapped
|
|
776
795
|
if (isProTier()) return null;
|
|
777
796
|
if (process.env.CI || process.env.GITHUB_ACTIONS) return null;
|
|
@@ -1140,12 +1159,113 @@ function hasPrBranchContext(toolInput = {}, repoRoot = null) {
|
|
|
1140
1159
|
return Boolean(branchName && !isProtectedBranchName(branchName));
|
|
1141
1160
|
}
|
|
1142
1161
|
|
|
1143
|
-
function
|
|
1162
|
+
function hasExplicitPrReference(toolInput = {}) {
|
|
1163
|
+
return Boolean(
|
|
1164
|
+
toolInput.prNumber || toolInput.prUrl || toolInput.pullRequestNumber || toolInput.pullRequestUrl
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const GH_BINARY_FIXED_PATH_DIRS = ['/usr/local/bin', '/usr/bin', '/bin', '/opt/homebrew/bin'];
|
|
1169
|
+
|
|
1170
|
+
// SonarCloud flags a bare `execFileSync('gh', ...)` as a PATH-injection hotspot
|
|
1171
|
+
// (same reasoning as scripts/ci-cd-hygiene-audit.js resolveGhBinary). Resolve
|
|
1172
|
+
// gh's absolute path from a fixed directory list — and ONLY that list. Never
|
|
1173
|
+
// fall back to a PATH-resolved bare name: a workspace-controlled program
|
|
1174
|
+
// earlier on $PATH would otherwise execute with this hook's privileges during
|
|
1175
|
+
// a commit-time check, and could return crafted output to fake "merged" and
|
|
1176
|
+
// bypass thread-resolution enforcement entirely (found by review on PR #3027).
|
|
1177
|
+
function resolveGhBinaryForPrCheck() {
|
|
1178
|
+
for (const dir of GH_BINARY_FIXED_PATH_DIRS) {
|
|
1179
|
+
const candidate = path.join(dir, 'gh');
|
|
1180
|
+
try {
|
|
1181
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
1182
|
+
} catch {
|
|
1183
|
+
// keep searching
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// 2026-07-24 self-lockout (issue #3025): a commit landing on a branch whose PR
|
|
1190
|
+
// was already merged armed this gate forever — the premise ("verify PR
|
|
1191
|
+
// threads") was checked purely by branch name, never against real GitHub PR
|
|
1192
|
+
// state, so there was nothing left to satisfy it once the PR closed. This
|
|
1193
|
+
// checks whether the branch's PR (if any) is already dormant — merged,
|
|
1194
|
+
// closed, or nonexistent — in which case there is nothing to verify. Returns
|
|
1195
|
+
// `{ dormant: true, reason }` when safe to skip the gate, `{ dormant: false }`
|
|
1196
|
+
// when a real open PR exists (keep gating), or `null` when the check could
|
|
1197
|
+
// not be completed (gh unavailable/unauthenticated/timed out) — callers must
|
|
1198
|
+
// treat `null` as "cannot verify" and fall back to arming the gate as before,
|
|
1199
|
+
// never as license to relax enforcement.
|
|
1200
|
+
//
|
|
1201
|
+
// Deliberately does NOT trust local git config (e.g. a missing
|
|
1202
|
+
// `branch.<name>.remote`) as a signal that no PR exists: local config can be
|
|
1203
|
+
// wrong or stale relative to GitHub (unset upstream, push under a different
|
|
1204
|
+
// ref, etc.) while a real open PR with real unresolved threads still exists
|
|
1205
|
+
// (found by review on PR #3027) — the live `gh` check is the only source of
|
|
1206
|
+
// truth here.
|
|
1207
|
+
function checkPrDormantForBranch(branchName, repoRoot, execFn = execFileSync) {
|
|
1208
|
+
if (!branchName || !repoRoot) return null;
|
|
1209
|
+
|
|
1210
|
+
const ghBinary = resolveGhBinaryForPrCheck();
|
|
1211
|
+
if (!ghBinary) return null; // gh not found in a trusted location — cannot verify
|
|
1212
|
+
|
|
1213
|
+
let raw;
|
|
1214
|
+
try {
|
|
1215
|
+
raw = execFn(ghBinary, ['pr', 'view', branchName, '--json', 'number,state'], {
|
|
1216
|
+
cwd: repoRoot,
|
|
1217
|
+
encoding: 'utf8',
|
|
1218
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1219
|
+
timeout: 5000,
|
|
1220
|
+
});
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
const stderr = String((error && error.stderr) || (error && error.message) || '');
|
|
1223
|
+
if (/no pull requests found/i.test(stderr)) {
|
|
1224
|
+
return { dormant: true, reason: 'no-pr-for-branch' };
|
|
1225
|
+
}
|
|
1226
|
+
return null; // gh unauthenticated/network error/timed out — cannot verify
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
let pr;
|
|
1230
|
+
try {
|
|
1231
|
+
pr = JSON.parse(raw);
|
|
1232
|
+
} catch {
|
|
1233
|
+
return null;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
if (pr && pr.state === 'MERGED') return { dormant: true, reason: 'pr-merged', prNumber: pr.number };
|
|
1237
|
+
if (pr && pr.state === 'CLOSED') return { dormant: true, reason: 'pr-closed', prNumber: pr.number };
|
|
1238
|
+
return { dormant: false, reason: 'pr-open', prNumber: pr && pr.number };
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function registerPrThreadResolutionClaimGate(toolName, toolInput = {}, execOverride) {
|
|
1144
1242
|
if (!isGitCommitCommand(toolName, toolInput)) return null;
|
|
1145
1243
|
const repoRoot = resolveRepoRoot(toolInput);
|
|
1146
1244
|
if (!hasPrBranchContext(toolInput, repoRoot)) return null;
|
|
1147
1245
|
|
|
1148
1246
|
const branchName = detectBranchName(toolInput, repoRoot);
|
|
1247
|
+
|
|
1248
|
+
// Only auto-detect dormant PRs when the branch name is the sole signal. An
|
|
1249
|
+
// explicit prNumber/prUrl is a direct claim from the caller that a specific
|
|
1250
|
+
// PR exists and matters here — trust it rather than re-deriving a possibly
|
|
1251
|
+
// different answer from the branch name.
|
|
1252
|
+
if (!hasExplicitPrReference(toolInput)) {
|
|
1253
|
+
const prState = checkPrDormantForBranch(branchName, repoRoot, execOverride || execFileSync);
|
|
1254
|
+
if (prState && prState.dormant) {
|
|
1255
|
+
// A dormant PR (merged/closed/nonexistent) means there is nothing to
|
|
1256
|
+
// verify for THIS commit — simply don't arm the gate. Deliberately does
|
|
1257
|
+
// NOT write to the shared pr_threads_checked/thread_resolution_verified
|
|
1258
|
+
// condition store: those keys are global, not scoped to a branch, so
|
|
1259
|
+
// writing to them here would leak satisfaction into a DIFFERENT
|
|
1260
|
+
// branch's pending gate if a commit on that branch arms it within the
|
|
1261
|
+
// same 5-minute TTL window — an agent could otherwise pre-satisfy the
|
|
1262
|
+
// gate by committing on an abandoned merged-PR branch first, then
|
|
1263
|
+
// switch to an active PR branch and skip real thread verification
|
|
1264
|
+
// (found by review on PR #3027).
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1149
1269
|
const claimGate = registerClaimGate(
|
|
1150
1270
|
PR_THREAD_RESOLUTION_CLAIM_PATTERN,
|
|
1151
1271
|
PR_THREAD_RESOLUTION_REQUIRED_ACTIONS,
|
|
@@ -3501,6 +3621,8 @@ module.exports = {
|
|
|
3501
3621
|
evaluateBoostedRiskTagGuard,
|
|
3502
3622
|
registerPrThreadResolutionClaimGate,
|
|
3503
3623
|
evaluatePendingPrThreadResolutionGate,
|
|
3624
|
+
checkPrDormantForBranch,
|
|
3625
|
+
resolveGhBinaryForPrCheck,
|
|
3504
3626
|
isReadOnlyObservabilityTool,
|
|
3505
3627
|
getLocalOnlyScopeSources,
|
|
3506
3628
|
isRemoteSideEffectCommand,
|