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.
Files changed (66) 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 +5 -3
  39. package/scripts/commercial-offer.js +18 -10
  40. package/scripts/external-customer-audit.js +881 -0
  41. package/scripts/feedback-loop.js +49 -15
  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/scripts/workflow-sentinel.js +4 -2
  66. package/src/api/server.js +521 -114
@@ -94,7 +94,9 @@ function emitAnonymousFeedbackPing(signal) {
94
94
  return;
95
95
  }
96
96
 
97
- const normalizedSignal = signal === 'positive' ? 'up' : signal === 'negative' ? 'down' : null;
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 (_) { /* fall through */ }
110
+ } catch {
111
+ installId = null;
112
+ }
109
113
  if (!installId) {
110
- try {
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 && meta.tier ? meta.tier : 'free').toLowerCase();
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 (_) { /* default to 'free' */ }
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
- fetch(`${base.replace(/\/+$/, '')}/v1/telemetry/ping`, {
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(() => { /* fire-and-forget */ });
150
- } catch (_) { /* telemetry must never disrupt CLI */ }
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 (_err) {
1824
- // Non-critical, do not crash feedback loop
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,
@@ -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,