thumbgate 1.27.16 → 1.27.18
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/marketplace.json +85 -0
- package/.claude-plugin/plugin.json +3 -4
- package/.well-known/llms.txt +12 -33
- package/.well-known/mcp/server-card.json +8 -8
- package/README.md +30 -215
- package/adapters/chatgpt/openapi.yaml +1695 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/gemini/function-declarations.json +0 -1
- package/adapters/mcp/server-stdio.js +11 -263
- package/adapters/opencode/opencode.json +1 -1
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +122 -1421
- package/bin/postinstall.js +27 -11
- package/config/gate-templates.json +0 -216
- package/config/gates/claim-verification.json +0 -12
- package/config/gates/default.json +2 -31
- package/config/github-about.json +2 -2
- package/config/mcp-allowlists.json +13 -23
- package/config/merge-quality-checks.json +1 -0
- package/config/model-candidates.json +6 -121
- package/openapi/openapi.yaml +0 -12
- package/package.json +98 -228
- package/public/blog.html +4 -4
- package/public/codex-plugin.html +20 -72
- package/public/compare.html +8 -31
- package/public/dashboard.html +166 -930
- package/public/federal.html +2 -2
- package/public/guide.html +9 -33
- package/public/index.html +116 -495
- package/public/learn.html +18 -191
- package/public/lessons.html +10 -168
- package/public/numbers.html +7 -7
- package/public/pro.html +11 -34
- package/scripts/agent-memory-lifecycle.js +0 -211
- package/scripts/agent-readiness.js +3 -167
- package/scripts/agent-reward-model.js +1 -53
- package/scripts/auto-promote-gates.js +10 -82
- package/scripts/auto-wire-hooks.js +0 -14
- package/scripts/billing.js +1 -93
- package/scripts/build-metadata.js +10 -50
- package/scripts/cli-feedback.js +6 -18
- package/scripts/cli-schema.js +0 -90
- package/scripts/cli-telemetry.js +1 -6
- package/scripts/commercial-offer.js +2 -82
- package/scripts/context-manager.js +6 -74
- package/scripts/dashboard.js +2 -68
- package/scripts/durability/step.js +3 -3
- package/scripts/export-databricks-bundle.js +1 -5
- package/scripts/export-dpo-pairs.js +2 -7
- package/scripts/feedback-loop.js +1 -123
- package/scripts/feedback-quality.js +0 -87
- package/scripts/filesystem-search.js +10 -35
- package/scripts/gate-stats.js +0 -89
- package/scripts/gates-engine.js +85 -1105
- package/scripts/gemini-embedding-policy.js +1 -2
- package/scripts/hook-runtime.js +14 -20
- package/scripts/hook-thumbgate-cache-updater.js +2 -18
- package/scripts/hybrid-feedback-context.js +7 -142
- package/scripts/lesson-inference.js +3 -8
- package/scripts/lesson-search.js +1 -17
- package/scripts/license.js +1 -0
- package/scripts/llm-client.js +9 -390
- package/scripts/local-model-profile.js +8 -15
- package/scripts/memory-scope-readiness.js +0 -159
- package/scripts/meta-agent-loop.js +0 -36
- package/scripts/operational-integrity.js +5 -39
- package/scripts/oss-pr-opportunity-scout.js +5 -35
- package/scripts/plausible-server-events.js +6 -9
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/proxy-pointer-rag-guardrails.js +1 -42
- package/scripts/rate-limiter.js +13 -64
- package/scripts/secret-scanner.js +5 -44
- package/scripts/security-scanner.js +10 -260
- package/scripts/self-distill-agent.js +1 -3
- package/scripts/seo-gsd.js +2 -916
- package/scripts/statusline-cache-path.js +2 -17
- package/scripts/statusline-local-stats.js +1 -9
- package/scripts/statusline-meta.js +2 -28
- package/scripts/statusline.sh +4 -20
- package/scripts/telemetry-analytics.js +0 -357
- package/scripts/thompson-sampling.js +10 -31
- package/scripts/thumbgate-bench.js +1 -16
- package/scripts/thumbgate-search.js +19 -85
- package/scripts/tool-registry.js +1 -169
- package/scripts/vector-store.js +0 -45
- package/scripts/workflow-sentinel.js +53 -286
- package/scripts/workspace-evolver.js +2 -62
- package/src/api/server.js +290 -2578
- package/.claude/commands/dashboard.md +0 -15
- package/.claude/commands/thumbgate-blocked.md +0 -27
- package/.claude/commands/thumbgate-dashboard.md +0 -15
- package/.claude/commands/thumbgate-doctor.md +0 -30
- package/.claude/commands/thumbgate-guard.md +0 -36
- package/.claude/commands/thumbgate-protect.md +0 -30
- package/.claude/commands/thumbgate-rules.md +0 -30
- package/.well-known/agentic-verify.txt +0 -1
- package/adapters/gcp/dfcx-webhook-gate.js +0 -295
- package/adapters/letta/README.md +0 -41
- package/adapters/letta/thumbgate-letta-adapter.js +0 -133
- package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
- package/bench/observability-eval-suite.json +0 -26
- package/bin/dashboard-cli.js +0 -7
- package/commands/dashboard.md +0 -15
- package/commands/thumbgate-dashboard.md +0 -15
- package/config/builtin-lessons.json +0 -23
- package/config/gate-classifier-routing.json +0 -98
- package/config/post-deploy-marketing-pages.json +0 -61
- package/public/about.html +0 -162
- package/public/agent-manager.html +0 -179
- package/public/agents-cost-savings.html +0 -153
- package/public/ai-malpractice-prevention.html +0 -818
- package/public/assets/brand/github-social-preview.png +0 -0
- package/public/assets/brand/thumbgate-icon-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
- package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
- package/public/assets/brand/thumbgate-logo-transparent.svg +0 -22
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +0 -19
- package/public/assets/brand/thumbgate-mark-pro.svg +0 -23
- package/public/assets/brand/thumbgate-mark-team.svg +0 -26
- package/public/assets/brand/thumbgate-mark.svg +0 -21
- package/public/assets/brand/thumbgate-wordmark.svg +0 -20
- package/public/assets/claude-thumbgate-statusbar.svg +0 -8
- package/public/assets/codex-thumbgate-statusbar-test.svg +0 -9
- package/public/assets/legal-intake-control-flow.svg +0 -66
- package/public/brand/thumbgate-mark.svg +0 -19
- package/public/brand/thumbgate-og.svg +0 -16
- package/public/chatgpt-app.html +0 -330
- package/public/codex-enterprise.html +0 -123
- package/public/js/buyer-intent.js +0 -672
- package/public/pricing.html +0 -399
- package/scripts/action-receipts.js +0 -324
- package/scripts/activation-quickstart.js +0 -187
- package/scripts/agent-operations-planner.js +0 -621
- package/scripts/ai-component-inventory.js +0 -367
- package/scripts/async-eval-observability.js +0 -236
- package/scripts/audit.js +0 -65
- package/scripts/aws-blocks-guardrails.js +0 -228
- package/scripts/classifier-routing.js +0 -130
- package/scripts/dashboard-chat.js +0 -332
- package/scripts/feedback-aggregate.js +0 -281
- package/scripts/feedback-sanitizer.js +0 -105
- package/scripts/hob-pack.js +0 -591
- package/scripts/hook-stop-anti-claim.js +0 -301
- package/scripts/install-shim.js +0 -87
- package/scripts/mcp-oauth.js +0 -293
- package/scripts/noop-detect.js +0 -285
- package/scripts/omlx-smoke.js +0 -192
- package/scripts/parallel-workflow-orchestrator.js +0 -293
- package/scripts/plan-gate.js +0 -243
- package/scripts/plausible-domain-config.js +0 -99
- package/scripts/qa-scenario-planner.js +0 -136
- package/scripts/repeat-metric.js +0 -137
- package/scripts/secret-fixture-tokens.js +0 -61
- package/scripts/secret-redaction.js +0 -166
- package/scripts/self-harness-optimizer.js +0 -141
- package/scripts/self-healing-check.js +0 -193
- package/scripts/silent-failure-cluster.js +0 -531
- package/scripts/statusline-cache-read.js +0 -57
- package/scripts/sync-telemetry-from-prod.js +0 -374
- package/scripts/tool-contract-validator.js +0 -76
- package/scripts/trajectory-scorer.js +0 -63
- package/scripts/verify-marketing-pages-deployed.js +0 -195
- package/scripts/visitor-journey.js +0 -172
|
@@ -331,55 +331,6 @@ function buildRewardReport(episodes = [], options = {}) {
|
|
|
331
331
|
deep: 'destructive, public, or production-adjacent work',
|
|
332
332
|
xhigh: 'payments, secrets, deploy-prod, data-loss, force-push-main',
|
|
333
333
|
},
|
|
334
|
-
continualAdapterPlan: buildContinualAdapterTrainingPlan(episodes, options),
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function buildContinualAdapterTrainingPlan(episodes = [], options = {}) {
|
|
339
|
-
const maxAdapters = Math.max(1, Number(options.maxAdapters || 4));
|
|
340
|
-
const minExamples = Math.max(1, Number(options.minExamples || 2));
|
|
341
|
-
const pairs = buildPreferencePairs(episodes, { ...options, maxPairs: Number(options.maxPairs || 24) });
|
|
342
|
-
const candidates = rankGateCandidatesByReward(episodes, {
|
|
343
|
-
...options,
|
|
344
|
-
minOccurrences: minExamples,
|
|
345
|
-
maxGateCandidates: maxAdapters,
|
|
346
|
-
}).slice(0, maxAdapters);
|
|
347
|
-
|
|
348
|
-
const adapters = candidates.map((candidate, index) => ({
|
|
349
|
-
id: `lora_${candidate.gateId || `adapter_${index + 1}`}`.slice(0, 96),
|
|
350
|
-
source: candidate.key,
|
|
351
|
-
trainingMode: 'concurrent-lora',
|
|
352
|
-
examples: candidate.occurrences,
|
|
353
|
-
priorityScore: candidate.priorityScore,
|
|
354
|
-
targetBehavior: candidate.recommendation,
|
|
355
|
-
retentionChecks: [
|
|
356
|
-
'baseline gate pass-rate must not regress',
|
|
357
|
-
'previous adapter reward must stay within tolerance',
|
|
358
|
-
'new adapter must improve blocked-repeat or verification reward on held-out episodes',
|
|
359
|
-
],
|
|
360
|
-
}));
|
|
361
|
-
|
|
362
|
-
const enoughData = pairs.length >= minExamples && adapters.length > 0;
|
|
363
|
-
return {
|
|
364
|
-
status: enoughData ? 'ready' : 'needs_more_feedback',
|
|
365
|
-
trainingStack: 'multi-lora-continual-learning',
|
|
366
|
-
baseModelPolicy: 'freeze base model; train small adapters from reward-ranked DPO pairs',
|
|
367
|
-
scheduling: adapters.length > 1 ? 'batch adapters concurrently when they share the same frozen base' : 'train serially until at least two adapter candidates exist',
|
|
368
|
-
adapters,
|
|
369
|
-
dpoPairsAvailable: pairs.length,
|
|
370
|
-
retentionGate: 'ship no adapter unless reward, regression, and prior-domain retention checks pass',
|
|
371
|
-
nextActions: enoughData
|
|
372
|
-
? [
|
|
373
|
-
'Export reward-ranked DPO pairs.',
|
|
374
|
-
'Train candidate LoRA adapters concurrently on the same frozen base.',
|
|
375
|
-
'Evaluate each adapter against held-out negative episodes and prior positive episodes.',
|
|
376
|
-
'Promote only the adapter that improves reward without retention regression.',
|
|
377
|
-
]
|
|
378
|
-
: [
|
|
379
|
-
'Capture more thumbs-up/down outcomes before training.',
|
|
380
|
-
'Promote deterministic gates first when examples are sparse.',
|
|
381
|
-
'Use local semantic recall until enough reward-ranked pairs exist for adapter training.',
|
|
382
|
-
],
|
|
383
334
|
};
|
|
384
335
|
}
|
|
385
336
|
|
|
@@ -468,10 +419,8 @@ if (isCliInvocation()) {
|
|
|
468
419
|
console.log(JSON.stringify(buildPreferencePairs(episodes), null, 2));
|
|
469
420
|
} else if (args.command === 'gates') {
|
|
470
421
|
console.log(JSON.stringify(rankGateCandidatesByReward(episodes), null, 2));
|
|
471
|
-
} else if (args.command === 'adapters') {
|
|
472
|
-
console.log(JSON.stringify(buildContinualAdapterTrainingPlan(episodes), null, 2));
|
|
473
422
|
} else {
|
|
474
|
-
console.error(`Unknown command: ${args.command}. Use: report, pairs, gates
|
|
423
|
+
console.error(`Unknown command: ${args.command}. Use: report, pairs, gates`);
|
|
475
424
|
process.exit(1);
|
|
476
425
|
}
|
|
477
426
|
}
|
|
@@ -479,7 +428,6 @@ if (isCliInvocation()) {
|
|
|
479
428
|
module.exports = {
|
|
480
429
|
HIGH_RISK_TAGS,
|
|
481
430
|
allocateTestTimeCompute,
|
|
482
|
-
buildContinualAdapterTrainingPlan,
|
|
483
431
|
buildPreferencePairFromEpisodes,
|
|
484
432
|
buildPreferencePairs,
|
|
485
433
|
buildRewardReport,
|
|
@@ -58,47 +58,6 @@ function readJSONL(filePath) {
|
|
|
58
58
|
}).filter(Boolean);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
// --- Self-Harness stage 3: regression-gated promotion -----------------------
|
|
62
|
-
// Inspired by "Self-Harness: Harnesses That Improve Themselves" (arXiv 2606.09498).
|
|
63
|
-
// Stages 1-2 (weakness mining -> rule extraction) already exist via lesson
|
|
64
|
-
// inference + this promoter. Stage 3 — accept a harness change only after
|
|
65
|
-
// regression-testing it does not degrade behavior — was missing: a noisy 3x
|
|
66
|
-
// capture could hard-block an over-broad pattern with no check that it wouldn't
|
|
67
|
-
// have wrongly blocked actions that were previously ALLOWED. This replays a
|
|
68
|
-
// candidate BLOCK rule against the audit trail's prior `allow` decisions; if it
|
|
69
|
-
// would have blocked safe actions, the caller quarantines it to `warn` instead.
|
|
70
|
-
const REGRESSION_FALSE_BLOCK_LIMIT = 0; // any prior safe action it would block => quarantine
|
|
71
|
-
|
|
72
|
-
function getAuditTrailPath() {
|
|
73
|
-
return path.join(path.dirname(getFeedbackLogPath()), 'audit-trail.jsonl');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Returns { falseBlocks, allowSampleSize } or null when there is no history /
|
|
77
|
-
// matcher available — in which case the caller promotes as usual (fail-open to
|
|
78
|
-
// existing behavior, since regression gating is an enhancement, not a hard gate).
|
|
79
|
-
function regressionCheck(gate, options = {}) {
|
|
80
|
-
const auditPath = options.auditTrailPath || getAuditTrailPath();
|
|
81
|
-
const entries = readJSONL(auditPath);
|
|
82
|
-
if (!entries.length) return null;
|
|
83
|
-
// Lazy-require to avoid the gates-engine <-> auto-promote-gates require cycle.
|
|
84
|
-
let matchesGate;
|
|
85
|
-
try { ({ matchesGate } = require('./gates-engine')); } catch { return null; }
|
|
86
|
-
if (typeof matchesGate !== 'function') return null;
|
|
87
|
-
const allowed = entries.filter((e) => e && e.decision === 'allow' && e.toolName);
|
|
88
|
-
if (!allowed.length) return null;
|
|
89
|
-
let falseBlocks = 0;
|
|
90
|
-
for (const e of allowed) {
|
|
91
|
-
try {
|
|
92
|
-
if (matchesGate(gate, e.toolName, e.toolInput || {})) falseBlocks += 1;
|
|
93
|
-
} catch { /* a bad pattern/entry never counts as a false block */ }
|
|
94
|
-
}
|
|
95
|
-
return { falseBlocks, allowSampleSize: allowed.length };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function safeRegressionCheck(gate, options) {
|
|
99
|
-
try { return regressionCheck(gate, options); } catch { return null; }
|
|
100
|
-
}
|
|
101
|
-
|
|
102
61
|
function loadAutoGates() {
|
|
103
62
|
const autoGatesPath = getAutoGatesPath();
|
|
104
63
|
if (!fs.existsSync(autoGatesPath)) {
|
|
@@ -234,9 +193,9 @@ function patternToGateId(key) {
|
|
|
234
193
|
return 'auto-' + key.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').slice(0, 50).toLowerCase();
|
|
235
194
|
}
|
|
236
195
|
|
|
237
|
-
function buildGateRule(group
|
|
238
|
-
const action =
|
|
239
|
-
const severity = action === 'block' ? 'critical' :
|
|
196
|
+
function buildGateRule(group) {
|
|
197
|
+
const action = group.count === 'MANUAL' ? group.manualAction || 'block' : (group.count >= BLOCK_THRESHOLD ? 'block' : 'warn');
|
|
198
|
+
const severity = action === 'block' ? 'critical' : 'medium';
|
|
240
199
|
const context = group.latestContext.slice(0, 120);
|
|
241
200
|
const kind = group.key.startsWith('diagnosis:')
|
|
242
201
|
? 'repeated diagnosis'
|
|
@@ -373,8 +332,7 @@ function forcePromote(context, action = 'block') {
|
|
|
373
332
|
return { gateId, action, totalGates: data.gates.length };
|
|
374
333
|
}
|
|
375
334
|
|
|
376
|
-
function promote(feedbackLogPath
|
|
377
|
-
const opts = options || {};
|
|
335
|
+
function promote(feedbackLogPath) {
|
|
378
336
|
const logPath = feedbackLogPath || getFeedbackLogPath();
|
|
379
337
|
const entries = readJSONL(logPath);
|
|
380
338
|
const groups = groupNegativeFeedback(entries, WINDOW_DAYS);
|
|
@@ -399,38 +357,17 @@ function promote(feedbackLogPath, options) {
|
|
|
399
357
|
const existing = data.gates[existingIdx];
|
|
400
358
|
const newAction = group.count >= BLOCK_THRESHOLD ? 'block' : 'warn';
|
|
401
359
|
if (existing.action !== newAction && newAction === 'block') {
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
// Would block prior safe actions — hold at warn instead of upgrading.
|
|
406
|
-
promotions.push({ type: 'upgrade-quarantined', gateId, from: existing.action, occurrences: group.count, falseBlocks: regression.falseBlocks });
|
|
407
|
-
} else {
|
|
408
|
-
// Upgrade from warn to block
|
|
409
|
-
data.gates[existingIdx] = { ...existing, action: 'block', severity: 'critical', occurrences: group.count, upgradedAt: new Date().toISOString() };
|
|
410
|
-
promotions.push({ type: 'upgrade', gateId, from: existing.action, to: 'block', occurrences: group.count });
|
|
411
|
-
}
|
|
360
|
+
// Upgrade from warn to block
|
|
361
|
+
data.gates[existingIdx] = { ...existing, action: 'block', severity: 'critical', occurrences: group.count, upgradedAt: new Date().toISOString() };
|
|
362
|
+
promotions.push({ type: 'upgrade', gateId, from: existing.action, to: 'block', occurrences: group.count });
|
|
412
363
|
}
|
|
413
364
|
// Update occurrence count even if no action change
|
|
414
365
|
data.gates[existingIdx].occurrences = group.count;
|
|
415
366
|
continue;
|
|
416
367
|
}
|
|
417
368
|
|
|
418
|
-
// New gate
|
|
419
|
-
const gate = buildGateRule(group
|
|
420
|
-
|
|
421
|
-
// Self-Harness stage 3: before a feedback rule goes live as a hard block,
|
|
422
|
-
// regression-test it against prior allowed actions. If it would have blocked
|
|
423
|
-
// safe actions, quarantine it to `warn` instead of `block`.
|
|
424
|
-
let regression = null;
|
|
425
|
-
if (gate.action === 'block' && !opts.gateAction && !opts.skipRegression) {
|
|
426
|
-
regression = safeRegressionCheck(gate, opts);
|
|
427
|
-
if (regression && regression.falseBlocks > REGRESSION_FALSE_BLOCK_LIMIT) {
|
|
428
|
-
gate.action = 'warn';
|
|
429
|
-
gate.severity = 'medium';
|
|
430
|
-
gate.quarantined = true;
|
|
431
|
-
gate.regression = regression;
|
|
432
|
-
}
|
|
433
|
-
}
|
|
369
|
+
// New gate
|
|
370
|
+
const gate = buildGateRule(group);
|
|
434
371
|
|
|
435
372
|
// Enforce max limit — rotate oldest
|
|
436
373
|
if (data.gates.length >= MAX_AUTO_GATES) {
|
|
@@ -439,13 +376,7 @@ function promote(feedbackLogPath, options) {
|
|
|
439
376
|
}
|
|
440
377
|
|
|
441
378
|
data.gates.push(gate);
|
|
442
|
-
promotions.push({
|
|
443
|
-
type: gate.quarantined ? 'new-quarantined' : 'new',
|
|
444
|
-
gateId: gate.id,
|
|
445
|
-
action: gate.action,
|
|
446
|
-
occurrences: group.count,
|
|
447
|
-
...(gate.quarantined ? { falseBlocks: regression.falseBlocks, allowSampleSize: regression.allowSampleSize } : {}),
|
|
448
|
-
});
|
|
379
|
+
promotions.push({ type: 'new', gateId: gate.id, action: gate.action, occurrences: group.count });
|
|
449
380
|
}
|
|
450
381
|
|
|
451
382
|
// Log promotions
|
|
@@ -506,9 +437,6 @@ module.exports = {
|
|
|
506
437
|
groupNegativeFeedback,
|
|
507
438
|
patternToGateId,
|
|
508
439
|
buildGateRule,
|
|
509
|
-
regressionCheck,
|
|
510
|
-
getAuditTrailPath,
|
|
511
|
-
REGRESSION_FALSE_BLOCK_LIMIT,
|
|
512
440
|
extractPatternKey,
|
|
513
441
|
normalizeCommandSignature,
|
|
514
442
|
isNegative,
|
|
@@ -27,7 +27,6 @@ const {
|
|
|
27
27
|
statuslineCommand,
|
|
28
28
|
userPromptHookCommand,
|
|
29
29
|
} = require('./hook-runtime');
|
|
30
|
-
const { installShim } = require('./install-shim');
|
|
31
30
|
|
|
32
31
|
function getHome() {
|
|
33
32
|
return process.env.HOME || process.env.USERPROFILE || '';
|
|
@@ -339,19 +338,6 @@ function wireClaudeHooks(options) {
|
|
|
339
338
|
options.projectSettingsPath || claudeProjectSettingsPath(options.projectDir);
|
|
340
339
|
const dryRun = options.dryRun || false;
|
|
341
340
|
const projectDir = options.projectDir || process.cwd();
|
|
342
|
-
|
|
343
|
-
// --- Install stable shim before resolving hook commands ---
|
|
344
|
-
// The shim at ~/.thumbgate/bin/thumbgate-hook always resolves @latest,
|
|
345
|
-
// so hooks never go stale across version bumps (Volta-style pattern).
|
|
346
|
-
// Skip in source-checkout mode — developers use direct node commands.
|
|
347
|
-
if (!dryRun && !require('./mcp-config').isSourceCheckout(path.join(__dirname, '..'))) {
|
|
348
|
-
try {
|
|
349
|
-
installShim();
|
|
350
|
-
} catch {
|
|
351
|
-
// Non-fatal: fall back to version-pinned commands
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
341
|
const desiredStatusLine = statuslineCommand();
|
|
356
342
|
|
|
357
343
|
// --- Step 0: clean up stale hooks from BOTH settings locations ---
|
package/scripts/billing.js
CHANGED
|
@@ -51,7 +51,6 @@ const {
|
|
|
51
51
|
} = require('./analytics-window');
|
|
52
52
|
const { ensureParentDir } = require('./fs-utils');
|
|
53
53
|
const mailer = require('./mailer');
|
|
54
|
-
const { recordCheckoutFunnelEvent } = require('./plausible-server-events');
|
|
55
54
|
|
|
56
55
|
function loadWorkflowSprintIntakeModule() {
|
|
57
56
|
const modulePath = path.resolve(__dirname, 'workflow-sprint-intake.js');
|
|
@@ -139,10 +138,6 @@ const IS_TEST = !!(
|
|
|
139
138
|
process.env.NODE_ENV === 'test'
|
|
140
139
|
);
|
|
141
140
|
|
|
142
|
-
function allowUnsignedStripeWebhooks() {
|
|
143
|
-
return IS_TEST && process.env.THUMBGATE_ALLOW_UNSIGNED_STRIPE_WEBHOOKS === '1';
|
|
144
|
-
}
|
|
145
|
-
|
|
146
141
|
function shouldMergeLegacyBillingData() {
|
|
147
142
|
return process.env._TEST_INCLUDE_LEGACY_BILLING_DATA === '1'
|
|
148
143
|
|| process.env.THUMBGATE_INCLUDE_LEGACY_BILLING_DATA === '1';
|
|
@@ -449,57 +444,6 @@ function buildCheckoutProductData({ name, description, appOrigin, planId }) {
|
|
|
449
444
|
};
|
|
450
445
|
}
|
|
451
446
|
|
|
452
|
-
/**
|
|
453
|
-
* Verify an ACTIVE Stripe product exists for the given plan name before
|
|
454
|
-
* we let buildSubscriptionPriceData create inline price_data under it.
|
|
455
|
-
*
|
|
456
|
-
* Stripe matches product_data by `name`. If only an archived product
|
|
457
|
-
* matches, new prices created via that path inherit active=false and the
|
|
458
|
-
* generated checkout URL renders "Something went wrong / The page you
|
|
459
|
-
* were looking for could not be found." for the buyer. Stripe Dashboard
|
|
460
|
-
* shows the session as `open` with no email captured — looks like the
|
|
461
|
-
* buyer abandoned, but they were never given a working page.
|
|
462
|
-
*
|
|
463
|
-
* Failing here surfaces the misconfiguration at the first checkout
|
|
464
|
-
* attempt instead of silently breaking every buyer for days.
|
|
465
|
-
*
|
|
466
|
-
* Verified incident: ThumbGate#2188 (May 2026) — 20 sessions abandoned in
|
|
467
|
-
* 7 days, all because the only product named "ThumbGate Pro" matching the
|
|
468
|
-
* inline product_data was archived (prod_UXxOHAfbDsPyRb), while an active
|
|
469
|
-
* product with the same name existed (prod_UW82THPxfNvwKT) that should
|
|
470
|
-
* have been used instead.
|
|
471
|
-
*/
|
|
472
|
-
async function verifyActiveProductForPlan(stripe, planId) {
|
|
473
|
-
const expectedName = planId === 'team' ? 'ThumbGate Team' : 'ThumbGate Pro';
|
|
474
|
-
let products;
|
|
475
|
-
try {
|
|
476
|
-
products = await stripe.products.list({ limit: 100 });
|
|
477
|
-
} catch (err) {
|
|
478
|
-
// Network/transient failures shouldn't block checkout creation.
|
|
479
|
-
// The original session.create call will surface real Stripe errors.
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
const matching = (products && products.data ? products.data : [])
|
|
483
|
-
.filter((p) => p && p.name === expectedName);
|
|
484
|
-
if (matching.length === 0) {
|
|
485
|
-
// No product with this name exists; Stripe will create a new one when
|
|
486
|
-
// session.create fires with inline product_data. Safe path.
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
const active = matching.find((p) => p.active === true);
|
|
490
|
-
if (!active) {
|
|
491
|
-
const archived = matching[0];
|
|
492
|
-
throw new Error(
|
|
493
|
-
`Refusing to create checkout session: Stripe product named "${expectedName}" ` +
|
|
494
|
-
`exists only in archived state (id=${archived.id}, active=false). New prices ` +
|
|
495
|
-
`created via inline product_data would inherit active=false, rendering ` +
|
|
496
|
-
`"page not found" on Stripe checkout for every buyer. Fix: reactivate the ` +
|
|
497
|
-
`archived product in Stripe Dashboard, or rename the active product to ` +
|
|
498
|
-
`match "${expectedName}". See ThumbGate#2188 for the May 2026 incident.`
|
|
499
|
-
);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
447
|
function buildSubscriptionPriceData(checkoutSelection, appOrigin) {
|
|
504
448
|
const isTeam = checkoutSelection.planId === 'team';
|
|
505
449
|
const annual = checkoutSelection.billingCycle === 'annual';
|
|
@@ -2581,18 +2525,6 @@ async function createCheckoutSession({ successUrl, cancelUrl, customerEmail, ins
|
|
|
2581
2525
|
}
|
|
2582
2526
|
|
|
2583
2527
|
const stripe = getStripeClient();
|
|
2584
|
-
|
|
2585
|
-
// Defensive guard against ThumbGate#2188:
|
|
2586
|
-
// When buildSubscriptionPriceData passes inline `product_data` to Stripe,
|
|
2587
|
-
// Stripe name-matches existing products. If the only existing product with
|
|
2588
|
-
// that name is ARCHIVED (active=false), the new price inherits active=false
|
|
2589
|
-
// and every Stripe checkout page renders "page not found" for the buyer.
|
|
2590
|
-
// That bug burnt 20+ silent abandoned sessions in May 2026. Fail fast
|
|
2591
|
-
// instead of letting the broken page ship.
|
|
2592
|
-
if (!packId) {
|
|
2593
|
-
await verifyActiveProductForPlan(stripe, checkoutSelection.planId);
|
|
2594
|
-
}
|
|
2595
|
-
|
|
2596
2528
|
const sessionPayload = buildCheckoutSessionPayload({
|
|
2597
2529
|
successUrl,
|
|
2598
2530
|
cancelUrl,
|
|
@@ -2905,7 +2837,7 @@ function disableCustomerKeys(customerId) {
|
|
|
2905
2837
|
}
|
|
2906
2838
|
|
|
2907
2839
|
function verifyWebhookSignature(rawBody, signature) {
|
|
2908
|
-
if (!CONFIG.STRIPE_WEBHOOK_SECRET) return
|
|
2840
|
+
if (!CONFIG.STRIPE_WEBHOOK_SECRET) return true;
|
|
2909
2841
|
if (!signature || !rawBody) return false;
|
|
2910
2842
|
|
|
2911
2843
|
// Stripe signature format: t=<timestamp>,v1=<hmac>,...
|
|
@@ -2935,13 +2867,6 @@ function verifyWebhookSignature(rawBody, signature) {
|
|
|
2935
2867
|
|
|
2936
2868
|
async function handleWebhook(rawBody, signature) {
|
|
2937
2869
|
if (LOCAL_MODE()) return { handled: false, reason: 'local_mode' };
|
|
2938
|
-
if (!CONFIG.STRIPE_WEBHOOK_SECRET && !allowUnsignedStripeWebhooks()) {
|
|
2939
|
-
return {
|
|
2940
|
-
handled: false,
|
|
2941
|
-
reason: 'invalid_signature',
|
|
2942
|
-
error: 'STRIPE_WEBHOOK_SECRET is required before Stripe webhooks can be processed.',
|
|
2943
|
-
};
|
|
2944
|
-
}
|
|
2945
2870
|
let event;
|
|
2946
2871
|
try {
|
|
2947
2872
|
if (CONFIG.STRIPE_WEBHOOK_SECRET) {
|
|
@@ -3050,22 +2975,6 @@ async function handleWebhook(rawBody, signature) {
|
|
|
3050
2975
|
attribution,
|
|
3051
2976
|
});
|
|
3052
2977
|
}
|
|
3053
|
-
// Fire Plausible purchase event so the funnel poller can measure
|
|
3054
|
-
// end-to-end conversion: visitor → CTA → checkout → email → Stripe → purchase.
|
|
3055
|
-
// Fire-and-forget (never blocks the webhook response).
|
|
3056
|
-
void recordCheckoutFunnelEvent('purchase', {
|
|
3057
|
-
page: '/success',
|
|
3058
|
-
props: {
|
|
3059
|
-
sessionId: session.id,
|
|
3060
|
-
customerId,
|
|
3061
|
-
traceId: traceId || '',
|
|
3062
|
-
packId: packId || '',
|
|
3063
|
-
amount: session.amount_total != null ? String(session.amount_total) : '',
|
|
3064
|
-
currency: session.currency || '',
|
|
3065
|
-
...attribution,
|
|
3066
|
-
},
|
|
3067
|
-
});
|
|
3068
|
-
|
|
3069
2978
|
return {
|
|
3070
2979
|
handled: true,
|
|
3071
2980
|
action: 'provisioned_api_key',
|
|
@@ -3218,7 +3127,6 @@ module.exports = {
|
|
|
3218
3127
|
_buildTrialActivationEmail: buildTrialActivationEmail,
|
|
3219
3128
|
_sendTrialActivationEmail: sendTrialActivationEmail,
|
|
3220
3129
|
_resolveSubscriptionCheckoutSelection: resolveSubscriptionCheckoutSelection,
|
|
3221
|
-
_verifyActiveProductForPlan: verifyActiveProductForPlan,
|
|
3222
3130
|
_API_KEYS_PATH: () => CONFIG.API_KEYS_PATH,
|
|
3223
3131
|
_FUNNEL_LEDGER_PATH: () => CONFIG.FUNNEL_LEDGER_PATH,
|
|
3224
3132
|
_REVENUE_LEDGER_PATH: () => CONFIG.REVENUE_LEDGER_PATH,
|
|
@@ -5,11 +5,6 @@ const PROJECT_ROOT = path.resolve(__dirname, '..');
|
|
|
5
5
|
const DEFAULT_BUILD_METADATA_PATH = path.join(PROJECT_ROOT, 'config', 'build-metadata.json');
|
|
6
6
|
const BUILD_SHA_ENV_KEY = 'THUMBGATE_BUILD_SHA';
|
|
7
7
|
const BUILD_GENERATED_AT_ENV_KEY = 'THUMBGATE_BUILD_GENERATED_AT';
|
|
8
|
-
// Railway injects this automatically for GitHub-connected deployments: the git
|
|
9
|
-
// SHA of the commit that triggered the deploy. It is the ground truth for what
|
|
10
|
-
// code is actually live, and unlike THUMBGATE_BUILD_SHA it cannot drift (Railway
|
|
11
|
-
// sets it per deploy). https://docs.railway.com/reference/variables
|
|
12
|
-
const RAILWAY_GIT_COMMIT_SHA_ENV_KEY = 'RAILWAY_GIT_COMMIT_SHA';
|
|
13
8
|
|
|
14
9
|
function normalizeNullableText(value) {
|
|
15
10
|
if (typeof value !== 'string') {
|
|
@@ -21,69 +16,35 @@ function normalizeNullableText(value) {
|
|
|
21
16
|
}
|
|
22
17
|
|
|
23
18
|
function resolveBuildMetadata({ env = process.env, filePath } = {}) {
|
|
24
|
-
// Precedence: immutable JSON file (baked into Docker image at build time, so it
|
|
25
|
-
// ALWAYS matches the deployed code) wins over runtime env vars. Env vars are
|
|
26
|
-
// mutable Railway/host config that can drift — they shadowed the freshly-stamped
|
|
27
|
-
// SHA in prod on 2026-05-20 and made /health lie about the deployed commit.
|
|
28
|
-
// Fall back to env vars only when the file is missing or its values are null,
|
|
29
|
-
// and require an explicit SHA env var (not just a stray GENERATED_AT) before
|
|
30
|
-
// trusting the env branch.
|
|
31
19
|
const resolvedPath =
|
|
32
20
|
normalizeNullableText(filePath) ||
|
|
33
21
|
normalizeNullableText(env.THUMBGATE_BUILD_METADATA_PATH) ||
|
|
34
22
|
DEFAULT_BUILD_METADATA_PATH;
|
|
35
23
|
const envBuildSha = normalizeNullableText(env[BUILD_SHA_ENV_KEY]);
|
|
36
|
-
const railwayGitSha = normalizeNullableText(env[RAILWAY_GIT_COMMIT_SHA_ENV_KEY]);
|
|
37
24
|
const envGeneratedAt = normalizeNullableText(env[BUILD_GENERATED_AT_ENV_KEY]);
|
|
38
25
|
|
|
39
|
-
|
|
40
|
-
let fileGeneratedAt = null;
|
|
41
|
-
try {
|
|
42
|
-
const parsed = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
|
|
43
|
-
fileBuildSha = normalizeNullableText(parsed.buildSha);
|
|
44
|
-
fileGeneratedAt = normalizeNullableText(parsed.generatedAt);
|
|
45
|
-
} catch {
|
|
46
|
-
// file missing or unreadable — fall through to env branch
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (fileBuildSha) {
|
|
26
|
+
if (envBuildSha || envGeneratedAt) {
|
|
50
27
|
return {
|
|
51
28
|
path: resolvedPath,
|
|
52
|
-
buildSha:
|
|
53
|
-
generatedAt:
|
|
29
|
+
buildSha: envBuildSha,
|
|
30
|
+
generatedAt: envGeneratedAt,
|
|
54
31
|
};
|
|
55
32
|
}
|
|
56
33
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// has drifted in prod (stuck reporting an old commit while newer code was live,
|
|
60
|
-
// because RAILWAY_SYNC_VARIABLES is off and `railway up` stamping is unreliable).
|
|
61
|
-
// RAILWAY_GIT_COMMIT_SHA is injected by Railway per deploy, so it always matches
|
|
62
|
-
// the code actually serving traffic on a GitHub-connected service.
|
|
63
|
-
if (railwayGitSha) {
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
|
|
64
36
|
return {
|
|
65
37
|
path: resolvedPath,
|
|
66
|
-
buildSha:
|
|
67
|
-
generatedAt:
|
|
38
|
+
buildSha: normalizeNullableText(parsed.buildSha),
|
|
39
|
+
generatedAt: normalizeNullableText(parsed.generatedAt),
|
|
68
40
|
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Last resort: the workflow-managed env var. Only trust it when an explicit SHA
|
|
72
|
-
// is set. (Previously a bare GENERATED_AT with no SHA could short-circuit and
|
|
73
|
-
// return { buildSha: null }, losing both signals; now we require the SHA.)
|
|
74
|
-
if (envBuildSha) {
|
|
41
|
+
} catch {
|
|
75
42
|
return {
|
|
76
43
|
path: resolvedPath,
|
|
77
|
-
buildSha:
|
|
78
|
-
generatedAt:
|
|
44
|
+
buildSha: null,
|
|
45
|
+
generatedAt: null,
|
|
79
46
|
};
|
|
80
47
|
}
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
path: resolvedPath,
|
|
84
|
-
buildSha: null,
|
|
85
|
-
generatedAt: fileGeneratedAt || envGeneratedAt,
|
|
86
|
-
};
|
|
87
48
|
}
|
|
88
49
|
|
|
89
50
|
function writeBuildMetadataFile({ sha, outputPath, generatedAt = new Date().toISOString() }) {
|
|
@@ -144,7 +105,6 @@ if (require.main === module) {
|
|
|
144
105
|
module.exports = {
|
|
145
106
|
BUILD_GENERATED_AT_ENV_KEY,
|
|
146
107
|
BUILD_SHA_ENV_KEY,
|
|
147
|
-
RAILWAY_GIT_COMMIT_SHA_ENV_KEY,
|
|
148
108
|
DEFAULT_BUILD_METADATA_PATH,
|
|
149
109
|
resolveBuildMetadata,
|
|
150
110
|
writeBuildMetadataFile,
|
package/scripts/cli-feedback.js
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
const { captureFeedback } = require('./feedback-loop');
|
|
17
|
-
const { isGenericFeedbackText, normalizeFeedbackSignal } = require('./feedback-quality');
|
|
18
17
|
const { loadOptionalModule } = require('./private-core-boundary');
|
|
19
18
|
// `history-distiller` is a PRIVATE_CORE_MODULE — present in this checkout and
|
|
20
19
|
// in ThumbGate-Core, but intentionally excluded from the public npm tarball.
|
|
@@ -48,21 +47,15 @@ const RST = '\x1b[0m';
|
|
|
48
47
|
*/
|
|
49
48
|
function processInlineFeedback({ signal, context, chatHistory, whatWentWrong, whatWorked } = {}) {
|
|
50
49
|
const isDown = signal === 'down' || signal === 'negative';
|
|
51
|
-
const normalizedQualitySignal = normalizeFeedbackSignal(isDown ? 'down' : 'up');
|
|
52
|
-
const providedContext = String(context || '').trim();
|
|
53
|
-
const genericContext = isGenericFeedbackText(providedContext, normalizedQualitySignal);
|
|
54
|
-
const effectiveWhatWentWrong = whatWentWrong || (isDown && providedContext && !genericContext ? providedContext : undefined);
|
|
55
|
-
const effectiveWhatWorked = whatWorked || (!isDown && providedContext && !genericContext ? providedContext : undefined);
|
|
56
50
|
|
|
57
51
|
// 1. Capture the feedback
|
|
58
52
|
let feedbackResult;
|
|
59
53
|
try {
|
|
60
54
|
feedbackResult = captureFeedback({
|
|
61
55
|
signal: isDown ? 'down' : 'up',
|
|
62
|
-
context:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
whatWorked: effectiveWhatWorked,
|
|
56
|
+
context: context || (isDown ? 'Thumbs down from CLI' : 'Thumbs up from CLI'),
|
|
57
|
+
whatWentWrong: whatWentWrong || undefined,
|
|
58
|
+
whatWorked: whatWorked || undefined,
|
|
66
59
|
});
|
|
67
60
|
} catch (err) {
|
|
68
61
|
feedbackResult = { accepted: false, reason: err.message };
|
|
@@ -95,19 +88,14 @@ function formatCliOutput(result) {
|
|
|
95
88
|
if (result.feedbackResult && result.feedbackResult.accepted !== false) {
|
|
96
89
|
lines.push(`${isDown ? R : G}${BD}${isDown ? '👎 Thumbs down recorded' : '👍 Thumbs up recorded'}${RST}`);
|
|
97
90
|
const feedbackId = (result.feedbackResult.feedbackEvent && result.feedbackResult.feedbackEvent.id) || result.feedbackResult.id;
|
|
98
|
-
const memoryId = (result.feedbackResult.memoryRecord && result.feedbackResult.memoryRecord.id) || result.feedbackResult.memoryId;
|
|
99
91
|
if (feedbackId) {
|
|
100
|
-
lines.push(`${D}
|
|
101
|
-
if (memoryId) lines.push(`${D} Memory ID : ${memoryId}${RST}`);
|
|
92
|
+
lines.push(`${D} ID: ${feedbackId}${RST}`);
|
|
102
93
|
// Echo feedback ID to stderr so it's visible directly in the terminal,
|
|
103
94
|
// not hidden behind Claude Code's "ctrl+o to expand" MCP call collapse.
|
|
104
|
-
process.stderr.write(`✅ Feedback captured (${feedbackId}
|
|
95
|
+
process.stderr.write(`✅ Feedback captured (${feedbackId})\n`);
|
|
105
96
|
}
|
|
106
97
|
} else {
|
|
107
|
-
|
|
108
|
-
? ` ${result.feedbackResult.prompt || result.feedbackResult.message || ''}`.trimEnd()
|
|
109
|
-
: '';
|
|
110
|
-
lines.push(`${R}Feedback not accepted: ${(result.feedbackResult && result.feedbackResult.reason) || 'unknown'}${clarification ? ` — ${clarification}` : ''}${RST}`);
|
|
98
|
+
lines.push(`${R}Feedback not accepted: ${(result.feedbackResult && result.feedbackResult.reason) || 'unknown'}${RST}`);
|
|
111
99
|
}
|
|
112
100
|
|
|
113
101
|
// Distilled lesson (if thumbs down)
|