thumbgate 0.9.9 → 0.9.11

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 (160) hide show
  1. package/.claude-plugin/README.md +4 -4
  2. package/.claude-plugin/marketplace.json +4 -2
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +115 -312
  6. package/adapters/README.md +2 -2
  7. package/adapters/amp/skills/{rlhf-feedback → thumbgate-feedback}/SKILL.md +1 -1
  8. package/adapters/chatgpt/openapi.yaml +2 -2
  9. package/adapters/claude/.mcp.json +3 -3
  10. package/adapters/codex/config.toml +4 -4
  11. package/adapters/gemini/function-declarations.json +1 -1
  12. package/adapters/mcp/server-stdio.js +66 -6
  13. package/adapters/opencode/opencode.json +4 -2
  14. package/bin/cli.js +188 -39
  15. package/config/e2e-critical-flows.json +4 -0
  16. package/config/gates/default.json +74 -2
  17. package/config/github-about.json +1 -1
  18. package/config/mcp-allowlists.json +33 -6
  19. package/config/skill-packs/react-testing.json +1 -1
  20. package/config/tessl-tiles.json +3 -3
  21. package/openapi/openapi.yaml +2 -2
  22. package/package.json +23 -9
  23. package/plugins/amp-skill/INSTALL.md +3 -2
  24. package/plugins/amp-skill/SKILL.md +1 -0
  25. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  26. package/plugins/claude-codex-bridge/.mcp.json +5 -3
  27. package/plugins/claude-codex-bridge/README.md +1 -1
  28. package/plugins/claude-codex-bridge/skills/setup/SKILL.md +1 -1
  29. package/plugins/claude-skill/INSTALL.md +4 -3
  30. package/plugins/claude-skill/SKILL.md +1 -1
  31. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  32. package/plugins/codex-profile/.mcp.json +5 -3
  33. package/plugins/codex-profile/INSTALL.md +2 -2
  34. package/plugins/codex-profile/README.md +1 -1
  35. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  36. package/plugins/cursor-marketplace/README.md +5 -5
  37. package/plugins/cursor-marketplace/mcp.json +4 -2
  38. package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +1 -1
  39. package/plugins/cursor-marketplace/scripts/gate-check.sh +15 -5
  40. package/plugins/gemini-extension/INSTALL.md +4 -4
  41. package/plugins/opencode-profile/INSTALL.md +5 -5
  42. package/public/dashboard.html +15 -8
  43. package/public/index.html +134 -375
  44. package/public/js/buyer-intent.js +252 -0
  45. package/public/pro.html +1085 -0
  46. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  47. package/scripts/adk-consolidator.js +17 -5
  48. package/scripts/agent-readiness.js +3 -1
  49. package/scripts/agent-security-hardening.js +4 -4
  50. package/scripts/auto-promote-gates.js +8 -0
  51. package/scripts/auto-wire-hooks.js +105 -21
  52. package/scripts/billing.js +111 -7
  53. package/scripts/build-metadata.js +14 -0
  54. package/scripts/check-congruence.js +1 -1
  55. package/scripts/context-engine.js +2 -1
  56. package/scripts/daemon-manager.js +2 -2
  57. package/scripts/dashboard.js +2 -2
  58. package/scripts/data-governance.js +1 -1
  59. package/scripts/deploy-gcp.sh +1 -1
  60. package/scripts/deploy-policy.js +22 -4
  61. package/scripts/dispatch-brief.js +1 -1
  62. package/scripts/ensure-repo-bootstrap.js +1 -1
  63. package/scripts/feedback-attribution.js +22 -10
  64. package/scripts/feedback-fallback.js +3 -2
  65. package/scripts/feedback-inbox-read.js +1 -1
  66. package/scripts/feedback-loop.js +41 -3
  67. package/scripts/feedback-paths.js +8 -8
  68. package/scripts/feedback-schema.js +1 -1
  69. package/scripts/feedback-to-memory.js +2 -2
  70. package/scripts/filesystem-search.js +2 -2
  71. package/scripts/gates-engine.js +765 -34
  72. package/scripts/generate-paperbanana-diagrams.sh +3 -3
  73. package/scripts/github-about.js +1 -1
  74. package/scripts/gtm-revenue-loop.js +20 -1
  75. package/scripts/hook-runtime.js +89 -0
  76. package/scripts/hook-stop-self-score.sh +3 -3
  77. package/scripts/hook-thumbgate-cache-updater.js +98 -37
  78. package/scripts/hosted-config.js +12 -10
  79. package/scripts/hybrid-feedback-context.js +54 -13
  80. package/scripts/install-mcp.js +14 -1
  81. package/scripts/intent-router.js +1 -1
  82. package/scripts/internal-agent-bootstrap.js +1 -1
  83. package/scripts/lesson-inference.js +6 -1
  84. package/scripts/license.js +54 -16
  85. package/scripts/mcp-config.js +69 -7
  86. package/scripts/memory-migration.js +1 -1
  87. package/scripts/money-watcher.js +166 -16
  88. package/scripts/operational-integrity.js +480 -0
  89. package/scripts/optimize-context.js +1 -1
  90. package/scripts/perplexity-marketing.js +1 -1
  91. package/scripts/post-everywhere.js +7 -12
  92. package/scripts/post-to-x.js +1 -1
  93. package/scripts/pr-manager.js +14 -11
  94. package/scripts/problem-detail.js +10 -10
  95. package/scripts/profile-router.js +2 -0
  96. package/scripts/prompt-dlp.js +1 -0
  97. package/scripts/prove-adapters.js +6 -6
  98. package/scripts/prove-automation.js +1 -1
  99. package/scripts/prove-autoresearch.js +1 -1
  100. package/scripts/prove-claim-verification.js +3 -3
  101. package/scripts/prove-data-pipeline.js +5 -5
  102. package/scripts/prove-data-quality.js +1 -1
  103. package/scripts/prove-evolution.js +7 -7
  104. package/scripts/prove-harnesses.js +2 -2
  105. package/scripts/prove-lancedb.js +2 -2
  106. package/scripts/prove-local-intelligence.js +1 -1
  107. package/scripts/prove-loop-closure.js +1 -1
  108. package/scripts/prove-predictive-insights.js +2 -2
  109. package/scripts/prove-runtime.js +6 -6
  110. package/scripts/prove-seo-gsd.js +1 -1
  111. package/scripts/prove-settings.js +4 -4
  112. package/scripts/prove-subway-upgrades.js +1 -1
  113. package/scripts/prove-tessl.js +2 -2
  114. package/scripts/prove-xmemory.js +2 -2
  115. package/scripts/publish-decision.js +10 -0
  116. package/scripts/published-cli.js +34 -0
  117. package/scripts/rate-limiter.js +2 -2
  118. package/scripts/reddit-monitor-cron.sh +2 -2
  119. package/scripts/reminder-engine.js +1 -1
  120. package/scripts/schedule-manager.js +3 -3
  121. package/scripts/self-healing-check.js +1 -1
  122. package/scripts/shieldcortex-memory-firewall-runner.mjs +1 -1
  123. package/scripts/skill-quality-tracker.js +1 -1
  124. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  125. package/scripts/social-analytics/db/social-analytics.db-wal +0 -0
  126. package/scripts/social-analytics/engagement-audit.js +202 -0
  127. package/scripts/social-analytics/generate-instagram-card.js +1 -1
  128. package/scripts/social-analytics/instagram-thumbgate-post.js +5 -1
  129. package/scripts/social-analytics/install-growth-automation.js +114 -0
  130. package/scripts/social-analytics/publish-instagram-thumbgate.js +8 -2
  131. package/scripts/social-analytics/publish-thumbgate-launch.js +1 -1
  132. package/scripts/social-analytics/publishers/reddit.js +7 -12
  133. package/scripts/social-analytics/publishers/zernio.js +19 -0
  134. package/scripts/social-analytics/reconcile-thumbgate-campaign.js +165 -0
  135. package/scripts/social-analytics/schedule-thumbgate-campaign.js +275 -0
  136. package/scripts/social-analytics/sync-launch-assets.js +185 -0
  137. package/scripts/social-pipeline.js +2 -2
  138. package/scripts/social-post-hourly.js +185 -0
  139. package/scripts/social-quality-gate.js +119 -3
  140. package/scripts/social-reply-monitor.js +150 -34
  141. package/scripts/statusline-cache-path.js +27 -0
  142. package/scripts/statusline-meta.js +22 -0
  143. package/scripts/statusline.sh +24 -32
  144. package/scripts/sync-version.js +24 -12
  145. package/scripts/telemetry-analytics.js +4 -4
  146. package/scripts/tessl-export.js +1 -1
  147. package/scripts/test-coverage.js +20 -13
  148. package/scripts/thumbgate-search.js +2 -2
  149. package/scripts/tool-registry.js +98 -1
  150. package/scripts/train_from_feedback.py +1 -1
  151. package/scripts/user-profile.js +4 -4
  152. package/scripts/validate-feedback.js +1 -1
  153. package/scripts/vector-store.js +1 -1
  154. package/scripts/verification-loop.js +1 -1
  155. package/scripts/verify-run.js +1 -1
  156. package/scripts/weekly-auto-post.js +1 -1
  157. package/skills/{rlhf-feedback → thumbgate-feedback}/SKILL.md +1 -1
  158. package/src/api/server.js +291 -41
  159. package/scripts/__pycache__/train_from_feedback.cpython-314.pyc +0 -0
  160. package/scripts/social-analytics/db/social-analytics.db +0 -0
