thumbgate 1.28.3 → 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 +5 -3
- package/scripts/commercial-offer.js +18 -10
- package/scripts/external-customer-audit.js +881 -0
- package/scripts/feedback-loop.js +49 -15
- 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/scripts/workflow-sentinel.js +4 -2
- package/src/api/server.js +521 -114
package/scripts/feedback-loop.js
CHANGED
|
@@ -94,7 +94,9 @@ function emitAnonymousFeedbackPing(signal) {
|
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
let normalizedSignal = null;
|
|
98
|
+
if (signal === 'positive') normalizedSignal = 'up';
|
|
99
|
+
if (signal === 'negative') normalizedSignal = 'down';
|
|
98
100
|
if (!normalizedSignal) return;
|
|
99
101
|
|
|
100
102
|
// Reuse the canonical installId from cli-telemetry.js (persisted at
|
|
@@ -105,24 +107,24 @@ function emitAnonymousFeedbackPing(signal) {
|
|
|
105
107
|
try {
|
|
106
108
|
const { getInstallId } = require('./cli-telemetry');
|
|
107
109
|
installId = getInstallId();
|
|
108
|
-
} catch
|
|
110
|
+
} catch {
|
|
111
|
+
installId = null;
|
|
112
|
+
}
|
|
109
113
|
if (!installId) {
|
|
110
|
-
|
|
111
|
-
installId = require('crypto').randomUUID();
|
|
112
|
-
} catch (_) {
|
|
113
|
-
return; // no crypto, no install id → drop silently
|
|
114
|
-
}
|
|
114
|
+
installId = crypto.randomUUID();
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
let tier = 'free';
|
|
118
118
|
try {
|
|
119
119
|
const { getStatuslineMeta } = require('./statusline-meta');
|
|
120
120
|
const meta = getStatuslineMeta({ env });
|
|
121
|
-
const rawTier = String(meta
|
|
121
|
+
const rawTier = String(meta?.tier || 'free').toLowerCase();
|
|
122
122
|
if (rawTier === 'pro' || rawTier === 'enterprise' || rawTier === 'free') {
|
|
123
123
|
tier = rawTier;
|
|
124
124
|
}
|
|
125
|
-
} catch
|
|
125
|
+
} catch {
|
|
126
|
+
tier = 'free';
|
|
127
|
+
}
|
|
126
128
|
|
|
127
129
|
const base = env.THUMBGATE_PUBLIC_APP_ORIGIN
|
|
128
130
|
|| env.THUMBGATE_API_URL
|
|
@@ -141,13 +143,17 @@ function emitAnonymousFeedbackPing(signal) {
|
|
|
141
143
|
if (typeof fetch !== 'function' || typeof AbortSignal === 'undefined' || typeof AbortSignal.timeout !== 'function') {
|
|
142
144
|
return;
|
|
143
145
|
}
|
|
144
|
-
|
|
146
|
+
let appOrigin = String(base);
|
|
147
|
+
while (appOrigin.endsWith('/')) appOrigin = appOrigin.slice(0, -1);
|
|
148
|
+
void fetch(`${appOrigin}/v1/telemetry/ping`, {
|
|
145
149
|
method: 'POST',
|
|
146
150
|
headers: { 'Content-Type': 'application/json' },
|
|
147
151
|
body,
|
|
148
152
|
signal: AbortSignal.timeout(2000),
|
|
149
|
-
}).catch(() =>
|
|
150
|
-
} catch
|
|
153
|
+
}).catch(() => undefined);
|
|
154
|
+
} catch {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
function isAuditTrailEntry(entry = {}) {
|
|
@@ -1695,6 +1701,32 @@ function captureFeedback(params) {
|
|
|
1695
1701
|
|
|
1696
1702
|
const _captureMs = Date.now() - _captureStart;
|
|
1697
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
|
+
|
|
1698
1730
|
// Auto-open feedback session for follow-up capture
|
|
1699
1731
|
let feedbackSession = null;
|
|
1700
1732
|
try {
|
|
@@ -1793,7 +1825,7 @@ function captureFeedback(params) {
|
|
|
1793
1825
|
// AGENTS.md/GEMINI.md merely because a negative rule was promoted.
|
|
1794
1826
|
if (isSelfHarnessOptimizerEnabled()) {
|
|
1795
1827
|
try {
|
|
1796
|
-
const { fork } = require('child_process');
|
|
1828
|
+
const { fork } = require('node:child_process');
|
|
1797
1829
|
const localOptimizerPath = path.join(process.cwd(), 'scripts', 'self-harness-optimizer.js');
|
|
1798
1830
|
const packageOptimizerPath = path.join(__dirname, 'self-harness-optimizer.js');
|
|
1799
1831
|
|
|
@@ -1820,8 +1852,9 @@ function captureFeedback(params) {
|
|
|
1820
1852
|
feedbackDir: FEEDBACK_DIR,
|
|
1821
1853
|
outputDir: process.env.THUMBGATE_OBSIDIAN_VAULT_PATH,
|
|
1822
1854
|
});
|
|
1823
|
-
} catch
|
|
1824
|
-
|
|
1855
|
+
} catch {
|
|
1856
|
+
resolve();
|
|
1857
|
+
return;
|
|
1825
1858
|
}
|
|
1826
1859
|
resolve();
|
|
1827
1860
|
});
|
|
@@ -2511,6 +2544,7 @@ function buildCorrectiveActionsReminder(correctiveActions = []) {
|
|
|
2511
2544
|
|
|
2512
2545
|
module.exports = {
|
|
2513
2546
|
captureFeedback: captureFeedbackIdempotent,
|
|
2547
|
+
emitAnonymousFeedbackPing,
|
|
2514
2548
|
buildFeedbackSourceIdentity,
|
|
2515
2549
|
compactMemories,
|
|
2516
2550
|
buildCorrectiveActionsReminder,
|
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,
|