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.
Files changed (65) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.well-known/llms.txt +18 -10
  3. package/.well-known/mcp/server-card.json +1 -1
  4. package/README.md +5 -2
  5. package/adapters/claude/.mcp.json +2 -2
  6. package/adapters/forge/forge.yaml +3 -3
  7. package/adapters/mcp/server-stdio.js +1 -1
  8. package/adapters/opencode/opencode.json +1 -1
  9. package/bin/cli.js +8 -8
  10. package/bin/postinstall.js +4 -13
  11. package/config/github-about.json +5 -4
  12. package/config/post-deploy-marketing-pages.json +6 -6
  13. package/docs/integrations/grafana/README.md +109 -0
  14. package/docs/integrations/grafana/thumbgate-revenue-evidence-dashboard.json +1930 -0
  15. package/openapi/openapi.yaml +240 -5
  16. package/package.json +59 -19
  17. package/public/agent-manager.html +10 -11
  18. package/public/agents-cost-savings.html +2 -2
  19. package/public/assets/brand/thumbgate-logo-transparent.svg +6 -11
  20. package/public/assets/brand/thumbgate-mark-inline-v3.svg +11 -10
  21. package/public/assets/brand/thumbgate-mark.svg +10 -11
  22. package/public/blog/inside-your-boundary.html +114 -0
  23. package/public/blog/process-over-outcome-gates.html +119 -0
  24. package/public/blog.html +296 -402
  25. package/public/brand/thumbgate-mark.svg +5 -9
  26. package/public/codex-enterprise.html +2 -2
  27. package/public/compare.html +12 -3
  28. package/public/diagnostic.html +79 -29
  29. package/public/guide.html +4 -4
  30. package/public/index.html +1088 -2098
  31. package/public/install.html +3 -3
  32. package/public/js/buyer-intent.js +33 -18
  33. package/public/numbers.html +2 -2
  34. package/public/pricing.html +268 -408
  35. package/public/pro.html +4 -4
  36. package/scripts/billing.js +456 -126
  37. package/scripts/buyer-paths.js +102 -0
  38. package/scripts/cli-feedback.js +2 -2
  39. package/scripts/commercial-offer.js +18 -10
  40. package/scripts/external-customer-audit.js +881 -0
  41. package/scripts/feedback-loop.js +26 -0
  42. package/scripts/gates-engine.js +123 -1
  43. package/scripts/grafana-revenue-evidence.js +856 -0
  44. package/scripts/jsonl-window.js +89 -0
  45. package/scripts/lesson-embedding-index.js +3 -7
  46. package/scripts/meta-agent-loop.js +20 -2
  47. package/scripts/observability-env.js +139 -0
  48. package/scripts/observability-setup.js +55 -0
  49. package/scripts/plausible-domain-config.js +4 -0
  50. package/scripts/provider-live-evidence.js +1290 -0
  51. package/scripts/provider-payment-reconciler.js +442 -0
  52. package/scripts/provider-revenue-evidence.js +249 -0
  53. package/scripts/rate-limiter.js +1 -5
  54. package/scripts/revenue-action-eligibility.js +414 -0
  55. package/scripts/revenue-evidence-remediation.js +694 -0
  56. package/scripts/revenue-offer-system.js +709 -0
  57. package/scripts/sales-pipeline.js +1117 -0
  58. package/scripts/seo-gsd.js +8 -4
  59. package/scripts/stripe-credentials.js +37 -0
  60. package/scripts/stripe-revenue-catalog-audit.js +363 -0
  61. package/scripts/stripe-revenue-catalog.js +164 -0
  62. package/scripts/telemetry-analytics.js +23 -3
  63. package/scripts/vector-store.js +83 -7
  64. package/scripts/workflow-intake-queue.js +483 -0
  65. package/src/api/server.js +521 -114
@@ -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 {
@@ -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 registerPrThreadResolutionClaimGate(toolName, toolInput = {}) {
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,