@@ -12,7 +12,6 @@
12
12
 
13
13
  const fs = require('fs');
14
14
  const path = require('path');
15
- const { GoogleGenAI } = require('@google/genai');
16
15
 
17
16
  const PROJECT_ROOT = path.join(__dirname, '..');
18
17
  const { getFeedbackPaths, readJSONL } = require('./feedback-loop');
@@ -22,7 +21,7 @@ const { trackEvent, shouldInjectReminder, injectReminder } = require('./reminder
22
21
  const { validateApiKey } = require('./billing');
23
22
 
24
23
  // Keep track of the last processed ID to avoid re-consolidating the exact same logs
25
- const STATE_FILE = process.env.ADK_STATE_FILE || path.join(PROJECT_ROOT, '.rlhf', 'adk-state.json');
24
+ const STATE_FILE = process.env.ADK_STATE_FILE || path.join(PROJECT_ROOT, '.thumbgate', 'adk-state.json');
26
25
 
27
26
  function ensureDir(dirPath) {
28
27
  if (!fs.existsSync(dirPath)) {
@@ -91,7 +90,20 @@ async function consolidateMemory() {
91
90
  return;
92
91
  }
93
92
 
94
- const ai = useFakeConsolidation ? null : new GoogleGenAI({ apiKey });
93
+ let ai = null;
94
+ if (!useFakeConsolidation) {
95
+ let GoogleGenAI;
96
+ try {
97
+ ({ GoogleGenAI } = require('@google/genai'));
98
+ } catch (error) {
99
+ if (error && error.code === 'MODULE_NOT_FOUND') {
100
+ console.warn('[ADK Consolidator] @google/genai is not installed. Skipping active consolidation.');
101
+ return;
102
+ }
103
+ throw error;
104
+ }
105
+ ai = new GoogleGenAI({ apiKey });
106
+ }
95
107
  const paths = getFeedbackPaths();
96
108
  const state = loadState();
97
109
 
@@ -193,7 +205,7 @@ Output ONLY valid JSON:
193
205
  if (shouldInjectReminder('guardrail_spike')) {
194
206
  const topRule = criticalInsights[0].rule;
195
207
  const reminderTurns = injectReminder([], 'guardrail_spike', { rule: topRule });
196
- const reminderPath = path.join(PROJECT_ROOT, '.rlhf', `reminder_${Date.now()}.json`);
208
+ const reminderPath = path.join(PROJECT_ROOT, '.thumbgate', `reminder_${Date.now()}.json`);
197
209
  fs.writeFileSync(reminderPath, JSON.stringify(reminderTurns[0], null, 2));
198
210
  console.log(`[ADK Consolidator] Emitted system reminder: ${reminderPath}`);
199
211
  }
@@ -202,7 +214,7 @@ Output ONLY valid JSON:
202
214
  // Emit A2UI components (New Model)
203
215
  result.consolidatedInsights.forEach(insight => {
204
216
  const proposal = createRuleProposal(insight.pattern, insight.rule, insight.severity);
205
- const a2uiPath = path.join(PROJECT_ROOT, '.rlhf', `a2ui_proposal_${Date.now()}.json`);
217
+ const a2uiPath = path.join(PROJECT_ROOT, '.thumbgate', `a2ui_proposal_${Date.now()}.json`);
206
218
  fs.writeFileSync(a2uiPath, JSON.stringify(proposal, null, 2));
207
219
  console.log(`[ADK Consolidator] Emitted A2UI Proposal: ${a2uiPath}`);
208
220
  });
@@ -20,6 +20,8 @@ const WRITE_CAPABLE_TOOLS = new Set([
20
20
  'evaluate_context_pack',
21
21
  'generate_skill',
22
22
  'satisfy_gate',
23
+ 'set_task_scope',
24
+ 'approve_protected_action',
23
25
  'track_action',
24
26
  'register_claim_gate',
25
27
  ]);
@@ -29,7 +31,7 @@ const BOOTSTRAP_FILES = [
29
31
  { id: 'claude', path: 'CLAUDE.md', required: true },
30
32
  { id: 'gemini', path: 'GEMINI.md', required: true },
31
33
  { id: 'mcp', path: '.mcp.json', required: true },
32
- { id: 'rlhfConfig', path: '.thumbgate/config.json', required: false },
34
+ { id: 'thumbgateConfig', path: '.thumbgate/config.json', required: false },
33
35
  ];
34
36
 
35
37
  const MCP_PROFILE_TIERS = {
@@ -84,10 +84,10 @@ function getCredentialAudit({ periodHours = 24 } = {}) {
84
84
 
85
85
  // MCP profile tool allowlists (loaded from config or defaults)
86
86
  const PROFILE_ALLOWLISTS = {
87
- essential: new Set(['capture_feedback', 'recall', 'search_lessons', 'search_rlhf', 'prevention_rules', 'enforcement_matrix', 'feedback_stats', 'estimate_uncertainty', 'org_dashboard']),
88
- readonly: new Set(['recall', 'feedback_summary', 'search_lessons', 'verify_claim', 'gate_stats', 'search_rlhf', 'feedback_stats', 'estimate_uncertainty', 'org_dashboard']),
89
- locked: new Set(['feedback_summary', 'search_lessons', 'diagnose_failure', 'list_intents', 'plan_intent', 'list_harnesses', 'verify_claim']),
90
- commerce: new Set(['capture_feedback', 'recall', 'search_rlhf', 'commerce_recall', 'track_action', 'verify_claim', 'feedback_stats']),
87
+ essential: new Set(['capture_feedback', 'recall', 'search_lessons', 'search_thumbgate', 'prevention_rules', 'enforcement_matrix', 'feedback_stats', 'estimate_uncertainty', 'org_dashboard', 'set_task_scope', 'get_scope_state', 'set_branch_governance', 'get_branch_governance', 'approve_protected_action', 'check_operational_integrity']),
88
+ readonly: new Set(['recall', 'feedback_summary', 'search_lessons', 'verify_claim', 'gate_stats', 'search_thumbgate', 'feedback_stats', 'estimate_uncertainty', 'org_dashboard', 'get_scope_state', 'get_branch_governance', 'check_operational_integrity']),
89
+ locked: new Set(['feedback_summary', 'search_lessons', 'diagnose_failure', 'list_intents', 'plan_intent', 'list_harnesses', 'verify_claim', 'get_scope_state', 'get_branch_governance', 'check_operational_integrity']),
90
+ commerce: new Set(['capture_feedback', 'recall', 'search_thumbgate', 'commerce_recall', 'track_action', 'verify_claim', 'feedback_stats', 'set_task_scope', 'get_scope_state', 'set_branch_governance', 'get_branch_governance', 'approve_protected_action', 'check_operational_integrity']),
91
91
  };
92
92
 
93
93
  /**
@@ -13,6 +13,14 @@ const WINDOW_DAYS = 30;
13
13
  const NEG_SIGNALS = new Set(['negative', 'negative_strong', 'down', 'thumbs_down']);
14
14
 
15
15
  function getFeedbackLogPath() {
16
+ if (process.env.THUMBGATE_FEEDBACK_DIR) {
17
+ return path.join(process.env.THUMBGATE_FEEDBACK_DIR, 'feedback-log.jsonl');
18
+ }
19
+ const localFallback = path.join(process.cwd(), '.thumbgate', 'feedback-log.jsonl');
20
+ const localClaude = path.join(process.cwd(), '.claude', 'memory', 'feedback', 'feedback-log.jsonl');
21
+ if (fs.existsSync(localFallback)) return localFallback;
22
+ if (fs.existsSync(localClaude)) return localClaude;
23
+ return localFallback; // default even if doesn't exist
16
24
  return path.join(resolveFeedbackDir(), 'feedback-log.jsonl');
17
25
  }
18
26
 
@@ -15,39 +15,31 @@
15
15
 
16
16
  const fs = require('fs');
17
17
  const path = require('path');
18
-
19
- const PKG_ROOT = path.join(__dirname, '..');
20
-
21
- function quoteShellPath(filePath) {
22
- return `"${String(filePath).replace(/"/g, '\\"')}"`;
23
- }
18
+ const {
19
+ cacheUpdateHookCommand,
20
+ preToolHookCommand,
21
+ sessionStartHookCommand,
22
+ statuslineCommand,
23
+ userPromptHookCommand,
24
+ } = require('./hook-runtime');
24
25
 
25
26
  function getHome() {
26
27
  return process.env.HOME || process.env.USERPROFILE || '';
27
28
  }
28
29
 
29
30
  // --- Hook definitions ---
30
-
31
- function preToolHookCommand() {
32
- return `bash ${quoteShellPath(path.join(PKG_ROOT, 'scripts', 'generate-pretool-hook.sh'))}`;
33
- }
34
-
35
- function userPromptHookCommand() {
36
- return `bash ${quoteShellPath(path.join(PKG_ROOT, 'scripts', 'hook-auto-capture.sh'))}`;
37
- }
38
-
39
- function sessionStartHookCommand() {
40
- return `bash ${quoteShellPath(path.join(PKG_ROOT, 'scripts', 'rlhf_session_start.sh'))}`;
41
- }
42
-
43
31
  const CLAUDE_HOOKS = {
44
32
  PreToolUse: {
45
- matcher: 'Bash',
33
+ matcher: 'Bash|Edit|Write|MultiEdit',
46
34
  hooks: [{ type: 'command', command: preToolHookCommand() }],
47
35
  },
48
36
  UserPromptSubmit: {
49
37
  hooks: [{ type: 'command', command: userPromptHookCommand() }],
50
38
  },
39
+ PostToolUse: {
40
+ matcher: 'mcp__thumbgate__feedback_stats|mcp__thumbgate__dashboard',
41
+ hooks: [{ type: 'command', command: cacheUpdateHookCommand() }],
42
+ },
51
43
  SessionStart: {
52
44
  hooks: [{ type: 'command', command: sessionStartHookCommand() }],
53
45
  },
@@ -78,6 +70,10 @@ function claudeSettingsPath() {
78
70
  return path.join(getHome(), '.claude', 'settings.local.json');
79
71
  }
80
72
 
73
+ function claudeSharedSettingsPath() {
74
+ return path.join(getHome(), '.claude', 'settings.json');
75
+ }
76
+
81
77
  function loadJsonFile(filePath) {
82
78
  if (!fs.existsSync(filePath)) return null;
83
79
  try {
@@ -96,17 +92,67 @@ function hookAlreadyPresent(hookArray, command) {
96
92
  );
97
93
  }
98
94
 
95
+ function pruneLegacyHookEntries(hookArray, expectedCommand, legacyPattern) {
96
+ if (!Array.isArray(hookArray)) {
97
+ return { hooks: [], removed: false };
98
+ }
99
+
100
+ let removed = false;
101
+ const hooks = hookArray.filter((entry) => {
102
+ const entryHooks = Array.isArray(entry && entry.hooks) ? entry.hooks : [];
103
+ const shouldRemove = entryHooks.some((hook) => {
104
+ const command = hook && typeof hook.command === 'string' ? hook.command : '';
105
+ return command !== expectedCommand && legacyPattern.test(command);
106
+ });
107
+ if (shouldRemove) {
108
+ removed = true;
109
+ return false;
110
+ }
111
+ return true;
112
+ });
113
+
114
+ return { hooks, removed };
115
+ }
116
+
117
+ function syncClaudeStatusLine(settingsPath, desiredStatusLine, dryRun) {
118
+ const settings = loadJsonFile(settingsPath) || {};
119
+ if (settings.statusLine && settings.statusLine.command === desiredStatusLine) {
120
+ return false;
121
+ }
122
+
123
+ settings.statusLine = { type: 'command', command: desiredStatusLine };
124
+ if (!dryRun) {
125
+ const dir = path.dirname(settingsPath);
126
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
127
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
128
+ }
129
+ return true;
130
+ }
131
+
99
132
  function wireClaudeHooks(options) {
100
133
  const settingsPath = options.settingsPath || claudeSettingsPath();
134
+ const sharedSettingsPath = options.sharedSettingsPath || claudeSharedSettingsPath();
101
135
  const dryRun = options.dryRun || false;
136
+ const desiredStatusLine = statuslineCommand();
102
137
 
103
138
  let settings = loadJsonFile(settingsPath) || {};
104
139
  settings.hooks = settings.hooks || {};
105
140
 
106
141
  const added = [];
142
+ const legacyPatterns = {
143
+ PreToolUse: /(generate-pretool-hook\.sh|\bgate-check\b)/,
144
+ UserPromptSubmit: /(hook-auto-capture\.sh|hook-auto-capture\b)/,
145
+ PostToolUse: /(hook-thumbgate-cache-updater|cache-update\b)/,
146
+ SessionStart: /(thumbgate_session_start\.sh|session-start\b)/,
147
+ };
107
148
 
108
149
  for (const [lifecycle, hookDef] of Object.entries(CLAUDE_HOOKS)) {
109
150
  const hookCommand = hookDef.hooks[0].command;
151
+ const pruned = pruneLegacyHookEntries(settings.hooks[lifecycle], hookCommand, legacyPatterns[lifecycle]);
152
+ settings.hooks[lifecycle] = pruned.hooks;
153
+ if (pruned.removed) {
154
+ added.push({ lifecycle, command: `${hookCommand} (replaced legacy ThumbGate hook)` });
155
+ }
110
156
 
111
157
  if (hookAlreadyPresent(settings.hooks[lifecycle], hookCommand)) {
112
158
  continue;
@@ -122,15 +168,41 @@ function wireClaudeHooks(options) {
122
168
  }
123
169
 
124
170
  if (added.length === 0) {
125
- return { changed: false, settingsPath, added: [] };
171
+ if (!settings.statusLine || settings.statusLine.command !== desiredStatusLine) {
172
+ if (!dryRun) {
173
+ const dir = path.dirname(settingsPath);
174
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
175
+ }
176
+ settings.statusLine = { type: 'command', command: desiredStatusLine };
177
+ if (!dryRun) {
178
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
179
+ }
180
+ const addedEntries = [{ lifecycle: 'statusLine', command: desiredStatusLine }];
181
+ if (syncClaudeStatusLine(sharedSettingsPath, desiredStatusLine, dryRun)) {
182
+ addedEntries.push({ lifecycle: 'statusLine', command: `${desiredStatusLine} (synced ~/.claude/settings.json)` });
183
+ }
184
+ return { changed: true, settingsPath, added: addedEntries };
185
+ }
186
+ const sharedStatusChanged = syncClaudeStatusLine(sharedSettingsPath, desiredStatusLine, dryRun);
187
+ return {
188
+ changed: sharedStatusChanged,
189
+ settingsPath,
190
+ added: sharedStatusChanged ? [{ lifecycle: 'statusLine', command: `${desiredStatusLine} (synced ~/.claude/settings.json)` }] : [],
191
+ };
126
192
  }
127
193
 
194
+ settings.statusLine = { type: 'command', command: desiredStatusLine };
195
+
128
196
  if (!dryRun) {
129
197
  const dir = path.dirname(settingsPath);
130
198
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
131
199
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
132
200
  }
133
201
 
202
+ if (syncClaudeStatusLine(sharedSettingsPath, desiredStatusLine, dryRun)) {
203
+ added.push({ lifecycle: 'statusLine', command: `${desiredStatusLine} (synced ~/.claude/settings.json)` });
204
+ }
205
+
134
206
  return { changed: true, settingsPath, added };
135
207
  }
136
208
 
@@ -151,6 +223,11 @@ function wireCodexHooks(options) {
151
223
  const preToolCmd = preToolHookCommand();
152
224
  const userPromptCmd = userPromptHookCommand();
153
225
 
226
+ const preToolPruned = pruneLegacyHookEntries(config.hooks.PreToolUse, preToolCmd, /(generate-pretool-hook\.sh|\bgate-check\b)/);
227
+ config.hooks.PreToolUse = preToolPruned.hooks;
228
+ const userPromptPruned = pruneLegacyHookEntries(config.hooks.UserPromptSubmit, userPromptCmd, /(hook-auto-capture\.sh|hook-auto-capture\b)/);
229
+ config.hooks.UserPromptSubmit = userPromptPruned.hooks;
230
+
154
231
  if (!hookAlreadyPresent(config.hooks.PreToolUse, preToolCmd)) {
155
232
  config.hooks.PreToolUse = config.hooks.PreToolUse || [];
156
233
  config.hooks.PreToolUse.push({
@@ -198,6 +275,11 @@ function wireGeminiHooks(options) {
198
275
  const preToolCmd = preToolHookCommand();
199
276
  const userPromptCmd = userPromptHookCommand();
200
277
 
278
+ const preToolPruned = pruneLegacyHookEntries(settings.hooks.PreToolUse, preToolCmd, /(generate-pretool-hook\.sh|\bgate-check\b)/);
279
+ settings.hooks.PreToolUse = preToolPruned.hooks;
280
+ const userPromptPruned = pruneLegacyHookEntries(settings.hooks.UserPromptSubmit, userPromptCmd, /(hook-auto-capture\.sh|hook-auto-capture\b)/);
281
+ settings.hooks.UserPromptSubmit = userPromptPruned.hooks;
282
+
201
283
  if (!hookAlreadyPresent(settings.hooks.PreToolUse, preToolCmd)) {
202
284
  settings.hooks.PreToolUse = settings.hooks.PreToolUse || [];
203
285
  settings.hooks.PreToolUse.push({
@@ -286,8 +368,10 @@ module.exports = {
286
368
  loadJsonFile,
287
369
  parseFlags,
288
370
  claudeSettingsPath,
371
+ claudeSharedSettingsPath,
289
372
  codexConfigPath,
290
373
  geminiSettingsPath,
374
+ syncClaudeStatusLine,
291
375
  CLAUDE_HOOKS,
292
376
  preToolHookCommand,
293
377
  userPromptHookCommand,
@@ -16,12 +16,11 @@ function withTimeout(promise, ms = STRIPE_TIMEOUT_MS) {
16
16
  const fs = require('fs');
17
17
  const path = require('path');
18
18
  const crypto = require('crypto');
19
- const Stripe = require('stripe');
20
19
  const { createTraceId } = require('./hosted-config');
21
20
  const {
22
21
  getFeedbackPaths,
23
22
  getLegacyFeedbackDir,
24
- getRlhfFeedbackDir,
23
+ getFallbackFeedbackDir,
25
24
  resolveFallbackArtifactPath,
26
25
  } = require('./feedback-paths');
27
26
  const { getTelemetryAnalytics, getTelemetrySourceDiagnostics } = require('./telemetry-analytics');
@@ -71,6 +70,9 @@ const CONFIG = {
71
70
  get LOCAL_CHECKOUT_SESSIONS_PATH() {
72
71
  return process.env._TEST_LOCAL_CHECKOUT_SESSIONS_PATH || path.join(getFeedbackPaths().FEEDBACK_DIR, 'local-checkout-sessions.json');
73
72
  },
73
+ get NEWSLETTER_SUBSCRIBERS_PATH() {
74
+ return process.env._TEST_NEWSLETTER_SUBSCRIBERS_PATH || path.join(getFeedbackPaths().FEEDBACK_DIR, 'newsletter-subscribers.jsonl');
75
+ },
74
76
  CREDIT_PACKS: {
75
77
  'mistake-free-starter': {
76
78
  id: 'mistake-free-starter',
@@ -89,11 +91,27 @@ function resolveLegacyBillingPath(fileName) {
89
91
  }
90
92
 
91
93
  let _stripeClient = null;
94
+ let _stripeCtor = null;
95
+
96
+ function getStripeConstructor() {
97
+ if (_stripeCtor) return _stripeCtor;
98
+ try {
99
+ _stripeCtor = require('stripe');
100
+ return _stripeCtor;
101
+ } catch (error) {
102
+ if (error && error.code === 'MODULE_NOT_FOUND') {
103
+ throw new Error('stripe package is not installed. Live billing features are unavailable.');
104
+ }
105
+ throw error;
106
+ }
107
+ }
108
+
92
109
  function getStripeClient() {
93
110
  if (!_stripeClient) {
94
111
  if (!CONFIG.STRIPE_SECRET_KEY) {
95
112
  throw new Error('STRIPE_SECRET_KEY is missing. Stripe client cannot be initialized.');
96
113
  }
114
+ const Stripe = getStripeConstructor();
97
115
  _stripeClient = new Stripe(CONFIG.STRIPE_SECRET_KEY);
98
116
  }
99
117
  return _stripeClient;
@@ -297,6 +315,11 @@ function buildBillingSourceDiagnostics(feedbackDir) {
297
315
  legacyPath: resolveLegacyBillingPath('local-checkout-sessions.json'),
298
316
  mode: 'fallback',
299
317
  });
318
+ const newsletterSubscribers = describeDataFile({
319
+ primaryPath: CONFIG.NEWSLETTER_SUBSCRIBERS_PATH,
320
+ legacyPath: resolveLegacyBillingPath('newsletter-subscribers.jsonl'),
321
+ mode: 'fallback',
322
+ });
300
323
  const telemetry = getTelemetrySourceDiagnostics(feedbackDir);
301
324
  const warnings = [
302
325
  ...telemetry.warnings,
@@ -338,7 +361,7 @@ function buildBillingSourceDiagnostics(feedbackDir) {
338
361
  ));
339
362
  }
340
363
 
341
- const mixedRoots = [keyStore, funnelLedger, revenueLedger, checkoutSessions, telemetry]
364
+ const mixedRoots = [keyStore, funnelLedger, revenueLedger, checkoutSessions, newsletterSubscribers, telemetry]
342
365
  .some((descriptor) => descriptor.mixedRoots || descriptor.activeMode === 'legacy_fallback');
343
366
  if (mixedRoots) {
344
367
  warnings.push(buildSourceWarning(
@@ -349,7 +372,7 @@ function buildBillingSourceDiagnostics(feedbackDir) {
349
372
 
350
373
  return {
351
374
  feedbackDir,
352
- rlhfFeedbackDir: getRlhfFeedbackDir(),
375
+ fallbackFeedbackDir: getFallbackFeedbackDir(),
353
376
  legacyFeedbackDir: getLegacyFeedbackDir(),
354
377
  mixedRoots,
355
378
  files: {
@@ -357,6 +380,7 @@ function buildBillingSourceDiagnostics(feedbackDir) {
357
380
  funnelLedger,
358
381
  revenueLedger,
359
382
  checkoutSessions,
383
+ newsletterSubscribers,
360
384
  telemetry,
361
385
  },
362
386
  warnings,
@@ -622,6 +646,13 @@ function loadRevenueLedger() {
622
646
  );
623
647
  }
624
648
 
649
+ function loadNewsletterSubscribers() {
650
+ return loadJsonlRecords(
651
+ CONFIG.NEWSLETTER_SUBSCRIBERS_PATH,
652
+ resolveLegacyBillingPath('newsletter-subscribers.jsonl')
653
+ );
654
+ }
655
+
625
656
  function resolveRevenueLedgerFilePath() {
626
657
  const primary = CONFIG.REVENUE_LEDGER_PATH;
627
658
  const legacy = resolveLegacyBillingPath('revenue-events.jsonl');
@@ -1331,6 +1362,11 @@ function getBusinessAnalytics(options = {}) {
1331
1362
  analyticsWindow,
1332
1363
  (entry) => entry && entry.submittedAt
1333
1364
  );
1365
+ const newsletterSubscribers = filterEntriesForWindow(
1366
+ loadNewsletterSubscribers(),
1367
+ analyticsWindow,
1368
+ (entry) => entry && entry.subscribedAt
1369
+ );
1334
1370
  const funnel = getFunnelAnalytics({ ...analyticsWindow, extraRevenueEvents });
1335
1371
  const acquisitionEvents = events.filter((entry) => entry && entry.stage === 'acquisition');
1336
1372
  const paidEvents = events.filter((entry) => entry && entry.stage === 'paid');
@@ -1544,6 +1580,17 @@ function getBusinessAnalytics(options = {}) {
1544
1580
  let workflowSprintLeadLatestAt = null;
1545
1581
  let workflowSprintLeadContactable = 0;
1546
1582
  let qualifiedWorkflowSprintLeadCount = 0;
1583
+ const newsletterBySource = {};
1584
+ const newsletterByCampaign = {};
1585
+ const newsletterByCreator = {};
1586
+ const newsletterByCommunity = {};
1587
+ const newsletterByPostId = {};
1588
+ const newsletterByCommentId = {};
1589
+ const newsletterByCampaignVariant = {};
1590
+ const newsletterByOfferCode = {};
1591
+ const newsletterSubscriberKeys = new Set();
1592
+ let newsletterLatest = null;
1593
+ let newsletterLatestAt = null;
1547
1594
 
1548
1595
  for (const entry of workflowSprintLeads) {
1549
1596
  if (!entry || typeof entry !== 'object') continue;
@@ -1584,6 +1631,46 @@ function getBusinessAnalytics(options = {}) {
1584
1631
  }
1585
1632
  }
1586
1633
 
1634
+ for (const entry of newsletterSubscribers) {
1635
+ if (!entry || typeof entry !== 'object') continue;
1636
+ const attribution = extractAttribution({
1637
+ ...sanitizeMetadata(entry.attribution || {}),
1638
+ ...sanitizeMetadata(entry),
1639
+ });
1640
+ incrementCounter(newsletterBySource, resolveAttributionSource(attribution, entry.source || 'newsletter'));
1641
+ incrementCounter(newsletterByCampaign, resolveAttributionCampaign(attribution));
1642
+ incrementCounter(newsletterByCreator, attribution.creator);
1643
+ incrementCounter(newsletterByCommunity, attribution.community);
1644
+ incrementCounter(newsletterByPostId, attribution.postId);
1645
+ incrementCounter(newsletterByCommentId, attribution.commentId);
1646
+ incrementCounter(newsletterByCampaignVariant, attribution.campaignVariant);
1647
+ incrementCounter(newsletterByOfferCode, attribution.offerCode);
1648
+
1649
+ newsletterSubscriberKeys.add(
1650
+ pickFirstText(
1651
+ entry.email,
1652
+ entry.acquisitionId,
1653
+ entry.visitorId,
1654
+ entry.sessionId,
1655
+ entry.subscribedAt
1656
+ )
1657
+ );
1658
+
1659
+ if (!newsletterLatestAt || String(entry.subscribedAt || '') > newsletterLatestAt) {
1660
+ newsletterLatestAt = entry.subscribedAt || null;
1661
+ newsletterLatest = {
1662
+ email: entry.email || null,
1663
+ subscribedAt: entry.subscribedAt || null,
1664
+ source: resolveAttributionSource(attribution, entry.source || 'newsletter'),
1665
+ campaign: resolveAttributionCampaign(attribution),
1666
+ creator: attribution.creator || null,
1667
+ community: attribution.community || null,
1668
+ landingPath: pickFirstText(entry.landingPath, attribution.landingPath),
1669
+ referrerHost: entry.referrerHost || null,
1670
+ };
1671
+ }
1672
+ }
1673
+
1587
1674
  const unreconciledPaidEvents = paidEvents.filter((entry) => {
1588
1675
  const eventKey = resolvePaidProviderEventKey(entry);
1589
1676
  if (!eventKey) return true;
@@ -1603,6 +1690,7 @@ function getBusinessAnalytics(options = {}) {
1603
1690
  checkoutLookupFailures: telemetry.ctas ? telemetry.ctas.lookupFailures || 0 : 0,
1604
1691
  buyerLossFeedback: telemetry.buyerLoss ? telemetry.buyerLoss.totalSignals || 0 : 0,
1605
1692
  seoLandingViews: telemetry.seo ? telemetry.seo.landingViews || 0 : 0,
1693
+ newsletterSignups: newsletterSubscribers.length,
1606
1694
  };
1607
1695
 
1608
1696
  const operatorGeneratedAcquisition = {
@@ -1632,6 +1720,7 @@ function getBusinessAnalytics(options = {}) {
1632
1720
  tracksInvoices: false,
1633
1721
  tracksAttribution: true,
1634
1722
  tracksWorkflowSprintLeads: true,
1723
+ tracksNewsletterSubscribers: true,
1635
1724
  providerCoverage: {
1636
1725
  stripe: processorReconciledOrders > 0 ? 'booked_revenue+processor_reconciled' : 'booked_revenue',
1637
1726
  githubMarketplace: 'webhook_or_configured_plan_prices',
@@ -1701,6 +1790,20 @@ function getBusinessAnalytics(options = {}) {
1701
1790
  byCreator: qualifiedWorkflowSprintLeadByCreator,
1702
1791
  },
1703
1792
  },
1793
+ newsletter: {
1794
+ total: newsletterSubscribers.length,
1795
+ uniqueSubscribers: newsletterSubscriberKeys.size,
1796
+ bySource: newsletterBySource,
1797
+ byCampaign: newsletterByCampaign,
1798
+ byCreator: newsletterByCreator,
1799
+ byCommunity: newsletterByCommunity,
1800
+ byPostId: newsletterByPostId,
1801
+ byCommentId: newsletterByCommentId,
1802
+ byCampaignVariant: newsletterByCampaignVariant,
1803
+ byOfferCode: newsletterByOfferCode,
1804
+ latestSubscribedAt: newsletterLatestAt,
1805
+ latestSubscriber: newsletterLatest,
1806
+ },
1704
1807
  attribution: {
1705
1808
  acquisitionBySource: signupsBySource,
1706
1809
  acquisitionByCampaign: signupsByCampaign,
@@ -1822,6 +1925,7 @@ function getBillingSummary(options = {}) {
1822
1925
  signups: business.signups,
1823
1926
  revenue: business.revenue,
1824
1927
  pipeline: business.pipeline,
1928
+ newsletter: business.newsletter,
1825
1929
  attribution: business.attribution,
1826
1930
  trafficMetrics: business.trafficMetrics,
1827
1931
  operatorGeneratedAcquisition: business.operatorGeneratedAcquisition,
@@ -2078,7 +2182,7 @@ function provisionApiKey(customerId, opts = {}) {
2078
2182
  return { key, customerId, createdAt: meta.createdAt, installId: meta.installId || null, reused: true, remainingCredits: meta.remainingCredits };
2079
2183
  }
2080
2184
 
2081
- const key = `rlhf_${crypto.randomBytes(16).toString('hex')}`;
2185
+ const key = `tg_${crypto.randomBytes(16).toString('hex')}`;
2082
2186
  const createdAt = new Date().toISOString();
2083
2187
  store.keys[key] = {
2084
2188
  customerId,
@@ -2101,7 +2205,7 @@ function rotateApiKey(oldKey) {
2101
2205
 
2102
2206
  meta.active = false;
2103
2207
  meta.disabledAt = new Date().toISOString();
2104
- const newKey = `rlhf_${crypto.randomBytes(16).toString('hex')}`;
2208
+ const newKey = `tg_${crypto.randomBytes(16).toString('hex')}`;
2105
2209
  store.keys[newKey] = {
2106
2210
  customerId: meta.customerId,
2107
2211
  active: true,
@@ -2426,7 +2530,7 @@ function handleGithubWebhook(event) {
2426
2530
  }
2427
2531
 
2428
2532
  module.exports = {
2429
- CONFIG, createCheckoutSession, getCheckoutSessionStatus, provisionApiKey, rotateApiKey, validateApiKey, recordUsage, disableCustomerKeys, handleWebhook, verifyWebhookSignature, verifyGithubWebhookSignature, handleGithubWebhook, loadKeyStore, appendFunnelEvent, appendRevenueEvent, loadFunnelLedger, loadRevenueLedger, loadResolvedRevenueEvents, getFunnelAnalytics, getBusinessAnalytics, getBillingSummary, getBillingSummaryLive, listStripeReconciledRevenueEvents, repairGithubMarketplaceRevenueLedger,
2533
+ CONFIG, createCheckoutSession, getCheckoutSessionStatus, provisionApiKey, rotateApiKey, validateApiKey, recordUsage, disableCustomerKeys, handleWebhook, verifyWebhookSignature, verifyGithubWebhookSignature, handleGithubWebhook, loadKeyStore, appendFunnelEvent, appendRevenueEvent, loadFunnelLedger, loadRevenueLedger, loadNewsletterSubscribers, loadResolvedRevenueEvents, getFunnelAnalytics, getBusinessAnalytics, getBillingSummary, getBillingSummaryLive, listStripeReconciledRevenueEvents, repairGithubMarketplaceRevenueLedger,
2430
2534
  _buildCheckoutSessionPayload: buildCheckoutSessionPayload,
2431
2535
  _resolveSubscriptionCheckoutSelection: resolveSubscriptionCheckoutSelection,
2432
2536
  _API_KEYS_PATH: () => CONFIG.API_KEYS_PATH,
@@ -3,6 +3,8 @@ const path = require('path');
3
3
 
4
4
  const PROJECT_ROOT = path.resolve(__dirname, '..');
5
5
  const DEFAULT_BUILD_METADATA_PATH = path.join(PROJECT_ROOT, 'config', 'build-metadata.json');
6
+ const BUILD_SHA_ENV_KEY = 'THUMBGATE_BUILD_SHA';
7
+ const BUILD_GENERATED_AT_ENV_KEY = 'THUMBGATE_BUILD_GENERATED_AT';
6
8
 
7
9
  function normalizeNullableText(value) {
8
10
  if (typeof value !== 'string') {
@@ -18,6 +20,16 @@ function resolveBuildMetadata({ env = process.env, filePath } = {}) {
18
20
  normalizeNullableText(filePath) ||
19
21
  normalizeNullableText(env.THUMBGATE_BUILD_METADATA_PATH) ||
20
22
  DEFAULT_BUILD_METADATA_PATH;
23
+ const envBuildSha = normalizeNullableText(env[BUILD_SHA_ENV_KEY]);
24
+ const envGeneratedAt = normalizeNullableText(env[BUILD_GENERATED_AT_ENV_KEY]);
25
+
26
+ if (envBuildSha || envGeneratedAt) {
27
+ return {
28
+ path: resolvedPath,
29
+ buildSha: envBuildSha,
30
+ generatedAt: envGeneratedAt,
31
+ };
32
+ }
21
33
 
22
34
  try {
23
35
  const parsed = JSON.parse(fs.readFileSync(resolvedPath, 'utf8'));
@@ -91,6 +103,8 @@ if (require.main === module) {
91
103
  }
92
104
 
93
105
  module.exports = {
106
+ BUILD_GENERATED_AT_ENV_KEY,
107
+ BUILD_SHA_ENV_KEY,
94
108
  DEFAULT_BUILD_METADATA_PATH,
95
109
  resolveBuildMetadata,
96
110
  writeBuildMetadataFile,
@@ -126,7 +126,7 @@ async function main() {
126
126
  'README.md contains unresolved merge conflict markers'
127
127
  );
128
128
  check(
129
- landingHtml.includes('doesn\'t touch the model') || landingHtml.includes('different from RLHF-the-concept'),
129
+ landingHtml.includes('doesn\'t touch the model') || landingHtml.includes('different from model-training feedback loops'),
130
130
  'public/index.html missing honest disclaimer (FAQ or inline)'
131
131
  );
132
132
  check(
@@ -9,6 +9,7 @@
9
9
  * topical bundles and route queries to the most relevant subset. This reduces
10
10
  * MCP tool calls and context window consumption.
11
11
  *
12
+ * Ported from Subway_RN_Demo/scripts/context-engine.js for ThumbGate.
12
13
  * Ported from Subway_RN_Demo/scripts/context-engine.js for thumbgate.
13
14
  * PATH: PROJECT_ROOT = path.join(__dirname, '..') — 1 level up from scripts/
14
15
  */
@@ -311,7 +312,7 @@ function routeQuery(query, indexPath, topN) {
311
312
  if (lowerQuery.includes('build') || lowerQuery.includes('ci')) intentBoost = 'ci-cd';
312
313
  if (lowerQuery.includes('security') || lowerQuery.includes('audit')) intentBoost = 'security';
313
314
  if (lowerQuery.includes('mobile') || lowerQuery.includes('android')) intentBoost = 'mobile-dev';
314
- if (lowerQuery.includes('memory') || lowerQuery.includes('rlhf')) intentBoost = 'mcp-ai';
315
+ if (lowerQuery.includes('memory') || lowerQuery.includes('thumbgate')) intentBoost = 'mcp-ai';
315
316
 
316
317
  // Step 2: Contextual Ranking
317
318
  const queryTokens = query
@@ -6,11 +6,11 @@ const path = require('path');
6
6
  const { execSync } = require('child_process');
7
7
  const os = require('os');
8
8
 
9
- const LABEL = 'com.thumbgate.thumbgate';
9
+ const LABEL = 'com.thumbgate.daemon';
10
10
  const PLIST_PATH = path.join(os.homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
11
11
  const NODE_PATH = process.execPath;
12
12
  const GATEWAY_BIN = path.join(__dirname, '..', 'bin', 'cli.js');
13
- const LOG_DIR = path.join(os.homedir(), '.rlhf', 'logs');
13
+ const LOG_DIR = path.join(os.homedir(), '.thumbgate', 'logs');
14
14
 
15
15
  function generatePlist() {
16
16
  return `<?xml version="1.0" encoding="UTF-8"?>
@@ -105,7 +105,7 @@ function computeGateStats() {
105
105
  const autoGatesPath = getAutoGatesPath();
106
106
  const statsPath = path.join(
107
107
  process.env.HOME || '/tmp',
108
- '.rlhf',
108
+ '.thumbgate',
109
109
  'gate-stats.json'
110
110
  );
111
111
  const stats = readJsonFile(statsPath) || { blocked: 0, warned: 0, passed: 0, byGate: {} };
@@ -828,7 +828,7 @@ function printDashboard(data) {
828
828
  : '\u2192';
829
829
 
830
830
  console.log('');
831
- console.log('\uD83D\uDCCA RLHF Dashboard');
831
+ console.log('\uD83D\uDCCA ThumbGate Dashboard');
832
832
  console.log('\u2550'.repeat(46));
833
833
  console.log(` Approval Rate : ${approval.approvalRate}% \u2192 ${approval.recentRate}% (7-day trend ${trendArrow})`);
834
834
  console.log(` Total Signals : ${approval.total} (${approval.positive} positive, ${approval.negative} negative)`);
@@ -136,7 +136,7 @@ function enforceRetention() {
136
136
  */
137
137
  function generateDataUsageSummary() {
138
138
  const prefs = loadPreferences();
139
- const feedbackDir = process.env.THUMBGATE_FEEDBACK_DIR || path.join(process.cwd(), '.rlhf');
139
+ const feedbackDir = process.env.THUMBGATE_FEEDBACK_DIR || path.join(process.cwd(), '.thumbgate');
140
140
  const logPath = path.join(feedbackDir, 'feedback-log.jsonl');
141
141
  let entryCount = 0;
142
142
  if (fs.existsSync(logPath)) {