thumbgate 1.27.18 → 1.27.20

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 (170) hide show
  1. package/.claude/commands/dashboard.md +15 -0
  2. package/.claude/commands/thumbgate-blocked.md +27 -0
  3. package/.claude/commands/thumbgate-dashboard.md +15 -0
  4. package/.claude/commands/thumbgate-doctor.md +30 -0
  5. package/.claude/commands/thumbgate-guard.md +36 -0
  6. package/.claude/commands/thumbgate-protect.md +30 -0
  7. package/.claude/commands/thumbgate-rules.md +30 -0
  8. package/.claude-plugin/plugin.json +4 -3
  9. package/.well-known/agentic-verify.txt +1 -0
  10. package/.well-known/llms.txt +33 -12
  11. package/.well-known/mcp/server-card.json +8 -8
  12. package/README.md +246 -30
  13. package/adapters/claude/.mcp.json +2 -2
  14. package/adapters/codex/config.toml +2 -2
  15. package/adapters/gcp/dfcx-webhook-gate.js +295 -0
  16. package/adapters/gemini/function-declarations.json +1 -0
  17. package/adapters/letta/README.md +41 -0
  18. package/adapters/letta/thumbgate-letta-adapter.js +133 -0
  19. package/adapters/mcp/server-stdio.js +263 -11
  20. package/adapters/opencode/opencode.json +1 -1
  21. package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
  22. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
  23. package/bench/observability-eval-suite.json +26 -0
  24. package/bench/thumbgate-bench.json +2 -2
  25. package/bin/cli.js +1456 -122
  26. package/bin/dashboard-cli.js +7 -0
  27. package/bin/postinstall.js +11 -27
  28. package/commands/dashboard.md +15 -0
  29. package/commands/thumbgate-dashboard.md +15 -0
  30. package/config/gate-classifier-routing.json +98 -0
  31. package/config/gate-templates.json +216 -0
  32. package/config/gates/claim-verification.json +12 -0
  33. package/config/gates/default.json +31 -2
  34. package/config/github-about.json +2 -2
  35. package/config/mcp-allowlists.json +23 -13
  36. package/config/merge-quality-checks.json +0 -1
  37. package/config/model-candidates.json +121 -6
  38. package/config/post-deploy-marketing-pages.json +80 -0
  39. package/config/tessl-tiles.json +1 -3
  40. package/openapi/openapi.yaml +12 -0
  41. package/package.json +225 -100
  42. package/public/about.html +162 -0
  43. package/public/agent-manager.html +179 -0
  44. package/public/agents-cost-savings.html +153 -0
  45. package/public/ai-malpractice-prevention.html +818 -0
  46. package/public/assets/brand/github-social-preview.png +0 -0
  47. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  48. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  49. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  50. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  51. package/public/assets/brand/thumbgate-logo-transparent.svg +28 -0
  52. package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -0
  53. package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
  54. package/public/assets/brand/thumbgate-mark-team.svg +26 -0
  55. package/public/assets/brand/thumbgate-mark.svg +21 -0
  56. package/public/assets/brand/thumbgate-wordmark.svg +20 -0
  57. package/public/assets/claude-thumbgate-statusbar.svg +8 -0
  58. package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
  59. package/public/assets/legal-intake-control-flow.svg +66 -0
  60. package/public/blog.html +4 -4
  61. package/public/brand/thumbgate-mark.svg +19 -0
  62. package/public/brand/thumbgate-og.svg +16 -0
  63. package/public/chatgpt-app.html +330 -0
  64. package/public/codex-enterprise.html +123 -0
  65. package/public/codex-plugin.html +72 -20
  66. package/public/compare.html +31 -8
  67. package/public/dashboard.html +930 -166
  68. package/public/diagnostic.html +345 -0
  69. package/public/federal.html +2 -2
  70. package/public/guide.html +33 -13
  71. package/public/index.html +469 -111
  72. package/public/install.html +193 -0
  73. package/public/js/buyer-intent.js +672 -0
  74. package/public/learn.html +183 -18
  75. package/public/lessons.html +168 -10
  76. package/public/numbers.html +7 -7
  77. package/public/pricing.html +399 -0
  78. package/public/pro.html +34 -11
  79. package/scripts/action-receipts.js +324 -0
  80. package/scripts/activation-quickstart.js +187 -0
  81. package/scripts/agent-memory-lifecycle.js +211 -0
  82. package/scripts/agent-operations-planner.js +621 -0
  83. package/scripts/agent-readiness.js +20 -3
  84. package/scripts/agent-reward-model.js +53 -1
  85. package/scripts/ai-component-inventory.js +367 -0
  86. package/scripts/async-eval-observability.js +236 -0
  87. package/scripts/audit.js +65 -0
  88. package/scripts/auto-promote-gates.js +82 -10
  89. package/scripts/auto-wire-hooks.js +14 -0
  90. package/scripts/aws-blocks-guardrails.js +272 -0
  91. package/scripts/billing.js +93 -1
  92. package/scripts/bot-detection.js +61 -3
  93. package/scripts/build-metadata.js +50 -10
  94. package/scripts/classifier-routing.js +130 -0
  95. package/scripts/cli-feedback.js +4 -2
  96. package/scripts/cli-schema.js +97 -0
  97. package/scripts/cli-telemetry.js +6 -1
  98. package/scripts/commercial-offer.js +82 -2
  99. package/scripts/context-manager.js +74 -6
  100. package/scripts/dashboard-chat.js +332 -0
  101. package/scripts/dashboard.js +68 -2
  102. package/scripts/export-databricks-bundle.js +5 -1
  103. package/scripts/export-dpo-pairs.js +7 -2
  104. package/scripts/feedback-aggregate.js +281 -0
  105. package/scripts/feedback-loop.js +123 -1
  106. package/scripts/feedback-quality.js +87 -0
  107. package/scripts/feedback-sanitizer.js +105 -0
  108. package/scripts/filesystem-search.js +35 -10
  109. package/scripts/gate-stats.js +89 -0
  110. package/scripts/gates-engine.js +1176 -85
  111. package/scripts/gemini-embedding-policy.js +2 -1
  112. package/scripts/hook-runtime.js +20 -14
  113. package/scripts/hook-stop-anti-claim.js +301 -0
  114. package/scripts/hook-thumbgate-cache-updater.js +18 -2
  115. package/scripts/hybrid-feedback-context.js +142 -7
  116. package/scripts/install-shim.js +87 -0
  117. package/scripts/lesson-inference.js +8 -3
  118. package/scripts/lesson-search.js +17 -1
  119. package/scripts/license.js +10 -10
  120. package/scripts/llm-client.js +169 -4
  121. package/scripts/local-model-profile.js +15 -8
  122. package/scripts/mcp-config.js +7 -1
  123. package/scripts/mcp-oauth.js +293 -0
  124. package/scripts/memory-scope-readiness.js +159 -0
  125. package/scripts/meta-agent-loop.js +36 -0
  126. package/scripts/noop-detect.js +285 -0
  127. package/scripts/operational-integrity.js +39 -5
  128. package/scripts/oss-pr-opportunity-scout.js +35 -5
  129. package/scripts/parallel-workflow-orchestrator.js +293 -0
  130. package/scripts/plan-gate.js +243 -0
  131. package/scripts/plausible-domain-config.js +99 -0
  132. package/scripts/plausible-server-events.js +9 -6
  133. package/scripts/pro-local-dashboard.js +4 -4
  134. package/scripts/proxy-pointer-rag-guardrails.js +42 -1
  135. package/scripts/published-cli.js +0 -8
  136. package/scripts/qa-scenario-planner.js +136 -0
  137. package/scripts/rate-limiter.js +64 -13
  138. package/scripts/repeat-metric.js +137 -0
  139. package/scripts/secret-fixture-tokens.js +61 -0
  140. package/scripts/secret-redaction.js +166 -0
  141. package/scripts/secret-scanner.js +44 -5
  142. package/scripts/security-scanner.js +260 -10
  143. package/scripts/self-distill-agent.js +3 -1
  144. package/scripts/self-harness-optimizer.js +141 -0
  145. package/scripts/self-healing-check.js +193 -0
  146. package/scripts/self-protection.js +90 -0
  147. package/scripts/seo-gsd.js +916 -7
  148. package/scripts/silent-failure-cluster.js +531 -0
  149. package/scripts/statusline-cache-path.js +17 -2
  150. package/scripts/statusline-cache-read.js +57 -0
  151. package/scripts/statusline-local-stats.js +9 -1
  152. package/scripts/statusline-meta.js +28 -2
  153. package/scripts/statusline.sh +20 -4
  154. package/scripts/sync-telemetry-from-prod.js +374 -0
  155. package/scripts/telemetry-analytics.js +357 -0
  156. package/scripts/thompson-sampling.js +31 -10
  157. package/scripts/thumbgate-bench.js +16 -1
  158. package/scripts/thumbgate-search.js +85 -19
  159. package/scripts/tool-contract-validator.js +76 -0
  160. package/scripts/tool-registry.js +169 -1
  161. package/scripts/trajectory-scorer.js +63 -0
  162. package/scripts/vector-store.js +45 -0
  163. package/scripts/verify-marketing-pages-deployed.js +212 -0
  164. package/scripts/visitor-journey.js +172 -0
  165. package/scripts/workflow-sentinel.js +286 -53
  166. package/scripts/workspace-evolver.js +62 -2
  167. package/src/api/server.js +2683 -319
  168. package/.claude-plugin/marketplace.json +0 -85
  169. package/adapters/chatgpt/openapi.yaml +0 -1695
  170. package/scripts/bot-detector.js +0 -50
package/bin/cli.js CHANGED
@@ -23,14 +23,18 @@
23
23
  * npx thumbgate background-governance # background-agent run report + risk check
24
24
  * npx thumbgate cfo # local operational billing summary
25
25
  * npx thumbgate pro # solo dashboard + exports side lane
26
+ * npx thumbgate diagnostic # team workflow diagnostic intake + checkout path
27
+ * npx thumbgate audit <file> # audit an agent transcript for repeat-mistake token waste
26
28
  */
27
29
 
28
30
  'use strict';
29
31
 
30
32
  const fs = require('fs');
33
+ const os = require('os');
31
34
  const path = require('path');
32
35
  const crypto = require('crypto');
33
- const { execSync, execFileSync } = require('child_process');
36
+ const http = require('http');
37
+ const { execSync, execFileSync, execFile, spawn } = require('child_process');
34
38
  const {
35
39
  codexAutoUpdateCliEntry,
36
40
  codexAutoUpdateMcpEntry,
@@ -40,13 +44,6 @@ const {
40
44
  resolveMcpEntry,
41
45
  } = require(path.join(__dirname, '..', 'scripts', 'mcp-config'));
42
46
  const { trackEvent } = require(path.join(__dirname, '..', 'scripts', 'cli-telemetry'));
43
- const {
44
- cacheUpdateHookCommand,
45
- preToolHookCommand,
46
- sessionStartHookCommand,
47
- statuslineCommand,
48
- userPromptHookCommand,
49
- } = require(path.join(__dirname, '..', 'scripts', 'hook-runtime'));
50
47
  const {
51
48
  PRO_MONTHLY_PAYMENT_LINK,
52
49
  PRO_PRICE_LABEL,
@@ -56,8 +53,74 @@ const COMMAND = process.argv[2];
56
53
  const CWD = process.cwd();
57
54
  const PKG_ROOT = path.join(__dirname, '..');
58
55
 
59
- const PRO_URL = 'https://thumbgate-production.up.railway.app';
56
+ const PRO_URL = 'https://thumbgate.ai';
60
57
  const PRO_CHECKOUT_URL = PRO_MONTHLY_PAYMENT_LINK;
58
+ // Mirrors scripts/rate-limiter.js TRIAL_DAYS — must stay in sync. Gitar
59
+ // caught a 14-vs-7 mismatch on PR #2337 (banner said "7-day trial" but
60
+ // the date label was computed 14 days out).
61
+ const TRIAL_DAYS = 7;
62
+
63
+ function checkoutUrlFor(source, content) {
64
+ try {
65
+ const base = content === 'capture_feedback'
66
+ ? 'https://buy.stripe.com/7sYfZhaiE1eSbO99uj3sI0d'
67
+ : PRO_CHECKOUT_URL;
68
+ const url = new URL(base);
69
+ url.searchParams.set('utm_source', source || 'cli');
70
+ url.searchParams.set('utm_medium', 'cli');
71
+ url.searchParams.set('utm_campaign', 'pro_conversion');
72
+ if (content) url.searchParams.set('utm_content', content);
73
+ return url.toString();
74
+ } catch (_) {
75
+ return content === 'capture_feedback'
76
+ ? 'https://buy.stripe.com/7sYfZhaiE1eSbO99uj3sI0d'
77
+ : PRO_CHECKOUT_URL;
78
+ }
79
+ }
80
+
81
+ function pricingUrlFor(source, content) {
82
+ try {
83
+ const url = new URL('/pricing', PRO_URL);
84
+ url.searchParams.set('utm_source', source || 'cli');
85
+ url.searchParams.set('utm_medium', 'cli');
86
+ url.searchParams.set('utm_campaign', 'pro_education');
87
+ if (content) url.searchParams.set('utm_content', content);
88
+ return url.toString();
89
+ } catch (_) {
90
+ return `${PRO_URL}/pricing`;
91
+ }
92
+ }
93
+
94
+ function diagnosticUrlFor(source, content) {
95
+ try {
96
+ const url = new URL('/diagnostic', PRO_URL);
97
+ url.searchParams.set('utm_source', source || 'cli');
98
+ url.searchParams.set('utm_medium', 'cli');
99
+ url.searchParams.set('utm_campaign', 'workflow_hardening_diagnostic');
100
+ if (content) url.searchParams.set('utm_content', content);
101
+ return url.toString();
102
+ } catch (_) {
103
+ return `${PRO_URL}/diagnostic`;
104
+ }
105
+ }
106
+
107
+ function diagnosticCheckoutUrlFor(source, content) {
108
+ try {
109
+ const url = new URL('/go/diagnostic', PRO_URL);
110
+ url.searchParams.set('utm_source', source || 'cli');
111
+ url.searchParams.set('utm_medium', 'cli');
112
+ url.searchParams.set('utm_campaign', 'workflow_hardening_diagnostic');
113
+ if (content) url.searchParams.set('utm_content', content);
114
+ return url.toString();
115
+ } catch (_) {
116
+ return `${PRO_URL}/go/diagnostic`;
117
+ }
118
+ }
119
+
120
+ function trialDeadlineLabel(now = new Date()) {
121
+ const deadline = new Date(now.getTime() + (TRIAL_DAYS * 24 * 60 * 60 * 1000));
122
+ return deadline.toISOString().slice(0, 10);
123
+ }
61
124
 
62
125
  function upgradeNudge() {
63
126
  if (process.env.THUMBGATE_NO_NUDGE === '1') return;
@@ -65,14 +128,37 @@ function upgradeNudge() {
65
128
  const { isProTier } = require(path.join(PKG_ROOT, 'scripts', 'rate-limiter'));
66
129
  if (isProTier()) return;
67
130
  } catch (_) { return; }
131
+ const pricingUrl = pricingUrlFor('cli_upgrade_nudge', COMMAND || 'general');
132
+ const diagnosticUrl = diagnosticUrlFor('cli_upgrade_nudge', COMMAND || 'general');
68
133
  process.stderr.write(
69
- '\n Team rollout: start with the Workflow Hardening Sprint\n' +
70
- ' https://thumbgate-production.up.railway.app/#workflow-sprint-intake\n' +
134
+ '\n Team rollout: start with the $499 Workflow Hardening Diagnostic\n' +
135
+ ` ${diagnosticUrl}\n` +
71
136
  `\n Solo side lane: Pro — ${PRO_PRICE_LABEL}\n` +
72
- ` ${PRO_CHECKOUT_URL}\n\n`
137
+ ' Keeps lessons, rules, and the dashboard synced across machines and agent runtimes.\n' +
138
+ ` ${pricingUrl}\n\n`
73
139
  );
74
140
  }
75
141
 
142
+ function diagnostic() {
143
+ const intakeUrl = diagnosticUrlFor('cli_diagnostic', COMMAND || 'diagnostic');
144
+ const checkoutUrl = diagnosticCheckoutUrlFor('cli_diagnostic', COMMAND || 'diagnostic');
145
+ console.log('');
146
+ console.log(' ThumbGate Workflow Hardening Diagnostic');
147
+ console.log(' ---------------------------------------');
148
+ console.log(' Use this when one repeated AI-agent workflow failure is already costing');
149
+ console.log(' review time, release confidence, customer trust, or money.');
150
+ console.log('');
151
+ console.log(' Best fit: one workflow, one repeated failure, one owner who can review proof.');
152
+ console.log(' Output: failure taxonomy, block/warn/human-review split, and proof checklist.');
153
+ console.log('');
154
+ console.log(' Start with intake:');
155
+ console.log(` ${intakeUrl}`);
156
+ console.log('');
157
+ console.log(' Ready to pay now:');
158
+ console.log(` ${checkoutUrl}`);
159
+ console.log('');
160
+ }
161
+
76
162
  function appendLocalTelemetry(payload) {
77
163
  try {
78
164
  const { getFeedbackPaths } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
@@ -126,7 +212,7 @@ function telemetryPing(installId) {
126
212
  timestamp: new Date().toISOString(),
127
213
  };
128
214
  appendLocalTelemetry(payloadObject);
129
- const apiUrl = process.env.THUMBGATE_API_URL || 'https://thumbgate-production.up.railway.app';
215
+ const apiUrl = process.env.THUMBGATE_API_URL || 'https://thumbgate.ai';
130
216
  const payload = JSON.stringify(payloadObject);
131
217
  try {
132
218
  const url = new URL('/v1/telemetry/ping', apiUrl);
@@ -145,25 +231,47 @@ function proNudge(context) {
145
231
  const { isProTier } = require(path.join(PKG_ROOT, 'scripts', 'rate-limiter'));
146
232
  if (isProTier()) return;
147
233
  } catch (_) { /* if rate-limiter is unavailable, fall through and nudge */ }
234
+ const checkoutUrl = checkoutUrlFor('cli_nudge', context || COMMAND || 'general');
235
+ const pricingUrl = pricingUrlFor('cli_nudge', context || COMMAND || 'general');
148
236
  const messages = [
149
- `\n 💡 Unlock Pro (${PRO_PRICE_LABEL}): searchable dashboard, DPO export, multi-repo sync\n ${PRO_CHECKOUT_URL}\n`,
150
- `\n 💡 Pro tip: export your feedback as DPO training pairs to improve your models.\n Get Pro: ${PRO_CHECKOUT_URL}\n`,
151
- `\n 💡 ThumbGate Pro: search, edit, and sync lessons across repos. ${PRO_PRICE_LABEL}.\n ${PRO_CHECKOUT_URL}\n`,
237
+ `\n 💡 Pro (${PRO_PRICE_LABEL}): keep lessons, rules, and dashboard state synced across machines and agent runtimes.\n See pricing: ${pricingUrl}\n`,
238
+ `\n 💡 You just taught ThumbGate something locally. Pro keeps that lesson alive on every laptop, CI box, and agent runtime.\n See pricing: ${pricingUrl}\n`,
239
+ `\n 💡 ThumbGate Pro syncs lessons/rules across Claude, Codex, Cursor, containers, and CI. ${PRO_PRICE_LABEL}.\n Start Pro: ${checkoutUrl}\n`,
152
240
  ];
153
241
  // Rotate message daily — no Math.random (security policy)
154
242
  const msg = messages[Math.floor(Date.now() / 86400000) % messages.length];
155
243
  process.stderr.write(msg);
156
244
  }
157
245
 
158
- function limitNudge(action) {
246
+ function limitNudge(action, limitResult = {}) {
159
247
  if (process.env.THUMBGATE_NO_NUDGE === '1') return;
248
+ const checkoutUrl = checkoutUrlFor('cli_limit', action);
249
+ const usageLine = Number.isFinite(limitResult.used) && Number.isFinite(limitResult.limit)
250
+ ? ` Usage: ${limitResult.used}/${limitResult.limit} (${limitResult.limitType || 'limit'}).\n`
251
+ : '';
252
+ const reason = limitResult.message
253
+ ? String(limitResult.message).split('\n')[0]
254
+ : 'Free tier limit reached.';
160
255
  process.stderr.write(
161
- `\n ⚠️ Free tier limit reached. Upgrade to Pro for unlimited: https://thumbgate-production.up.railway.app/pro\n` +
162
- ` ${action} daily limit reached. Upgrade to Pro for unlimited usage — ${PRO_PRICE_LABEL}:\n` +
163
- ` ${PRO_CHECKOUT_URL}\n\n`
256
+ `\n 🔒 ${reason}\n` +
257
+ usageLine +
258
+ ` Upgrade to Pro (${PRO_PRICE_LABEL}) to continue ${action.replace(/_/g, ' ')}:\n` +
259
+ ` ${checkoutUrl}\n\n`
164
260
  );
165
261
  }
166
262
 
263
+ function printInitConversionPrompt(email) {
264
+ if (process.env.THUMBGATE_NO_NUDGE === '1') return;
265
+ const checkoutUrl = checkoutUrlFor('cli_init', email ? 'init_email' : 'init_no_email');
266
+ console.log('');
267
+ console.log(' ┌──────────────────────────────────────────────────────────┐');
268
+ console.log(` │ 7-day Pro trial active through ${trialDeadlineLabel()}. │`);
269
+ console.log(' │ Pro keeps lessons/rules/dashboard synced everywhere. │');
270
+ console.log(' │ Add onboarding: npx thumbgate init --email you@company.com │');
271
+ console.log(` │ Upgrade: ${checkoutUrl}`);
272
+ console.log(' └──────────────────────────────────────────────────────────┘');
273
+ }
274
+
167
275
  function parseArgs(argv) {
168
276
  const args = {};
169
277
  argv.forEach((arg, index) => {
@@ -185,6 +293,73 @@ function parseArgs(argv) {
185
293
  return args;
186
294
  }
187
295
 
296
+ function probeDash(port) {
297
+ return new Promise((resolve) => {
298
+ const req = http.get({ hostname: '127.0.0.1', port, path: '/health', timeout: 750 }, (res) => {
299
+ res.resume();
300
+ resolve(res.statusCode >= 200 && res.statusCode < 500);
301
+ });
302
+ req.on('error', () => resolve(false));
303
+ req.on('timeout', () => { req.destroy(); resolve(false); });
304
+ });
305
+ }
306
+
307
+ async function waitDash(port, timeoutMs = 8000) {
308
+ const deadline = Date.now() + timeoutMs;
309
+ while (Date.now() < deadline) {
310
+ if (await probeDash(port)) return true;
311
+ await new Promise((r) => setTimeout(r, 250));
312
+ }
313
+ return false;
314
+ }
315
+
316
+ async function ensureDash(port) {
317
+ if (await probeDash(port)) {
318
+ return { started: false, pid: null };
319
+ }
320
+
321
+ const serverPath = path.join(PKG_ROOT, 'src', 'api', 'server.js');
322
+ const child = spawn(process.execPath, [serverPath], {
323
+ cwd: PKG_ROOT,
324
+ detached: true,
325
+ env: { ...process.env, PORT: String(port), THUMBGATE_ALLOW_INSECURE: process.env.THUMBGATE_ALLOW_INSECURE || 'true' },
326
+ stdio: 'ignore',
327
+ });
328
+ child.unref();
329
+
330
+ if (!(await waitDash(port))) {
331
+ throw new Error('Dashboard API timeout');
332
+ }
333
+
334
+ return { started: true, pid: child.pid };
335
+ }
336
+
337
+ function openBrowser(url) {
338
+ if (process.env.THUMBGATE_DASHBOARD_NO_OPEN === '1') {
339
+ console.log(`Ready: ${url}`);
340
+ return Promise.resolve();
341
+ }
342
+ const [command, args] = ({ darwin: ['open', [url]], win32: ['cmd', ['/c', 'start', '', url]] }[process.platform] || ['xdg-open', [url]]);
343
+ return new Promise((resolve, reject) => {
344
+ execFile(command, args, (err) => (err ? reject(err) : resolve()));
345
+ });
346
+ }
347
+
348
+ function parseTtlMs(value, fallbackMs = 5 * 60 * 1000) {
349
+ if (value === undefined || value === null || value === true || value === '') return fallbackMs;
350
+ const raw = String(value).trim().toLowerCase();
351
+ const match = raw.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/);
352
+ if (!match) return fallbackMs;
353
+ const amount = Number(match[1]);
354
+ if (!Number.isFinite(amount) || amount <= 0) return fallbackMs;
355
+ const unit = match[2] || 'ms';
356
+ const factor = unit === 'h' ? 60 * 60 * 1000
357
+ : unit === 'm' ? 60 * 1000
358
+ : unit === 's' ? 1000
359
+ : 1;
360
+ return Math.round(amount * factor);
361
+ }
362
+
188
363
  function readStdinText() {
189
364
  try {
190
365
  return fs.readFileSync(0, 'utf8');
@@ -395,64 +570,24 @@ function whichExists(cmd) {
395
570
 
396
571
  function setupClaude() {
397
572
  const mcpChanged = mergeMcpJson(path.join(CWD, '.mcp.json'), 'Claude Code', 'project');
573
+ const { wireHooks } = require(path.join(PKG_ROOT, 'scripts', 'auto-wire-hooks'));
574
+ const hookResult = wireHooks({ agent: 'claude-code' });
398
575
 
399
- // Upsert Stop hook into .claude/settings.json for autonomous self-scoring
400
- const settingsPath = path.join(CWD, '.claude', 'settings.json');
401
- const stopHookCommand = 'bash scripts/hook-stop-self-score.sh';
402
-
403
- let settings = { hooks: {} };
404
- if (fs.existsSync(settingsPath)) {
405
- try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (_) { /* fresh */ }
406
- }
407
- settings.hooks = settings.hooks || {};
408
-
409
- const stopAlreadyPresent = (settings.hooks.Stop || [])
410
- .some(entry => (entry.hooks || []).some(h => h.command === stopHookCommand));
411
-
412
- let hooksChanged = false;
413
- if (!stopAlreadyPresent) {
414
- settings.hooks.Stop = settings.hooks.Stop || [];
415
- settings.hooks.Stop.push({ hooks: [{ type: 'command', command: stopHookCommand }] });
416
- hooksChanged = true;
417
- console.log(' Claude Code: installed Stop hook');
576
+ if (hookResult.error) {
577
+ console.log(` Claude Code hooks: ${hookResult.error}`);
578
+ return mcpChanged;
418
579
  }
419
580
 
420
- // Upsert PostToolUse hook for ThumbGate statusline cache updates
421
- const cacheHookCommand = cacheUpdateHookCommand();
422
- const originalPostToolUseCount = (settings.hooks.PostToolUse || []).length;
423
- settings.hooks.PostToolUse = (settings.hooks.PostToolUse || []).filter(
424
- (entry) => !(entry.hooks || []).some((h) => h.command && h.command !== cacheHookCommand && /(hook-thumbgate-cache-updater|cache-update\b)/.test(h.command))
425
- );
426
- if (settings.hooks.PostToolUse.length !== originalPostToolUseCount) {
427
- hooksChanged = true;
428
- }
429
- const cacheAlreadyPresent = (settings.hooks.PostToolUse || [])
430
- .some(entry => (entry.hooks || []).some(h => h.command === cacheHookCommand || (h.command && h.command.includes('cache-update'))));
431
-
432
- if (!cacheAlreadyPresent) {
433
- settings.hooks.PostToolUse = settings.hooks.PostToolUse || [];
434
- settings.hooks.PostToolUse.push({
435
- matcher: 'mcp__thumbgate__feedback_stats|mcp__thumbgate__dashboard',
436
- hooks: [{ type: 'command', command: cacheHookCommand }]
437
- });
438
- hooksChanged = true;
439
- console.log(' Claude Code: installed ThumbGate cache updater hook');
440
- }
441
-
442
- // Upsert statusLine for ThumbGate feedback display
443
- const statuslineScript = statuslineCommand();
444
- if (!settings.statusLine || settings.statusLine.command !== statuslineScript) {
445
- settings.statusLine = { type: 'command', command: statuslineScript };
446
- hooksChanged = true;
447
- console.log(' Claude Code: installed ThumbGate status line');
448
- }
449
-
450
- if (hooksChanged) {
451
- fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
452
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
581
+ if (!hookResult.changed) {
582
+ console.log(` Claude Code hooks: already configured at ${hookResult.settingsPath}`);
583
+ } else {
584
+ for (const h of hookResult.added) {
585
+ console.log(` Claude Code: wired ${h.lifecycle} hook`);
586
+ }
587
+ console.log(` Claude Code settings: ${hookResult.settingsPath}`);
453
588
  }
454
589
 
455
- return mcpChanged || hooksChanged;
590
+ return mcpChanged || hookResult.changed;
456
591
  }
457
592
 
458
593
  function setupCodex() {
@@ -483,6 +618,20 @@ function setupCodex() {
483
618
  }
484
619
 
485
620
  function setupGemini() {
621
+ // Try to import custom commands as a Gemini plugin if the CLI is installed
622
+ const { execSync } = require('child_process');
623
+ let pluginImported = false;
624
+ for (const binName of ['agy', 'gemini']) {
625
+ try {
626
+ execSync(`${binName} plugin import "${PKG_ROOT}" --force`, { stdio: 'ignore' });
627
+ console.log(` Gemini: imported thumbgate plugin via ${binName}`);
628
+ pluginImported = true;
629
+ break;
630
+ } catch (err) {
631
+ // ignore errors if command doesn't exist or fails
632
+ }
633
+ }
634
+
486
635
  const settingsPath = path.join(HOME, '.gemini', 'settings.json');
487
636
  if (fs.existsSync(settingsPath)) {
488
637
  const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
@@ -503,13 +652,14 @@ function setupGemini() {
503
652
  }
504
653
  }
505
654
 
506
- if (!changed) return false;
655
+ if (!changed) return pluginImported;
507
656
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
508
657
  console.log(' Gemini: updated ~/.gemini/settings.json');
509
658
  return true;
510
659
  }
511
660
  // Fallback: project-level .gemini/settings.json
512
- return mergeMcpJson(path.join(CWD, '.gemini', 'settings.json'), 'Gemini', 'project');
661
+ const mcpChanged = mergeMcpJson(path.join(CWD, '.gemini', 'settings.json'), 'Gemini', 'project');
662
+ return pluginImported || mcpChanged;
513
663
  }
514
664
 
515
665
  function setupAmp() {
@@ -597,6 +747,104 @@ function detectAgent(projectDir) {
597
747
  return null;
598
748
  }
599
749
 
750
+ async function setupVertex(options = {}) {
751
+ const { execSync } = require('child_process');
752
+ const dryRun = options.dryRun === true || options['dry-run'] === true;
753
+ console.log(`\nthumbgate setup-vertex v${pkgVersion()}`);
754
+ console.log(' Zero-friction Google Cloud & Vertex AI onboarding...');
755
+ if (dryRun) {
756
+ console.log(' Dry run: will detect gcloud account/project, but will not enable services or write .env.');
757
+ }
758
+ console.log('');
759
+
760
+ // 1. Detect gcloud CLI
761
+ let activeAccount = '';
762
+ let activeProject = '';
763
+ try {
764
+ activeAccount = execSync('gcloud config get-value account', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
765
+ activeProject = execSync('gcloud config get-value project', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
766
+ } catch (_) {
767
+ console.log(' ⚠️ Google Cloud SDK (gcloud CLI) not detected or not logged in.');
768
+ console.log(' To automate setup, install the Google Cloud CLI and run: gcloud auth login');
769
+ console.log(' Otherwise, manually set the following variables in your .env file:');
770
+ console.log(' THUMBGATE_PROVIDER_MODE=vertex');
771
+ console.log(' VERTEX_PROJECT_ID=<your-gcp-project-id>');
772
+ console.log('');
773
+ return;
774
+ }
775
+
776
+ if (!activeAccount) {
777
+ console.log(' ⚠️ No active Google Cloud account set. Run: gcloud auth login');
778
+ return;
779
+ }
780
+
781
+ console.log(` Active Account : \x1b[36m${activeAccount}\x1b[0m`);
782
+ if (!activeProject) {
783
+ console.log(' ⚠️ No active Google Cloud project set. Run: gcloud config set project <PROJECT_ID>');
784
+ return;
785
+ }
786
+ console.log(` Active Project : \x1b[36m${activeProject}\x1b[0m`);
787
+
788
+ // Validate project ID matches GCP format before use in shell
789
+ if (!/^[a-z][a-z0-9-]{4,28}[a-z0-9]$/.test(activeProject)) {
790
+ console.log(' ⚠️ Invalid GCP project ID format. Aborting.');
791
+ return;
792
+ }
793
+
794
+ if (dryRun) {
795
+ console.log(` DRY-RUN would enable Vertex AI API for project: ${activeProject}`);
796
+ console.log(` DRY-RUN would write THUMBGATE_PROVIDER_MODE=vertex and VERTEX_PROJECT_ID=${activeProject} to .env.`);
797
+ console.log('');
798
+ console.log(' Dry run complete. Re-run without --dry-run to apply these changes.');
799
+ return;
800
+ }
801
+
802
+ // 2. Auto-enable Vertex AI API
803
+ console.log(' ⚙️ Enabling Vertex AI API in your project (this can take a few seconds)...');
804
+ try {
805
+ execSync(`gcloud services enable aiplatform.googleapis.com --project=${activeProject}`, { stdio: 'inherit' });
806
+ console.log(' ✅ Vertex AI API successfully enabled.');
807
+ } catch (err) {
808
+ console.log(' ⚠️ Could not programmatically enable Vertex AI API. Please make sure your billing is open.');
809
+ }
810
+
811
+ // 3. Write env config to .env
812
+ const envPath = path.join(process.cwd(), '.env');
813
+ let envContent = '';
814
+ if (fs.existsSync(envPath)) {
815
+ envContent = fs.readFileSync(envPath, 'utf8');
816
+ }
817
+
818
+ // Helper to update or append env values
819
+ function updateEnvKey(content, key, value) {
820
+ const regex = new RegExp(`^${key}=.*$`, 'm');
821
+ if (regex.test(content)) {
822
+ return content.replace(regex, `${key}=${value}`);
823
+ }
824
+ return content.trim() + `\n${key}=${value}\n`;
825
+ }
826
+
827
+ let updatedContent = updateEnvKey(envContent, 'THUMBGATE_PROVIDER_MODE', 'vertex');
828
+ updatedContent = updateEnvKey(updatedContent, 'VERTEX_PROJECT_ID', activeProject);
829
+
830
+ fs.writeFileSync(envPath, updatedContent, 'utf8');
831
+ console.log(' ✅ Wrote configuration to local .env file.');
832
+
833
+ // 4. Print gorgeous success activation box
834
+ console.log('');
835
+ console.log(' ╭──────────────────────────────────────────────────────────╮');
836
+ console.log(' │ Vertex AI Setup Complete │');
837
+ console.log(' │ │');
838
+ console.log(' │ ThumbGate wrote local Vertex routing config. │');
839
+ console.log(' │ This does not create or verify a Dialogflow CX agent. │');
840
+ console.log(' │ Verify DFCX with the console or Dialogflow CX REST API. │');
841
+ console.log(' │ │');
842
+ console.log(' │ Try a test run: │');
843
+ console.log(' │ npx thumbgate feedback-self-test │');
844
+ console.log(' ╰──────────────────────────────────────────────────────────╯');
845
+ console.log('');
846
+ }
847
+
600
848
  function quickStart() {
601
849
  const qsArgs = parseArgs(process.argv.slice(3));
602
850
  const projectDir = process.cwd();
@@ -657,8 +905,28 @@ function quickStart() {
657
905
  console.log('');
658
906
  }
659
907
 
908
+ // Activation walkthrough (guided first rule + live demonstrated block).
909
+ // Implementation lives in scripts/activation-quickstart.js so it can be unit
910
+ // tested without executing the CLI's top-level command switch. `init` is
911
+ // deliberately untouched — this is an additive, separate command.
912
+ function quickstart() {
913
+ return require(path.join(PKG_ROOT, 'scripts', 'activation-quickstart')).quickstart();
914
+ }
915
+
660
916
  function init(cliArgs = parseArgs(process.argv.slice(3))) {
661
917
  const args = { ...cliArgs };
918
+ if (args.help || args.h) {
919
+ console.log('Usage: npx thumbgate init [--agent <name>] [--wire-hooks] [--email you@company.com]');
920
+ console.log('');
921
+ console.log('Scaffold ThumbGate in the current project and wire detected agent integrations.');
922
+ console.log('');
923
+ console.log('Options:');
924
+ console.log(' --agent <name> Wire a specific agent: claude-code, codex, gemini, amp, cursor, cline');
925
+ console.log(' --wire-hooks Wire hooks only; do not scaffold project files');
926
+ console.log(' --email <email> Subscribe installer to the setup guide and trial reminders');
927
+ console.log(' --dry-run Show hook changes without writing them');
928
+ return;
929
+ }
662
930
 
663
931
  // --wire-hooks only mode: skip scaffolding, just wire hooks
664
932
  if (args['wire-hooks']) {
@@ -718,12 +986,43 @@ function init(cliArgs = parseArgs(process.argv.slice(3))) {
718
986
  // Always create .mcp.json (project-level MCP config used by Claude, Codex, Cursor)
719
987
  mergeMcpJson(path.join(CWD, '.mcp.json'), 'MCP');
720
988
 
989
+ // Copy custom slash commands (.claude/commands/*.md) to the project's config directories
990
+ const pkgCommandsDir = path.join(PKG_ROOT, '.claude', 'commands');
991
+ if (fs.existsSync(pkgCommandsDir)) {
992
+ const targets = [
993
+ path.join(CWD, '.claude', 'commands'),
994
+ path.join(CWD, '.gemini', 'commands'),
995
+ path.join(CWD, '.antigravitycli', 'commands')
996
+ ];
997
+ for (const projectCommandsDir of targets) {
998
+ if (!fs.existsSync(projectCommandsDir)) {
999
+ fs.mkdirSync(projectCommandsDir, { recursive: true });
1000
+ }
1001
+ try {
1002
+ const files = fs.readdirSync(pkgCommandsDir);
1003
+ for (const file of files) {
1004
+ if (file.endsWith('.md')) {
1005
+ fs.copyFileSync(path.join(pkgCommandsDir, file), path.join(projectCommandsDir, file));
1006
+ }
1007
+ }
1008
+ } catch (err) {
1009
+ console.log(` Failed to copy custom commands to ${path.relative(CWD, projectCommandsDir)}: ${err.message}`);
1010
+ }
1011
+ }
1012
+ console.log('Scaffolded custom slash commands directories (.claude, .gemini, .antigravitycli)');
1013
+ }
1014
+
721
1015
  // Auto-detect and configure platform-specific locations
722
1016
  console.log('');
723
1017
  console.log('Detecting platforms...');
724
1018
  let configured = 0;
725
1019
 
726
1020
  const platforms = [
1021
+ { name: 'Claude Code', detect: [
1022
+ () => whichExists('claude'),
1023
+ () => fs.existsSync(path.join(HOME, '.claude')),
1024
+ () => fs.existsSync(path.join(CWD, '.claude')),
1025
+ ], setup: setupClaude },
727
1026
  { name: 'Codex', detect: [() => whichExists('codex'), () => fs.existsSync(path.join(HOME, '.codex'))], setup: setupCodex },
728
1027
  { name: 'Gemini', detect: [() => whichExists('gemini'), () => fs.existsSync(path.join(HOME, '.gemini'))], setup: setupGemini },
729
1028
  { name: 'Amp', detect: [() => whichExists('amp'), () => fs.existsSync(path.join(HOME, '.amp'))], setup: setupAmp },
@@ -746,8 +1045,11 @@ function init(cliArgs = parseArgs(process.argv.slice(3))) {
746
1045
  }
747
1046
 
748
1047
  // ChatGPT — cannot be automated
749
- const chatgptSpec = path.join(PKG_ROOT, 'adapters', 'chatgpt', 'openapi.yaml');
750
- if (fs.existsSync(chatgptSpec)) {
1048
+ const chatgptSpec = [
1049
+ path.join(PKG_ROOT, 'openapi', 'openapi.yaml'),
1050
+ path.join(PKG_ROOT, 'adapters', 'chatgpt', 'openapi.yaml'),
1051
+ ].find((candidate) => fs.existsSync(candidate));
1052
+ if (chatgptSpec) {
751
1053
  const projectChatgptSpec = path.join(thumbgateDir, 'chatgpt-openapi.yaml');
752
1054
  fs.copyFileSync(chatgptSpec, projectChatgptSpec);
753
1055
  console.log(` ChatGPT: import ${path.relative(CWD, projectChatgptSpec)} in GPT Builder > Actions`);
@@ -787,16 +1089,43 @@ function init(cliArgs = parseArgs(process.argv.slice(3))) {
787
1089
  }
788
1090
 
789
1091
  console.log('');
790
- console.log(`thumbgate v${pkgVersion()} initialized.`);
791
- console.log('Run: npx thumbgate help');
1092
+ console.log(`✅ thumbgate v${pkgVersion()} initialized.`);
1093
+ const onboardingEmail = typeof args.email === 'string'
1094
+ ? args.email.trim()
1095
+ : (typeof args['onboarding-email'] === 'string' ? args['onboarding-email'].trim() : '');
1096
+ if (onboardingEmail) {
1097
+ try {
1098
+ execFileSync(process.execPath, [__filename, 'subscribe', onboardingEmail], {
1099
+ cwd: CWD,
1100
+ env: process.env,
1101
+ stdio: 'inherit',
1102
+ });
1103
+ trackEvent('cli_init_email_subscribed', { command: 'init', source: 'init_email' });
1104
+ } catch (_) {
1105
+ console.log(` Retry onboarding email: npx thumbgate subscribe ${onboardingEmail}`);
1106
+ }
1107
+ }
792
1108
  trackEvent('cli_init', { command: 'init' });
793
- proNudge();
1109
+
1110
+ // ---------------------------------------------------------------------------
1111
+ // Activation guide: the ONE thing the user should do next.
1112
+ // 98.5% of init users never promote their first prevention rule.
1113
+ // This is the funnel break — not conversion, not nudges — activation.
1114
+ // ---------------------------------------------------------------------------
794
1115
  console.log('');
795
- console.log(' ┌──────────────────────────────────────────────────────────┐');
796
- console.log(' │ Teams: shared enforcement, CI gates, audit trails │');
797
- console.log(' │ One correction protects every agent on your team. │');
798
- console.log(' │ https://thumbgate-production.up.railway.app/pro │');
799
- console.log(' └──────────────────────────────────────────────────────────┘');
1116
+ console.log(' ╭──────────────────────────────────────────────────────────╮');
1117
+ console.log(' │ NEXT: Prove feedback capture works │');
1118
+ console.log(' │ │');
1119
+ console.log(' │ npx thumbgate feedback-self-test │');
1120
+ console.log(' │ │');
1121
+ console.log(' │ Then dogfood it in chat: │');
1122
+ console.log(' │ thumbs down: agent skipped verification │');
1123
+ console.log(' ╰──────────────────────────────────────────────────────────╯');
1124
+ console.log('');
1125
+ printInitConversionPrompt(onboardingEmail);
1126
+ if (!onboardingEmail) {
1127
+ console.log(' Get trial reminders: npx thumbgate init --email you@company.com');
1128
+ }
800
1129
 
801
1130
  try {
802
1131
  const { appendFunnelEvent } = require(path.join(PKG_ROOT, 'scripts', 'billing'));
@@ -826,7 +1155,7 @@ function capture() {
826
1155
  const { getUsage } = require(path.join(PKG_ROOT, 'scripts', 'rate-limiter'));
827
1156
  const capLimit = checkLimit('capture_feedback');
828
1157
  if (!capLimit.allowed) {
829
- limitNudge('capture_feedback');
1158
+ limitNudge('capture_feedback', capLimit);
830
1159
  process.exit(1);
831
1160
  }
832
1161
  trackEvent('cli_capture', { command: 'capture' });
@@ -841,23 +1170,88 @@ function capture() {
841
1170
  return;
842
1171
  }
843
1172
 
844
- const signal = (args.feedback || '').toLowerCase();
1173
+ // Parse pure positional arguments
1174
+ const rawArgv = process.argv.slice(3);
1175
+ const positionalArgs = [];
1176
+ const BOOLEAN_FLAGS = new Set(['--json', '--verbose', '--quiet', '--dry-run', '--stats', '--summary', '--no-nudge', '--help']);
1177
+ for (let i = 0; i < rawArgv.length; i++) {
1178
+ const arg = rawArgv[i];
1179
+ if (arg.startsWith('--')) {
1180
+ if (!arg.includes('=') && !BOOLEAN_FLAGS.has(arg)) {
1181
+ i++;
1182
+ }
1183
+ } else {
1184
+ positionalArgs.push(arg);
1185
+ }
1186
+ }
1187
+
1188
+ let signal = (args.feedback || '').toLowerCase();
1189
+ let consumedSignalArgs = 0;
1190
+ if (!signal && positionalArgs[0]) {
1191
+ const { detectFeedbackSignal } = require(path.join(PKG_ROOT, 'scripts', 'feedback-quality'));
1192
+ const oneWord = positionalArgs[0];
1193
+ const twoWords = positionalArgs.slice(0, 2).join(' ');
1194
+ const detected = detectFeedbackSignal(twoWords) || detectFeedbackSignal(oneWord);
1195
+ if (detected) {
1196
+ signal = detected.signal;
1197
+ consumedSignalArgs = detectFeedbackSignal(twoWords) ? Math.min(2, positionalArgs.length) : 1;
1198
+ } else {
1199
+ const firstPos = positionalArgs[0].toLowerCase();
1200
+ if (['up', 'down', 'thumbsup', 'thumbsdown', 'thumbs_up', 'thumbs_down', 'positive', 'negative'].some(v => firstPos.includes(v))) {
1201
+ signal = firstPos;
1202
+ consumedSignalArgs = 1;
1203
+ }
1204
+ }
1205
+ }
1206
+
845
1207
  const normalized = ['up', 'thumbsup', 'thumbs_up', 'positive'].some(v => signal.includes(v)) ? 'up'
846
1208
  : ['down', 'thumbsdown', 'thumbs_down', 'negative'].some(v => signal.includes(v)) ? 'down'
847
1209
  : signal;
848
1210
 
849
1211
  if (normalized !== 'up' && normalized !== 'down') {
850
- console.error('Missing or unrecognized --feedback=up|down');
1212
+ console.error('Missing or unrecognized --feedback=up|down (or positional argument)');
851
1213
  process.exit(1);
852
1214
  }
853
1215
 
1216
+ const gateAction = (args.action || '').toLowerCase();
1217
+ if (gateAction && !['block', 'approve', 'warn'].includes(gateAction)) {
1218
+ console.error('Unrecognized --action. Use: block (default), approve, or warn');
1219
+ process.exit(1);
1220
+ }
1221
+
1222
+ let context = args.context || '';
1223
+ if (!context && consumedSignalArgs > 0) {
1224
+ context = positionalArgs.slice(consumedSignalArgs).join(' ');
1225
+ } else if (!context && positionalArgs[1]) {
1226
+ context = positionalArgs[1];
1227
+ }
1228
+
1229
+ let whatWentWrong = args['what-went-wrong'];
1230
+ if (!whatWentWrong && consumedSignalArgs > 0 && positionalArgs.length > consumedSignalArgs + 1) {
1231
+ whatWentWrong = positionalArgs.slice(consumedSignalArgs + 1).join(' ');
1232
+ } else if (!whatWentWrong && positionalArgs[2]) {
1233
+ whatWentWrong = positionalArgs[2];
1234
+ } else if (!whatWentWrong && normalized === 'down' && context) {
1235
+ whatWentWrong = context;
1236
+ }
1237
+
1238
+ let whatToChange = args['what-to-change'];
1239
+ if (!whatToChange && consumedSignalArgs > 0 && positionalArgs.length > consumedSignalArgs + 2) {
1240
+ whatToChange = positionalArgs.slice(consumedSignalArgs + 2).join(' ');
1241
+ } else if (!whatToChange && positionalArgs[3]) {
1242
+ whatToChange = positionalArgs[3];
1243
+ } else if (!whatToChange && normalized === 'down' && context) {
1244
+ whatToChange = `avoid: ${context}`;
1245
+ }
1246
+
854
1247
  const result = captureFeedback({
855
1248
  signal: normalized,
856
- context: args.context || '',
857
- whatWentWrong: args['what-went-wrong'],
858
- whatToChange: args['what-to-change'],
1249
+ context: context,
1250
+ whatWentWrong: whatWentWrong,
1251
+ whatToChange: whatToChange,
859
1252
  whatWorked: args['what-worked'],
860
1253
  tags: args.tags,
1254
+ gateAction: gateAction || undefined,
861
1255
  });
862
1256
 
863
1257
  if (result.accepted) {
@@ -885,10 +1279,21 @@ function capture() {
885
1279
  const pct = Math.round((capLimit.used / capLimit.limit) * 100);
886
1280
  console.log(` Usage : ${capLimit.used}/${capLimit.limit} captures today (${pct}%)`);
887
1281
  if (capLimit.remaining <= 1) {
888
- console.log(` ⚠️ Free tier limit reached. Upgrade to Pro for unlimited: https://thumbgate-production.up.railway.app/pro`);
1282
+ console.log(` ⚠️ Free tier limit reached. Upgrade to Pro for unlimited: https://thumbgate.ai/pro`);
889
1283
  }
890
1284
  }
891
1285
  console.log('');
1286
+ try {
1287
+ const { buildCaptureReceipt } = require(path.join(PKG_ROOT, 'scripts', 'commercial-offer'));
1288
+ console.log(buildCaptureReceipt({
1289
+ signal: normalized,
1290
+ feedbackId: ev.id,
1291
+ memoryId: mem.id,
1292
+ actionType: ev.actionType,
1293
+ }));
1294
+ } catch (_) {
1295
+ // Receipt is a conversion aid, not part of feedback persistence.
1296
+ }
892
1297
  proNudge();
893
1298
  } else {
894
1299
  if (args.json) {
@@ -906,13 +1311,130 @@ function capture() {
906
1311
  }
907
1312
  }
908
1313
 
1314
+ function readJsonlEntries(filePath) {
1315
+ try {
1316
+ return fs.readFileSync(filePath, 'utf8')
1317
+ .split(/\r?\n/)
1318
+ .map((line) => line.trim())
1319
+ .filter(Boolean)
1320
+ .map((line) => JSON.parse(line));
1321
+ } catch (_) {
1322
+ return [];
1323
+ }
1324
+ }
1325
+
1326
+ function shellSingleQuote(value) {
1327
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
1328
+ }
1329
+
1330
+ function feedbackSelfTest() {
1331
+ const args = parseArgs(process.argv.slice(3));
1332
+ const signalArg = String(args.feedback || args.signal || 'down').toLowerCase();
1333
+ const normalized = ['up', 'thumbsup', 'thumbs_up', 'positive'].some((v) => signalArg.includes(v)) ? 'up'
1334
+ : ['down', 'thumbsdown', 'thumbs_down', 'negative'].some((v) => signalArg.includes(v)) ? 'down'
1335
+ : signalArg;
1336
+
1337
+ if (normalized !== 'up' && normalized !== 'down') {
1338
+ console.error('feedback-self-test needs --feedback=up|down when overriding the default.');
1339
+ process.exit(1);
1340
+ }
1341
+
1342
+ const previousFeedbackDir = process.env.THUMBGATE_FEEDBACK_DIR;
1343
+ const previousNoNudge = process.env.THUMBGATE_NO_NUDGE;
1344
+ const feedbackDir = args['feedback-dir']
1345
+ ? path.resolve(CWD, args['feedback-dir'])
1346
+ : args.persist
1347
+ ? null
1348
+ : fs.mkdtempSync(path.join(os.tmpdir(), 'thumbgate-feedback-self-test-'));
1349
+ const isolated = Boolean(feedbackDir);
1350
+
1351
+ if (feedbackDir) process.env.THUMBGATE_FEEDBACK_DIR = feedbackDir;
1352
+ process.env.THUMBGATE_NO_NUDGE = '1';
1353
+
1354
+ try {
1355
+ const { captureFeedback, getFeedbackPaths } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
1356
+ const context = args.context || `feedback self-test: typed thumbs ${normalized} reaches ThumbGate capture`;
1357
+ const result = captureFeedback({
1358
+ signal: normalized,
1359
+ context,
1360
+ whatWentWrong: normalized === 'down'
1361
+ ? (args['what-went-wrong'] || 'Need proof that feedback capture is wired in this runtime')
1362
+ : undefined,
1363
+ whatToChange: normalized === 'down'
1364
+ ? (args['what-to-change'] || 'Run a one-command self-test before claiming thumbs feedback is captured')
1365
+ : undefined,
1366
+ whatWorked: normalized === 'up'
1367
+ ? (args['what-worked'] || 'Feedback capture persisted and was verified by a self-test')
1368
+ : undefined,
1369
+ tags: args.tags || 'self-test,dogfood,feedback-capture',
1370
+ });
1371
+
1372
+ const paths = getFeedbackPaths();
1373
+ const feedbackRows = readJsonlEntries(paths.FEEDBACK_LOG_PATH);
1374
+ const memoryRows = readJsonlEntries(paths.MEMORY_LOG_PATH);
1375
+ const feedbackId = result.feedbackEvent && result.feedbackEvent.id;
1376
+ const memoryId = result.memoryRecord && result.memoryRecord.id;
1377
+ const feedbackStored = Boolean(feedbackId && feedbackRows.some((row) => row.id === feedbackId));
1378
+ const memoryStored = Boolean(memoryId && memoryRows.some((row) => row.id === memoryId));
1379
+ const ok = Boolean(result.accepted && feedbackStored && memoryStored);
1380
+
1381
+ const payload = {
1382
+ ok,
1383
+ command: 'feedback-self-test',
1384
+ signal: normalized,
1385
+ accepted: Boolean(result.accepted),
1386
+ feedbackId: feedbackId || null,
1387
+ memoryId: memoryId || null,
1388
+ feedbackStored,
1389
+ memoryStored,
1390
+ isolated,
1391
+ feedbackDir: paths.FEEDBACK_DIR,
1392
+ nextDogfoodCommand: `npx thumbgate capture --feedback=${normalized} --context=${shellSingleQuote(context)}`,
1393
+ };
1394
+
1395
+ if (args.json) {
1396
+ console.log(JSON.stringify(payload, null, 2));
1397
+ } else if (ok) {
1398
+ console.log('\nThumbGate feedback self-test: PASS');
1399
+ console.log('─'.repeat(50));
1400
+ console.log(` Captured : ${normalized} (${feedbackId})`);
1401
+ console.log(` Stored lesson: ${memoryId}`);
1402
+ console.log(` Storage : ${paths.FEEDBACK_DIR}`);
1403
+ console.log(` Mode : ${isolated ? 'isolated test store' : 'active ThumbGate store'}`);
1404
+ console.log('\nDogfood in chat with:');
1405
+ console.log(` thumbs ${normalized}: ${context}`);
1406
+ } else {
1407
+ console.log('\nThumbGate feedback self-test: FAIL');
1408
+ console.log('─'.repeat(50));
1409
+ console.log(` Accepted : ${payload.accepted}`);
1410
+ console.log(` Feedback stored: ${feedbackStored}`);
1411
+ console.log(` Memory stored : ${memoryStored}`);
1412
+ console.log(` Storage : ${paths.FEEDBACK_DIR}`);
1413
+ }
1414
+
1415
+ if (!ok) process.exit(2);
1416
+ } finally {
1417
+ if (previousFeedbackDir === undefined) delete process.env.THUMBGATE_FEEDBACK_DIR;
1418
+ else process.env.THUMBGATE_FEEDBACK_DIR = previousFeedbackDir;
1419
+ if (previousNoNudge === undefined) delete process.env.THUMBGATE_NO_NUDGE;
1420
+ else process.env.THUMBGATE_NO_NUDGE = previousNoNudge;
1421
+ }
1422
+ }
1423
+
909
1424
  function stats() {
910
1425
  trackEvent('cli_stats', { command: 'stats' });
911
1426
  const args = parseArgs(process.argv.slice(3));
912
1427
  const { analyzeFeedback } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
913
1428
  const data = analyzeFeedback();
914
1429
 
1430
+ // Gate enforcement stats — runtime intercepts + configured gates
1431
+ let gateData = { blocked: 0, warned: 0, passed: 0, byGate: {} };
1432
+ try { gateData = require(path.join(PKG_ROOT, 'scripts', 'gates-engine')).loadStats(); } catch {}
1433
+ let gateConfigData = { totalGates: 0, autoPromotedGates: 0, estimatedHoursSaved: '0.0', topBlocked: null, firstTimeFixRate: null };
1434
+ try { gateConfigData = require(path.join(PKG_ROOT, 'scripts', 'gate-stats')).calculateStats(); } catch {}
1435
+
915
1436
  const avgCostOfMistake = 2.50;
1437
+ const totalInterceptions = gateData.blocked + gateData.warned;
916
1438
  const payload = {
917
1439
  total: data.total,
918
1440
  positives: data.totalPositive,
@@ -922,6 +1444,13 @@ function stats() {
922
1444
  revenueAtRisk: Number((data.totalNegative * avgCostOfMistake).toFixed(2)),
923
1445
  topTags: data.topTags || [],
924
1446
  recentActivity: data.recentActivity || [],
1447
+ gatesBlocked: gateData.blocked,
1448
+ gatesWarned: gateData.warned,
1449
+ totalGates: gateConfigData.totalGates,
1450
+ autoPromotedGates: gateConfigData.autoPromotedGates,
1451
+ estimatedHoursSaved: gateConfigData.estimatedHoursSaved,
1452
+ topBlockedGate: gateConfigData.topBlocked ? gateConfigData.topBlocked.id : null,
1453
+ firstTimeFixRate: gateConfigData.firstTimeFixRate,
925
1454
  };
926
1455
 
927
1456
  if (args.json) {
@@ -935,17 +1464,36 @@ function stats() {
935
1464
  console.log(` Approval Rate : ${payload.approvalRate}%`);
936
1465
  console.log(` Recent Trend : ${payload.recentTrend}%`);
937
1466
 
1467
+ // Gate enforcement — the high-ROI section
1468
+ if (totalInterceptions > 0 || payload.totalGates > 0) {
1469
+ console.log('\n🛡️ PRE-ACTION GATE ENFORCEMENT');
1470
+ console.log(` Actions blocked : ${payload.gatesBlocked}`);
1471
+ console.log(` Actions warned : ${payload.gatesWarned}`);
1472
+ console.log(` Active gates : ${payload.totalGates} (${payload.autoPromotedGates} auto-promoted)`);
1473
+ if (payload.topBlockedGate) console.log(` Top blocker : ${payload.topBlockedGate}`);
1474
+ console.log(` Est. time saved : ~${payload.estimatedHoursSaved} hours`);
1475
+ const { formatFirstTimeFixRate } = require(path.join(PKG_ROOT, 'scripts', 'gate-stats'));
1476
+ console.log(` First-time fix : ${formatFirstTimeFixRate(payload.firstTimeFixRate)}`);
1477
+ }
1478
+
938
1479
  if (payload.negatives > 0) {
939
1480
  console.log('\n⚠️ REVENUE-AT-RISK ANALYSIS');
940
1481
  console.log(` Repeated Failures detected: ${payload.negatives}`);
941
1482
  console.log(` Estimated Operational Loss: $${payload.revenueAtRisk}`);
942
1483
  console.log(' Action Required: Run "npx thumbgate rules" to generate guardrails.');
943
1484
  console.log(' Strategic Recommendation: if this is a shared workflow problem, start the Workflow Hardening Sprint.');
944
- console.log(' Team intake: https://thumbgate-production.up.railway.app/#workflow-sprint-intake');
1485
+ console.log(' Team intake: https://thumbgate.ai/#workflow-sprint-intake');
945
1486
  console.log(' Solo side lane: npx thumbgate pro');
946
1487
  } else {
947
1488
  console.log('\n✅ System is currently high-reliability. No immediate revenue loss detected.');
948
1489
  }
1490
+ try {
1491
+ const { buildStatsReceipt } = require(path.join(PKG_ROOT, 'scripts', 'commercial-offer'));
1492
+ const receipt = buildStatsReceipt(payload);
1493
+ if (receipt) console.log(receipt);
1494
+ } catch (_) {
1495
+ // Keep stats resilient if the receipt helper is unavailable in old installs.
1496
+ }
949
1497
  proNudge();
950
1498
  }
951
1499
 
@@ -1051,13 +1599,14 @@ function pro() {
1051
1599
  } = require(path.join(PKG_ROOT, 'scripts', 'pro-local-dashboard'));
1052
1600
 
1053
1601
  function printProInfo() {
1054
- const hostedUrl = 'https://thumbgate-production.up.railway.app';
1602
+ const hostedUrl = 'https://thumbgate.ai';
1055
1603
  const truthUrl = 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/COMMERCIAL_TRUTH.md';
1056
1604
  console.log('\nThumbGate Pro — Local Dashboard');
1057
1605
  console.log('─'.repeat(50));
1058
1606
  console.log('Self-serve side lane today: Pro ($19/mo or $149/yr).');
1059
1607
  console.log('Every licensed Pro user gets a personal local dashboard on localhost.');
1060
1608
  console.log('\nWhat is available:');
1609
+ console.log(' - Hosted sync: keep lessons, rules, and dashboard state aligned across laptops, CI, containers, and agent runtimes');
1061
1610
  console.log(' - Local Pro dashboard: your own browser dashboard for search, gates, and DPO export');
1062
1611
  console.log(' - Team rollout path: shared hosted lessons, org visibility, and workflow proof');
1063
1612
  console.log(' - Commercial truth doc: source of truth for traction, pricing, and proof claims');
@@ -1195,6 +1744,13 @@ function lessons() {
1195
1744
  const tags = String(args.tags || '').split(',').map((t) => t.trim()).filter(Boolean);
1196
1745
  const query = args.query || process.argv.slice(3).find((a) => !a.startsWith('--')) || '';
1197
1746
  const limit = Number(args.limit || 10);
1747
+ const { checkLimit } = require(path.join(PKG_ROOT, 'scripts', 'rate-limiter'));
1748
+ const searchLimit = checkLimit('search_lessons');
1749
+ if (!searchLimit.allowed) {
1750
+ trackEvent('cli_upgrade_prompt', { command: 'lessons', action: 'search_lessons', limitType: searchLimit.limitType || null });
1751
+ limitNudge('search_lessons', searchLimit);
1752
+ process.exit(1);
1753
+ }
1198
1754
 
1199
1755
  // --remote: fetch from hosted Railway instance
1200
1756
  if (args.remote) {
@@ -1361,6 +1917,7 @@ function modelCandidatesCmd() {
1361
1917
  const maxCandidates = args.max ? Number(args.max) : undefined;
1362
1918
  const { reportPath, report } = writeModelCandidatesReport(undefined, {
1363
1919
  workload: args.workload,
1920
+ workloadFile: args['workload-file'] || args.workloadFile,
1364
1921
  provider: args.provider,
1365
1922
  family: args.family,
1366
1923
  gateway: args.gateway,
@@ -1461,13 +2018,10 @@ function risk() {
1461
2018
  }
1462
2019
 
1463
2020
  function exportDpo() {
1464
- const { isProTier } = require(path.join(PKG_ROOT, 'scripts', 'rate-limiter'));
1465
- if (!isProTier(null)) {
1466
- process.stderr.write(
1467
- `\n 🔒 DPO Export requires Pro (${PRO_PRICE_LABEL}).\n` +
1468
- ` Your feedback would generate valuable training pairs.\n` +
1469
- ` Upgrade: ${PRO_CHECKOUT_URL}\n\n`
1470
- );
2021
+ const { checkLimit } = require(path.join(PKG_ROOT, 'scripts', 'rate-limiter'));
2022
+ const exportLimit = checkLimit('export_dpo');
2023
+ if (!exportLimit.allowed) {
2024
+ limitNudge('export_dpo', exportLimit);
1471
2025
  process.exit(1);
1472
2026
  }
1473
2027
  const extraArgs = process.argv.slice(3).join(' ');
@@ -1484,13 +2038,10 @@ function exportDpo() {
1484
2038
  }
1485
2039
 
1486
2040
  function exportDatabricks() {
1487
- const { isProTier } = require(path.join(PKG_ROOT, 'scripts', 'rate-limiter'));
1488
- if (!isProTier(null)) {
1489
- process.stderr.write(
1490
- `\n 🔒 Databricks Export requires Pro (${PRO_PRICE_LABEL}).\n` +
1491
- ` Export feedback logs + proof artifacts for analytics.\n` +
1492
- ` Upgrade: ${PRO_CHECKOUT_URL}\n\n`
1493
- );
2041
+ const { checkLimit } = require(path.join(PKG_ROOT, 'scripts', 'rate-limiter'));
2042
+ const exportLimit = checkLimit('export_databricks');
2043
+ if (!exportLimit.allowed) {
2044
+ limitNudge('export_databricks', exportLimit);
1494
2045
  process.exit(1);
1495
2046
  }
1496
2047
  const extraArgs = process.argv.slice(3).join(' ');
@@ -1685,6 +2236,26 @@ function pulse() {
1685
2236
  });
1686
2237
  }
1687
2238
 
2239
+ function checkUpdateCmd() {
2240
+ const { checkUpdate } = require(path.join(PKG_ROOT, 'scripts', 'check-update'));
2241
+ const args = parseArgs(process.argv.slice(3));
2242
+ checkUpdate({ verbose: !args.json, force: args.force }).then((res) => {
2243
+ if (args.json) {
2244
+ console.log(JSON.stringify(res, null, 2));
2245
+ }
2246
+ process.exit(0);
2247
+ }).catch((err) => {
2248
+ console.error(err && err.message ? err.message : err);
2249
+ process.exit(1);
2250
+ });
2251
+ }
2252
+
2253
+ function selfUpdateCmd() {
2254
+ const { selfUpdate } = require(path.join(PKG_ROOT, 'scripts', 'check-update'));
2255
+ const success = selfUpdate();
2256
+ process.exit(success ? 0 : 1);
2257
+ }
2258
+
1688
2259
  function dispatchBrief() {
1689
2260
  const args = parseArgs(process.argv.slice(3));
1690
2261
  const {
@@ -1718,6 +2289,25 @@ function gateStats() {
1718
2289
  console.log('\n' + formatStats(stats) + '\n');
1719
2290
  }
1720
2291
 
2292
+ function contextPacks() {
2293
+ const args = parseArgs(process.argv.slice(3));
2294
+ const { generateAutoContextPacks } = require(path.join(PKG_ROOT, 'scripts', 'auto-context-packs'));
2295
+ const result = generateAutoContextPacks();
2296
+ if (args.json) {
2297
+ console.log(JSON.stringify(result, null, 2));
2298
+ return;
2299
+ }
2300
+ console.log(`\nGenerated ${result.packCount} context pack(s):`);
2301
+ for (const p of result.packs) {
2302
+ console.log(` [${p.type}] ${p.name}`);
2303
+ console.log(` -> ${p.filePath}`);
2304
+ }
2305
+ if (result.packCount === 0) {
2306
+ console.log(' (No failure patterns found yet — capture some feedback first.)');
2307
+ }
2308
+ console.log('');
2309
+ }
2310
+
1721
2311
  function harnessAudit() {
1722
2312
  const args = parseArgs(process.argv.slice(3));
1723
2313
  const {
@@ -1998,7 +2588,7 @@ function backgroundGovernance() {
1998
2588
  console.log(' - Run --check before dispatching an unattended PR job.');
1999
2589
  console.log(' - Route protected branches and large blast-radius jobs to human review.');
2000
2590
  console.log(' - Convert CI failures into thumbs-down lessons so repeats become Pre-Action Checks.');
2001
- console.log('\nGuide: https://thumbgate-production.up.railway.app/guides/background-agent-governance\n');
2591
+ console.log('\nGuide: https://thumbgate.ai/guides/background-agent-governance\n');
2002
2592
  }
2003
2593
 
2004
2594
  function optimize() {
@@ -2006,6 +2596,52 @@ function optimize() {
2006
2596
  doOptimize();
2007
2597
  }
2008
2598
 
2599
+ function syncGcp() {
2600
+ const { syncToGcp } = require(path.join(PKG_ROOT, 'adapters', 'gcp', 'sync.js'));
2601
+ syncToGcp();
2602
+ }
2603
+
2604
+ function cleanup() {
2605
+ console.log('Cleaning up ThumbGate processes...');
2606
+ try {
2607
+ const { execSync } = require('child_process');
2608
+ // Kill all 'thumbgate serve' and 'thumbgate dashboard' processes except this one
2609
+ const pids = execSync("ps aux | grep -E 'thumbgate (serve|dashboard|mcp)' | grep -v 'grep' | grep -v 'cleanup' | awk '{print $2}'", { encoding: 'utf8' })
2610
+ .split('\n')
2611
+ .filter(Boolean)
2612
+ .map(Number)
2613
+ .filter(pid => pid !== process.pid);
2614
+
2615
+ if (pids.length > 0) {
2616
+ console.log(`Killing ${pids.length} process(es): ${pids.join(', ')}`);
2617
+ pids.forEach(pid => {
2618
+ try { process.kill(pid, 'SIGTERM'); } catch (_) {}
2619
+ });
2620
+ // Give them a moment to die
2621
+ execSync('sleep 1');
2622
+ } else {
2623
+ console.log('No other ThumbGate processes found.');
2624
+ }
2625
+
2626
+ // Check port 3456 specifically
2627
+ try {
2628
+ const portPids = execSync("lsof -ti :3456", { encoding: 'utf8' })
2629
+ .split('\n')
2630
+ .map(s => s.trim())
2631
+ .filter(Boolean)
2632
+ .map(Number);
2633
+ portPids.forEach(pid => {
2634
+ console.log(`Killing process ${pid} holding port 3456`);
2635
+ try { process.kill(pid, 'SIGKILL'); } catch (_) {}
2636
+ });
2637
+ } catch (_) { /* port already free */ }
2638
+
2639
+ console.log('✅ Cleanup complete. Run "npx thumbgate pro" to restart the dashboard.');
2640
+ } catch (err) {
2641
+ console.error(`Cleanup failed: ${err.message}`);
2642
+ }
2643
+ }
2644
+
2009
2645
  function serve() {
2010
2646
  try {
2011
2647
  const { repairCodexHooks } = require(path.join(PKG_ROOT, 'scripts', 'codex-self-heal'));
@@ -2042,11 +2678,34 @@ function install() {
2042
2678
  }
2043
2679
 
2044
2680
  async function gateCheck() {
2045
- const payload = readStdinText();
2046
- const input = payload ? JSON.parse(payload) : {};
2047
- const gatesEngine = require(path.join(PKG_ROOT, 'scripts', 'gates-engine'));
2048
- const output = await gatesEngine.runAsync(input);
2049
- process.stdout.write(output + '\n');
2681
+ // Explicit emergency escape hatch ONLY. The 2026-06-03 hotfix made this
2682
+ // bypass-by-default, which silently disabled ThumbGate's enforcement entirely
2683
+ // (the firewall approved everything). Restored 2026-06-04: enforcement runs by
2684
+ // default in warn-by-default posture (see gates-engine applyEnforcementPosture);
2685
+ // set THUMBGATE_HOTFIX_BYPASS=1 to disable all checks if a gate ever misfires.
2686
+ if (process.env.THUMBGATE_HOTFIX_BYPASS === '1') {
2687
+ process.stdout.write(JSON.stringify({
2688
+ decision: 'approve',
2689
+ reason: 'hotfix-bypass-opt-in',
2690
+ hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: '' }
2691
+ }) + '\n');
2692
+ return;
2693
+ }
2694
+ try {
2695
+ const payload = readStdinText();
2696
+ const input = payload ? JSON.parse(payload) : {};
2697
+ const gatesEngine = require(path.join(PKG_ROOT, 'scripts', 'gates-engine'));
2698
+ const output = await gatesEngine.runAsync(input);
2699
+ process.stdout.write(output + '\n');
2700
+ } catch (err) {
2701
+ process.stderr.write(`gate-check error: ${err.message}\n`);
2702
+ process.stdout.write(JSON.stringify({
2703
+ hookSpecificOutput: {
2704
+ hookEventName: 'PreToolUse',
2705
+ additionalContext: `[ThumbGate Error] ${err.message}`,
2706
+ }
2707
+ }) + '\n');
2708
+ }
2050
2709
  }
2051
2710
 
2052
2711
  function cacheUpdate() {
@@ -2073,9 +2732,14 @@ function statuslineRender() {
2073
2732
 
2074
2733
  function hookAutoCapture() {
2075
2734
  syncActiveProjectContext();
2076
- const prompt = process.env.CLAUDE_USER_PROMPT || process.env.THUMBGATE_USER_PROMPT || readStdinText().trim();
2735
+ const prompt = process.env.CLAUDE_USER_PROMPT
2736
+ || process.env.THUMBGATE_USER_PROMPT
2737
+ || process.env.CODEX_USER_PROMPT
2738
+ || process.env.USER_PROMPT
2739
+ || readStdinText().trim();
2077
2740
  const { evaluatePromptGuard } = require(path.join(PKG_ROOT, 'scripts', 'prompt-guard'));
2078
2741
  const { processInlineFeedback, formatCliOutput } = require(path.join(PKG_ROOT, 'scripts', 'cli-feedback'));
2742
+ const { detectFeedbackSignal } = require(path.join(PKG_ROOT, 'scripts', 'feedback-quality'));
2079
2743
  const { loadOptionalModule } = require(path.join(PKG_ROOT, 'scripts', 'private-core-boundary'));
2080
2744
  const { recordConversationEntry, readRecentConversationWindow } = loadOptionalModule(
2081
2745
  path.join(PKG_ROOT, 'scripts', 'feedback-history-distiller'),
@@ -2097,14 +2761,12 @@ function hookAutoCapture() {
2097
2761
  return;
2098
2762
  }
2099
2763
 
2100
- const lower = prompt.toLowerCase();
2101
- const isUp = /(thumbs?\s*up|that worked|looks good|nice work|perfect|good job)/i.test(lower);
2102
- const isDown = /(thumbs?\s*down|that failed|that was wrong|fix this)/i.test(lower);
2103
- if (!isUp && !isDown) {
2764
+ const detected = detectFeedbackSignal(prompt);
2765
+ if (!detected) {
2104
2766
  return;
2105
2767
  }
2106
2768
 
2107
- const signal = isDown ? 'down' : 'up';
2769
+ const signal = detected.signal;
2108
2770
  const conversationWindow = readRecentConversationWindow({ limit: 8 });
2109
2771
  const result = processInlineFeedback({
2110
2772
  signal,
@@ -2185,6 +2847,27 @@ function installMcp() {
2185
2847
 
2186
2848
  function dashboard() {
2187
2849
  const args = parseArgs(process.argv.slice(3));
2850
+ if (args.open || args.web) {
2851
+ const { resolveProjectDir } = require(path.join(PKG_ROOT, 'scripts', 'feedback-paths'));
2852
+ const projectDir = resolveProjectDir({ cwd: process.cwd(), env: process.env });
2853
+ const port = process.env.PORT || 3456;
2854
+ const url = `http://127.0.0.1:${port}/dashboard?project=${encodeURIComponent(projectDir)}`;
2855
+
2856
+ ensureDash(Number(port))
2857
+ .then((server) => {
2858
+ if (server.started) {
2859
+ console.log(`API ${port} pid ${server.pid}`);
2860
+ }
2861
+ return openBrowser(url);
2862
+ })
2863
+ .then(() => process.exit(0))
2864
+ .catch((err) => {
2865
+ console.error(err && err.message ? err.message : err);
2866
+ process.exit(1);
2867
+ });
2868
+ return;
2869
+ }
2870
+
2188
2871
  const { printDashboard } = require(path.join(PKG_ROOT, 'scripts', 'dashboard'));
2189
2872
  const { getOperationalDashboard } = require(path.join(PKG_ROOT, 'scripts', 'operational-dashboard'));
2190
2873
 
@@ -2304,6 +2987,64 @@ function startApi() {
2304
2987
  }
2305
2988
  }
2306
2989
 
2990
+ function breakGlass() {
2991
+ const args = parseArgs(process.argv.slice(3));
2992
+ const positionalReason = process.argv.slice(3).find((arg) => !arg.startsWith('--'));
2993
+ const reason = String(args.reason || positionalReason || '').trim();
2994
+ if (!reason) {
2995
+ console.error('Usage: npx thumbgate break-glass --reason "why this recovery is needed" [--ttl=5m] [--json]');
2996
+ process.exit(1);
2997
+ }
2998
+
2999
+ const ttlMs = parseTtlMs(args.ttl, 5 * 60 * 1000);
3000
+ const { breakGlassEmergency } = require(path.join(PKG_ROOT, 'scripts', 'gates-engine'));
3001
+ const result = breakGlassEmergency({ reason, ttlMs });
3002
+ if (args.json) {
3003
+ console.log(JSON.stringify(result, null, 2));
3004
+ return;
3005
+ }
3006
+
3007
+ console.log('ThumbGate break-glass active.');
3008
+ console.log(` Reason : ${result.reason}`);
3009
+ console.log(` Expires : ${result.expiresAt}`);
3010
+ console.log(' Unlocked : hook settings edits, pr_create_allowed, pr_threads_checked');
3011
+ console.log(' Still gated: local-only scope, force-push, protected branch push, unsafe chmod, broad rm -rf');
3012
+ }
3013
+
3014
+ function aiInventory() {
3015
+ const args = parseArgs(process.argv.slice(3));
3016
+ const {
3017
+ scanAiComponents,
3018
+ buildCycloneDxMlBom,
3019
+ formatInventoryText,
3020
+ writeOutput,
3021
+ } = require(path.join(PKG_ROOT, 'scripts', 'ai-component-inventory'));
3022
+ const rootDir = path.resolve(String(args.root || args.cwd || CWD));
3023
+ const format = String(args.format || (args.json ? 'json' : 'summary')).toLowerCase();
3024
+ const inventory = scanAiComponents({
3025
+ rootDir,
3026
+ maxFiles: args['max-files'] ? Number(args['max-files']) : undefined,
3027
+ includeSnippets: args.snippets !== false,
3028
+ });
3029
+
3030
+ let payload;
3031
+ if (format === 'cyclonedx' || format === 'ml-bom' || format === 'mlbom') {
3032
+ payload = JSON.stringify(buildCycloneDxMlBom(inventory, { version: pkgVersion() }), null, 2);
3033
+ } else if (format === 'json') {
3034
+ payload = JSON.stringify(inventory, null, 2);
3035
+ } else {
3036
+ payload = formatInventoryText(inventory);
3037
+ }
3038
+
3039
+ if (args.output) {
3040
+ writeOutput(path.resolve(String(args.output)), `${payload}\n`);
3041
+ console.log(`Wrote AI inventory evidence to ${path.resolve(String(args.output))}`);
3042
+ return;
3043
+ }
3044
+
3045
+ console.log(payload);
3046
+ }
3047
+
2307
3048
  function help() {
2308
3049
  const v = pkgVersion();
2309
3050
  const helpArgs = process.argv.slice(3);
@@ -2319,13 +3060,19 @@ function help() {
2319
3060
  console.log('');
2320
3061
  console.log('Common commands:');
2321
3062
  console.log(' init Detect agent and wire ThumbGate hooks');
3063
+ console.log(' feedback-self-test Prove thumbs capture works locally');
2322
3064
  console.log(' capture --feedback=up|down --context="<text>" Capture a thumbs signal as a stored lesson');
2323
3065
  console.log(' stats Approval rate, recent trend, blocked-pattern count');
2324
3066
  console.log(' lessons [query] Search promoted lessons');
2325
3067
  console.log(' explore Interactive TUI for lessons, gates, stats');
2326
3068
  console.log(' dashboard Open the local ThumbGate dashboard');
3069
+ console.log(' ai-inventory Scan AI/ML components and export ML-BOM evidence');
2327
3070
  console.log(' doctor Audit runtime isolation + bootstrap context');
3071
+ console.log(' break-glass --reason="..." Short TTL recovery if gates over-fire');
3072
+ console.log(' brain [--write] Build the agent-readable context brain (lessons + rules + gates)');
2328
3073
  console.log(' pro ThumbGate Pro (dashboard, exports, sync)');
3074
+ console.log(' diagnostic $499 Workflow Hardening Diagnostic for one repeated team failure');
3075
+ console.log(' subscribe <email> Get the 5-min setup guide + weekly tips by email');
2329
3076
  console.log('');
2330
3077
  console.log('More:');
2331
3078
  console.log(' thumbgate help all Full subcommand surface (~70 commands)');
@@ -2371,7 +3118,11 @@ function help() {
2371
3118
 
2372
3119
  // Legacy and specialist commands kept visible until they graduate into the schema.
2373
3120
  console.log('Also available:');
2374
- console.log(' install-mcp Install MCP server into Claude Code settings (--project for local)');
3121
+ console.log(' install-mcp Install MCP server + PreToolUse hooks into Claude Code settings');
3122
+ console.log(' default: machine-wide (~/.claude/settings.json — shared dashboard)');
3123
+ console.log(' --project: per-repo (<cwd>/.claude/settings.json — isolated dashboard)');
3124
+ console.log(' --no-hooks: MCP only, skip hook wiring');
3125
+ console.log(' break-glass Short TTL recovery if gates over-fire');
2375
3126
  console.log(' cfo Hosted billing summary (local fallback JSON)');
2376
3127
  console.log(' billing:setup Generate operator key + print Railway setup instructions');
2377
3128
  console.log(' repair-github-marketplace Repair legacy GitHub Marketplace amount mappings');
@@ -2392,6 +3143,7 @@ function help() {
2392
3143
  console.log(' proxy-pointer-rag-guardrails Map visual document RAG signals to Document RAG Safety gates');
2393
3144
  console.log(' rag-precision-guardrails Map retrieval tuning regressions to Document RAG Safety gates');
2394
3145
  console.log(' ai-engineering-stack-guardrails Map gateway, MCP, AGENTS.md, LLM wiki, reviewer, and sandbox gaps to stack gates');
3146
+ console.log(' ai-inventory Scan AI/ML components and export JSON or CycloneDX ML-BOM evidence');
2395
3147
  console.log(' upstream-contributions Find dependency issues worth fixing without promotional PRs');
2396
3148
  console.log(' long-running-agent-context-guardrails Map structured-memory gaps to long-running agent gates');
2397
3149
  console.log(' reasoning-efficiency-guardrails Map reasoning compression signals to efficiency gates');
@@ -2416,6 +3168,7 @@ function help() {
2416
3168
 
2417
3169
  console.log('Examples:');
2418
3170
  console.log(' npx thumbgate init');
3171
+ console.log(' npx thumbgate feedback-self-test');
2419
3172
  console.log(' npx thumbgate status --json');
2420
3173
  console.log(' npx thumbgate explore lessons --json');
2421
3174
  console.log(' npx thumbgate explore gates --json');
@@ -2425,6 +3178,7 @@ function help() {
2425
3178
  console.log(' npx thumbgate proxy-pointer-rag-guardrails --tree-path=.rag/tree.json --image-pointers=paper-1/figures/fig2.png --documents=paper-1 --visual-claims --json');
2426
3179
  console.log(' npx thumbgate rag-precision-guardrails --baseline-recall=0.86 --new-recall=0.72 --threshold-change --agentic --structural-near-misses --json');
2427
3180
  console.log(' npx thumbgate ai-engineering-stack-guardrails --mcp-tool-count=182 --direct-provider-keys --llm-wiki-pages=24 --context-freshness-days=30 --background-agents --json');
3181
+ console.log(' npx thumbgate ai-inventory --format=cyclonedx --output=.thumbgate/ai-mlbom.json');
2428
3182
  console.log(' npx thumbgate long-running-agent-context-guardrails --request-count=80 --output-mb=3 --raw-chat-only --json');
2429
3183
  console.log(' npx thumbgate reasoning-efficiency-guardrails --baseline-tokens=1200 --compressed-tokens=980 --baseline-accuracy=0.84 --compressed-accuracy=0.85 --verifier --json');
2430
3184
  console.log(' npx thumbgate deepseek-v4-runtime-guardrails --context-tokens=900000 --hybrid-attention --speculative-decoding --accept-length=1.4 --precision-mode=fp8 --json');
@@ -2446,11 +3200,271 @@ if (COMMAND === 'daemon' || COMMAND === 'serve-daemon') {
2446
3200
  process.exit(0);
2447
3201
  }
2448
3202
 
3203
+ // ---------------------------------------------------------------------------
3204
+ // Global --help interceptor: `thumbgate <cmd> --help` shows per-command help
3205
+ // instead of running the command. Commands with their own --help guard (init)
3206
+ // handle it internally; everything else falls through here.
3207
+ // ---------------------------------------------------------------------------
3208
+ const _cliSubArgs = process.argv.slice(3);
3209
+ const _wantsHelp = _cliSubArgs.includes('--help') || _cliSubArgs.includes('-h');
3210
+
3211
+ const SUBCOMMAND_HELP = {
3212
+ capture: 'Usage: npx thumbgate capture --feedback=up|down --context="..." [--what-worked="..."] [--what-went-wrong="..."] [--what-to-change="..."] [--tags=a,b]',
3213
+ feedback: 'Usage: npx thumbgate feedback --feedback=up|down --context="..." [--what-worked="..."] [--what-went-wrong="..."] [--what-to-change="..."] [--tags=a,b]',
3214
+ 'feedback-self-test': 'Usage: npx thumbgate feedback-self-test [--json] [--persist] [--feedback=up|down]\n\nCapture a synthetic thumbs signal and verify feedback-log + memory-log writes. Defaults to an isolated test store; use --persist to dogfood the active ThumbGate store.',
3215
+ dogfood: 'Usage: npx thumbgate dogfood [--json] [--persist] [--feedback=up|down]\n\nAlias for feedback-self-test.',
3216
+ stats: 'Usage: npx thumbgate stats\n\nShow gate enforcement statistics: blocked/warned counts, active gates, time saved.',
3217
+ trial: 'Usage: npx thumbgate trial\n\nShow Pro trial status, remaining days, and upgrade path.',
3218
+ pro: 'Usage: npx thumbgate pro [--activate <key>]\n\nLaunch the local Pro dashboard or activate a Pro license key.',
3219
+ diagnostic: 'Usage: npx thumbgate diagnostic\n\nShow the $499 Workflow Hardening Diagnostic intake and checkout paths for teams with one repeated AI-agent workflow failure.',
3220
+ subscribe: 'Usage: npx thumbgate subscribe <email>\n\nSubscribe to the 5-minute setup guide + trial reminders.',
3221
+ lessons: 'Usage: npx thumbgate lessons [--query="..."] [--limit=N]\n\nSearch the lesson database (Pro feature).',
3222
+ search: 'Usage: npx thumbgate search <query>\n\nSearch ThumbGate knowledge base (Pro feature).',
3223
+ 'gate-check': 'Usage: npx thumbgate gate-check\n\nPreToolUse hook interface: reads tool call JSON from stdin, outputs gate verdict.',
3224
+ 'hermes-gate': 'Usage: npx thumbgate hermes-gate\n\nNous Research Hermes Agent pre_tool_call shell hook: reads Hermes tool-call JSON from stdin, runs the ThumbGate gate pipeline (strict by default), and outputs {"decision":"block","reason":...} to veto or {} to allow. Gates terminal/patch/skill_manage etc. See adapters/hermes/config.yaml.',
3225
+ 'break-glass': 'Usage: npx thumbgate break-glass --reason="why" [--ttl=5m] [--json]\n\nShort-lived recovery path for over-firing gates. Allows hook settings edits and satisfies PR-create/thread-check gates without disabling core destructive-action protections.',
3226
+ serve: 'Usage: npx thumbgate serve\n\nStart the MCP stdio server. This is for agent runtimes, not the local HTTP dashboard.',
3227
+ mcp: 'Usage: npx thumbgate mcp\n\nAlias for `thumbgate serve`.',
3228
+ dashboard: 'Usage: npx thumbgate dashboard [--window=today|7d|30d] [--open]\n\nPrint the operational dashboard summary or open the browser HTTP dashboard (use --open). Defaults to PORT=3456.',
3229
+ 'start-api': 'Usage: npx thumbgate start-api\n\nStart the local ThumbGate HTTP API/dashboard. Defaults to PORT=8787; use PORT=3456 for statusline localhost links.',
3230
+ 'export-dpo': 'Usage: npx thumbgate export-dpo [--format=jsonl|csv]\n\nExport feedback as DPO training pairs (Pro feature).',
3231
+ status: 'Usage: npx thumbgate status\n\nShow ThumbGate system health and active configuration.',
3232
+ watch: 'Usage: npx thumbgate watch\n\nWatch for feedback changes and auto-regenerate prevention rules.',
3233
+ compact: 'Usage: npx thumbgate compact\n\nCompact the lesson database and reclaim disk space.',
3234
+ 'context-packs': 'Usage: npx thumbgate context-packs\n\nGenerate context packs from top failure patterns.',
3235
+ suggest: 'Usage: npx thumbgate suggest <gate-id>\n\nSuggest fixes for a specific gate based on lesson history.',
3236
+ cost: 'Usage: npx thumbgate cost [--json] [--stats <path>] [--mix \'{"claude-sonnet-4-5":0.8,...}\']\n\nShow cumulative $ and tokens saved by PreToolUse gate blocks. Reads ~/.thumbgate/gate-stats.json.',
3237
+ savings: 'Usage: npx thumbgate savings [--json] [--stats <path>] [--mix \'{"claude-sonnet-4-5":0.8,...}\']\n\nAlias for `thumbgate cost`.',
3238
+ 'setup-vertex': 'Usage: npx thumbgate setup-vertex [--dry-run]\n\nAuto-enable Vertex AI API on GCP and write local Vertex routing config to .env. With --dry-run, only detect the active account/project and print the planned changes. This does not create or verify a Dialogflow CX agent; use the Dialogflow CX REST API or console for live-agent evidence.',
3239
+ 'ai-inventory': 'Usage: npx thumbgate ai-inventory [--root <dir>] [--format=summary|json|cyclonedx] [--output <path>] [--max-files=N]\n\nScan source/manifests/model artifacts for AI, ML, agent-framework, vector DB, Vertex, Gemini, and Dialogflow CX components. Use --format=cyclonedx to produce exportable ML-BOM evidence for enterprise reviews.',
3240
+ brain: 'Usage: npx thumbgate brain [--write] [--json] [--limit=N]\n\nBuild the agent-readable "context brain" — a single artifact consolidating this\nrepo\'s lessons, prevention rules, active gates, and project context for a coding\nagent to read BEFORE acting. --write saves it to .thumbgate/BRAIN.md (versioned,\ndeterministic). --json emits the structured model. --limit caps lessons (default 15).',
3241
+ 'team-sync': 'Usage: npx thumbgate team-sync\n\nSynchronize prevention rules and context brain with your team\'s git repository (git pull --rebase & git push), then auto-rebuild the local brain.',
3242
+ };
3243
+
3244
+ if (_wantsHelp && COMMAND && SUBCOMMAND_HELP[COMMAND]) {
3245
+ console.log(SUBCOMMAND_HELP[COMMAND]);
3246
+ process.exit(0);
3247
+ }
3248
+
3249
+ // -----------------------------------------------------------------------------
3250
+ // brain — consolidate ThumbGate's institutional memory (lessons + prevention
3251
+ // rules + active gates + project context) into a single, versioned,
3252
+ // agent-readable "context brain". The persistent context a coding agent should
3253
+ // read BEFORE acting, so it stops repeating mistakes. Composes the existing
3254
+ // explore-subcommands primitives; output is deterministic (no volatile
3255
+ // timestamp) so `.thumbgate/BRAIN.md` diffs only when the underlying memory
3256
+ // changes.
3257
+ // -----------------------------------------------------------------------------
3258
+ function buildBrainModel(opts = {}) {
3259
+ const { exploreLessons, exploreRules, exploreGates } = require(path.join(PKG_ROOT, 'scripts', 'explore-subcommands'));
3260
+ let feedbackDir;
3261
+ try {
3262
+ const { getFeedbackPaths } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
3263
+ feedbackDir = getFeedbackPaths().FEEDBACK_DIR;
3264
+ } catch (_) { feedbackDir = path.join(CWD, '.thumbgate'); }
3265
+ const limit = Math.max(1, Number(opts.limit) || 15);
3266
+ const safe = (fn, fallback) => { try { return fn(); } catch (_) { return fallback; } };
3267
+ const lessons = safe(() => exploreLessons({ feedbackDir, pkgRoot: PKG_ROOT, limit, json: true }), { lessons: [], total: 0 });
3268
+ const rules = safe(() => exploreRules({ feedbackDir, pkgRoot: PKG_ROOT, json: true }), { rules: [], total: 0 });
3269
+ const gates = safe(() => exploreGates({ feedbackDir, pkgRoot: PKG_ROOT, json: true }), { gates: [], total: 0 });
3270
+ const instructionFiles = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md', '.cursorrules', '.thumbgate/config.json']
3271
+ .filter((f) => { try { return fs.existsSync(path.join(CWD, f)); } catch (_) { return false; } });
3272
+ return { lessons, rules, gates, project: { root: CWD, instructionFiles } };
3273
+ }
3274
+
3275
+ function renderBrainMarkdown(model) {
3276
+ const out = [];
3277
+ out.push('# ThumbGate Context Brain');
3278
+ out.push('');
3279
+ out.push('<!-- The persistent context a coding agent should read BEFORE acting in this repo.');
3280
+ out.push(' Generated by `npx thumbgate brain`. Rebuild after capturing feedback. -->');
3281
+ out.push('');
3282
+ out.push('> Institutional memory for AI coding agents working here: what has been');
3283
+ out.push('> learned, what to avoid, and what is enforced. Read this first.');
3284
+ out.push('');
3285
+
3286
+ out.push('## What this codebase taught its agents (lessons)');
3287
+ out.push('');
3288
+ const lessons = (model.lessons && model.lessons.lessons) || [];
3289
+ if (!lessons.length) {
3290
+ out.push('_No lessons captured yet. Run `npx thumbgate capture --feedback=down --context="what failed"`._');
3291
+ } else {
3292
+ for (const l of lessons) {
3293
+ const sig = String(l.signal || '').toLowerCase();
3294
+ const mark = (sig.includes('positive') || sig === 'up') ? '✅' : ((sig.includes('negative') || sig === 'down') ? '⛔' : '•');
3295
+ const enforced = l.confidence === 'active' ? ' _(enforced)_' : '';
3296
+ const ctx = String(l.context || '').replace(/\s+/g, ' ').trim().slice(0, 220);
3297
+ if (ctx) out.push(`- ${mark} ${ctx}${enforced}`);
3298
+ }
3299
+ }
3300
+ out.push('');
3301
+
3302
+ out.push('## Guardrails — do NOT repeat these (prevention rules)');
3303
+ out.push('');
3304
+ const rules = (model.rules && model.rules.rules) || [];
3305
+ if (!rules.length) {
3306
+ out.push('_No prevention rules yet._');
3307
+ } else {
3308
+ for (const r of rules) {
3309
+ out.push(`- **${String(r.title || '').trim()}**`);
3310
+ const body = String(r.body || '').split('\n')[0].trim();
3311
+ if (body && body !== r.title) out.push(` - ${body.slice(0, 200)}`);
3312
+ }
3313
+ }
3314
+ out.push('');
3315
+
3316
+ out.push('## Active enforcement (gates)');
3317
+ out.push('');
3318
+ const gates = (model.gates && model.gates.gates) || [];
3319
+ if (!gates.length) {
3320
+ out.push('_No active gates._');
3321
+ } else {
3322
+ for (const g of gates) {
3323
+ const occ = g.occurrences ? ` (${g.occurrences}×)` : '';
3324
+ out.push(`- \`${g.pattern || g.id}\` → **${g.action}**${occ}`);
3325
+ }
3326
+ }
3327
+ out.push('');
3328
+
3329
+ out.push('## Project context');
3330
+ out.push('');
3331
+ out.push(`- Repo root: \`${model.project.root}\``);
3332
+ const files = model.project.instructionFiles;
3333
+ out.push(`- Agent instruction files: ${files.length ? files.map((f) => '`' + f + '`').join(', ') : '_none detected_'}`);
3334
+ out.push('');
3335
+ out.push('---');
3336
+ const lt = (model.lessons && model.lessons.total) || 0;
3337
+ const rt = (model.rules && model.rules.total) || 0;
3338
+ const gt = (model.gates && model.gates.total) || 0;
3339
+ out.push(`_${lt} lessons · ${rt} rules · ${gt} gates. Rebuild with \`npx thumbgate brain --write\`._`);
3340
+ out.push('');
3341
+ return out.join('\n');
3342
+ }
3343
+
3344
+ function cmdBrain(args = {}) {
3345
+ const model = buildBrainModel({ limit: args.limit });
3346
+ if (args.json) { console.log(JSON.stringify(model, null, 2)); return 0; }
3347
+ const md = renderBrainMarkdown(model);
3348
+ if (args.write) {
3349
+ const dir = path.join(CWD, '.thumbgate');
3350
+ const target = path.join(dir, 'BRAIN.md');
3351
+ try {
3352
+ fs.mkdirSync(dir, { recursive: true });
3353
+ fs.writeFileSync(target, md);
3354
+ } catch (err) {
3355
+ // Surface a clean, actionable error instead of an uncaught stack trace
3356
+ // (e.g. permission denied, read-only filesystem).
3357
+ console.error(`Could not write ${target}: ${err && err.message ? err.message : err}`);
3358
+ return 1;
3359
+ }
3360
+ const lt = (model.lessons && model.lessons.total) || 0;
3361
+ const rt = (model.rules && model.rules.total) || 0;
3362
+ const gt = (model.gates && model.gates.total) || 0;
3363
+ console.log(`\u{1f9e0} Wrote context brain to .thumbgate/BRAIN.md (${lt} lessons · ${rt} rules · ${gt} gates).`);
3364
+ console.log(' Point your agent at it: add "Read .thumbgate/BRAIN.md first" to CLAUDE.md / AGENTS.md.');
3365
+ return 0;
3366
+ }
3367
+ process.stdout.write(md);
3368
+ return 0;
3369
+ }
3370
+
3371
+ async function teamSync() {
3372
+ const { execSync } = require('child_process');
3373
+
3374
+ // Verify we are in a Git repo
3375
+ try {
3376
+ execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore', cwd: CWD });
3377
+ } catch (err) {
3378
+ console.error('❌ Error: The current directory is not a Git repository.');
3379
+ process.exit(1);
3380
+ }
3381
+
3382
+ console.log('🔄 Checking shared prevention rules status...');
3383
+
3384
+ const rulesRelative = '.thumbgate/prevention-rules.md';
3385
+ const brainRelative = '.thumbgate/BRAIN.md';
3386
+ const rulesPath = path.join(CWD, rulesRelative);
3387
+
3388
+ if (!fs.existsSync(rulesPath)) {
3389
+ console.log('⚠️ No local prevention rules file found to sync.');
3390
+ }
3391
+
3392
+ let statusOutput = '';
3393
+ try {
3394
+ statusOutput = execSync('git status --porcelain', { encoding: 'utf8', cwd: CWD }) || '';
3395
+ } catch (_) {}
3396
+ const hasLocalChanges = statusOutput.includes(rulesRelative) || statusOutput.includes(brainRelative) || statusOutput.includes('.thumbgate/');
3397
+
3398
+ if (hasLocalChanges) {
3399
+ console.log('📝 Local changes detected in prevention rules. Committing locally...');
3400
+ try {
3401
+ const filesToAdd = [rulesRelative, brainRelative].filter(f => fs.existsSync(path.join(CWD, f)));
3402
+ if (filesToAdd.length > 0) {
3403
+ const filesStr = filesToAdd.map(f => `"${f}"`).join(' ');
3404
+ execSync(`git add -f ${filesStr}`, { cwd: CWD });
3405
+ execSync('git commit -m "chore(thumbgate): update shared prevention rules [skip ci]"', { cwd: CWD });
3406
+ console.log('✅ Local rules committed successfully.');
3407
+ } else {
3408
+ console.log('✨ No local rules files exist to commit.');
3409
+ }
3410
+ } catch (e) {
3411
+ console.log('✨ No changes to commit (already staged/clean).');
3412
+ }
3413
+ } else {
3414
+ console.log('✨ No local rules changes to commit.');
3415
+ }
3416
+
3417
+ // Pull from remote
3418
+ console.log('📥 Pulling rules from teammate remote (git pull --rebase)...');
3419
+ try {
3420
+ execSync('git pull --rebase', { stdio: 'inherit', cwd: CWD });
3421
+ } catch (pullErr) {
3422
+ console.error('❌ Git pull failed. Please resolve conflicts manually.');
3423
+ process.exit(1);
3424
+ }
3425
+
3426
+ // Push to remote
3427
+ console.log('📤 Pushing rules to teammate remote (git push)...');
3428
+ try {
3429
+ execSync('git push', { stdio: 'inherit', cwd: CWD });
3430
+ } catch (pushErr) {
3431
+ console.error('❌ Git push failed. You may need to run git push manually.');
3432
+ process.exit(1);
3433
+ }
3434
+
3435
+ // Rebuild the context brain (.thumbgate/BRAIN.md) from the newly merged rules
3436
+ console.log('🧠 Rebuilding local context brain from merged rules...');
3437
+ try {
3438
+ const brainArgs = { write: true };
3439
+ cmdBrain(brainArgs);
3440
+ } catch (brainErr) {
3441
+ console.warn(`⚠️ Failed to rebuild context brain: ${brainErr.message}`);
3442
+ }
3443
+
3444
+ console.log('\n🚀 Team rules synchronization complete! Your agents now share team-wide learning.');
3445
+ }
3446
+
2449
3447
  switch (COMMAND) {
3448
+ case 'team-sync':
3449
+ case 'git-sync':
3450
+ teamSync().catch((err) => {
3451
+ console.error(err && err.message ? err.message : err);
3452
+ process.exit(1);
3453
+ });
3454
+ break;
3455
+ case '--version':
3456
+ case '-v':
3457
+ case 'version':
3458
+ console.log(pkgVersion());
3459
+ break;
2450
3460
  case 'init':
2451
3461
  init();
2452
3462
  upgradeNudge();
2453
3463
  break;
3464
+ case 'quickstart':
3465
+ case 'first-rule':
3466
+ quickstart();
3467
+ break;
2454
3468
  case 'quick-start':
2455
3469
  quickStart();
2456
3470
  break;
@@ -2464,6 +3478,12 @@ switch (COMMAND) {
2464
3478
  case 'mcp':
2465
3479
  serve();
2466
3480
  break;
3481
+ case 'cleanup':
3482
+ cleanup();
3483
+ break;
3484
+ case 'sync-gcp':
3485
+ syncGcp();
3486
+ break;
2467
3487
  case 'gate-check':
2468
3488
  gateCheck().catch((err) => {
2469
3489
  console.error(err && err.message ? err.message : err);
@@ -2487,6 +3507,16 @@ switch (COMMAND) {
2487
3507
  capture();
2488
3508
  upgradeNudge();
2489
3509
  break;
3510
+ case 'feedback-self-test':
3511
+ case 'dogfood':
3512
+ feedbackSelfTest();
3513
+ break;
3514
+ case 'setup-vertex':
3515
+ setupVertex(parseArgs(process.argv.slice(3))).catch((err) => {
3516
+ console.error(err && err.message ? err.message : err);
3517
+ process.exit(1);
3518
+ });
3519
+ break;
2490
3520
  case 'stats':
2491
3521
  stats();
2492
3522
  upgradeNudge();
@@ -2495,6 +3525,27 @@ switch (COMMAND) {
2495
3525
  case 'revenue':
2496
3526
  cfo();
2497
3527
  break;
3528
+ case 'cost':
3529
+ case 'savings':
3530
+ case 'costs': {
3531
+ const { main: costMain } = require(path.join(PKG_ROOT, 'scripts', 'cost-cli'));
3532
+ process.exit(costMain(process.argv.slice(3)));
3533
+ // process.exit doesn't return, but keep an explicit break so the switch
3534
+ // cannot accidentally fall through to case 'billing:setup' if a future
3535
+ // refactor wraps costMain in try/finally that intercepts the exit, or
3536
+ // a test runner stubs process.exit (flagged by gitar-bot on PR #2281).
3537
+ break;
3538
+ }
3539
+ case 'brain': {
3540
+ const sub = process.argv.slice(3).find((arg) => !arg.startsWith('--'));
3541
+ if (sub && ['init', 'context', 'remember', 'check', 'cleanup', 'status'].includes(sub)) {
3542
+ brain();
3543
+ } else {
3544
+ const brainArgs = parseArgs(process.argv.slice(3));
3545
+ process.exitCode = cmdBrain(brainArgs);
3546
+ }
3547
+ break;
3548
+ }
2498
3549
  case 'billing:setup':
2499
3550
  require(path.join(PKG_ROOT, 'scripts', 'billing-setup'));
2500
3551
  break;
@@ -2511,6 +3562,11 @@ switch (COMMAND) {
2511
3562
  case 'search-lessons':
2512
3563
  lessons();
2513
3564
  break;
3565
+ case 'notes': {
3566
+ const { cli: notesCli } = require(path.join(PKG_ROOT, 'scripts', 'implementation-notes'));
3567
+ notesCli(process.argv.slice(3));
3568
+ break;
3569
+ }
2514
3570
  case 'lesson-health':
2515
3571
  case 'stale': {
2516
3572
  const { initDB } = require(path.join(PKG_ROOT, 'scripts', 'lesson-db'));
@@ -2652,6 +3708,9 @@ switch (COMMAND) {
2652
3708
  case 'rules':
2653
3709
  rules();
2654
3710
  break;
3711
+ case 'context-packs':
3712
+ contextPacks();
3713
+ break;
2655
3714
  case 'harness-audit':
2656
3715
  case 'harness':
2657
3716
  harnessAudit();
@@ -2681,6 +3740,12 @@ switch (COMMAND) {
2681
3740
  case 'llm-wiki-guardrails':
2682
3741
  aiEngineeringStackGuardrails();
2683
3742
  break;
3743
+ case 'ai-inventory':
3744
+ case 'ai-component-inventory':
3745
+ case 'ml-bom':
3746
+ case 'mlbom':
3747
+ aiInventory();
3748
+ break;
2684
3749
  case 'deepseek-v4-runtime-guardrails':
2685
3750
  case 'deepseek-runtime-guardrails':
2686
3751
  case 'sparse-attention-runtime-guardrails':
@@ -2745,9 +3810,90 @@ switch (COMMAND) {
2745
3810
  case 'self-heal':
2746
3811
  selfHeal();
2747
3812
  break;
3813
+ case 'workflow':
3814
+ case 'swarm': {
3815
+ const args = parseArgs(process.argv.slice(3));
3816
+ let objective = args.objective;
3817
+ if (!objective) {
3818
+ const firstPositional = process.argv.slice(3).find((a, idx, arr) => {
3819
+ if (a.startsWith('--')) return false;
3820
+ const prev = arr[idx - 1];
3821
+ if (prev && prev.startsWith('--') && !prev.includes('=')) return false;
3822
+ return true;
3823
+ });
3824
+ if (firstPositional) objective = firstPositional;
3825
+ }
3826
+ if (!objective) {
3827
+ console.error('Error: objective is required. Run with --objective="your objective" or provide it as a positional argument.');
3828
+ process.exit(1);
3829
+ }
3830
+ const { executeWorkflow } = require(path.join(PKG_ROOT, 'scripts', 'parallel-workflow-orchestrator'));
3831
+ const concurrency = args.concurrency ? Number(args.concurrency) : undefined;
3832
+ const timeoutMs = args.timeoutMs ? Number(args.timeoutMs) : undefined;
3833
+ executeWorkflow(objective, { concurrency, timeoutMs, cwd: CWD })
3834
+ .then((res) => {
3835
+ if (args.json) {
3836
+ console.log(JSON.stringify(res, null, 2));
3837
+ } else {
3838
+ console.log(`\n✅ Parallel workflow execution complete.`);
3839
+ console.log(` Workflow ID: ${res.workflowId}`);
3840
+ console.log(` Objective : ${res.objective}`);
3841
+ console.log(` Duration : ${(res.durationMs / 1000).toFixed(2)}s`);
3842
+ console.log(` Report Path: ${res.reportPath}`);
3843
+ console.log(`\nReport Summary:\n`);
3844
+ console.log(fs.readFileSync(res.reportPath, 'utf8'));
3845
+ }
3846
+ process.exit(0);
3847
+ })
3848
+ .catch((err) => {
3849
+ console.error('Workflow execution failed:', err.message);
3850
+ process.exit(1);
3851
+ });
3852
+ break;
3853
+ }
3854
+ case 'trial': {
3855
+ // Show trial status — connects the 4K monthly npm installers to checkout
3856
+ const { isProTier, isInTrialPeriod, trialDaysRemaining, getInstallAgeDays } = require(path.join(PKG_ROOT, 'scripts', 'rate-limiter'));
3857
+ const ageDays = getInstallAgeDays();
3858
+ const inTrial = isInTrialPeriod();
3859
+ const remaining = trialDaysRemaining();
3860
+ const isPro = isProTier();
3861
+ console.log('');
3862
+ console.log(' ThumbGate Pro Trial');
3863
+ console.log(' ──────────────────');
3864
+ if (isPro && !inTrial) {
3865
+ console.log(' Status: ✅ Pro (active license or API key)');
3866
+ } else if (inTrial) {
3867
+ const expiryDate = new Date(Date.now() + remaining * 86400000).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
3868
+ console.log(` Status: 🎁 Active (${remaining} day${remaining === 1 ? '' : 's'} remaining)`);
3869
+ console.log(` Expires: ${expiryDate}`);
3870
+ console.log('');
3871
+ console.log(' All Pro features unlocked:');
3872
+ console.log(' • Unlimited prevention rules (free tier: 5)');
3873
+ console.log(' • Recall, lesson search, DPO export');
3874
+ console.log(' • Cross-machine sync via API key');
3875
+ } else {
3876
+ console.log(' Status: ⏰ Expired');
3877
+ if (ageDays !== null) {
3878
+ console.log(` Installed: ${Math.floor(ageDays)} days ago`);
3879
+ }
3880
+ console.log('');
3881
+ console.log(' Free tier: 5 active rules, no recall/search/export');
3882
+ }
3883
+ console.log('');
3884
+ console.log(` Keep Pro: ${PRO_CHECKOUT_URL}`);
3885
+ console.log(` Or set: THUMBGATE_API_KEY=<your-key>`);
3886
+ console.log('');
3887
+ break;
3888
+ }
2748
3889
  case 'pro':
2749
3890
  pro();
2750
3891
  break;
3892
+ case 'diagnostic':
3893
+ case 'workflow-diagnostic':
3894
+ case 'sprint-diagnostic':
3895
+ diagnostic();
3896
+ break;
2751
3897
  case 'activate':
2752
3898
  // Top-level alias: npx thumbgate activate <key>
2753
3899
  process.argv.splice(3, 0, '--activate');
@@ -2762,12 +3908,24 @@ switch (COMMAND) {
2762
3908
  case 'status':
2763
3909
  status();
2764
3910
  break;
3911
+ case 'break-glass':
3912
+ case 'breakglass':
3913
+ breakGlass();
3914
+ break;
2765
3915
  case 'funnel':
2766
3916
  funnel();
2767
3917
  break;
2768
3918
  case 'pulse':
2769
3919
  pulse();
2770
3920
  break;
3921
+ case 'check-update':
3922
+ case 'upgrade-check':
3923
+ checkUpdateCmd();
3924
+ break;
3925
+ case 'self-update':
3926
+ case 'upgrade-cli':
3927
+ selfUpdateCmd();
3928
+ break;
2771
3929
  case 'dispatch':
2772
3930
  case 'dispatch-brief':
2773
3931
  dispatchBrief();
@@ -2776,6 +3934,7 @@ switch (COMMAND) {
2776
3934
  // PreToolUse hook interface: reads tool call JSON from stdin, outputs gate verdict
2777
3935
  // Used by: generate-pretool-hook.sh → npx thumbgate gate-check
2778
3936
  const { run: gateRun, runAsync: gateRunAsync } = require(path.join(PKG_ROOT, 'scripts', 'gates-engine'));
3937
+ const { evaluateSelfProtection } = require(path.join(PKG_ROOT, 'scripts', 'self-protection'));
2779
3938
  let stdinData = '';
2780
3939
  process.stdin.setEncoding('utf8');
2781
3940
  process.stdin.on('data', (chunk) => { stdinData += chunk; });
@@ -2783,7 +3942,32 @@ switch (COMMAND) {
2783
3942
  try {
2784
3943
  const input = JSON.parse(stdinData);
2785
3944
  const output = await gateRunAsync(input);
2786
- process.stdout.write(output + '\n');
3945
+ // Self-protection overlay (2026-07-08): the gate engine only denies edits
3946
+ // to ThumbGate's own governance files under strict enforcement; by default
3947
+ // such edits pass as a clean ALLOW. Surface them (or block in strict) so an
3948
+ // agent can't silently disable the firewall. Only overlays when the engine
3949
+ // did not already produce a hard deny.
3950
+ let verdict = output;
3951
+ try {
3952
+ const parsed = JSON.parse(output || '{}');
3953
+ const alreadyDeny = (parsed.hookSpecificOutput && parsed.hookSpecificOutput.permissionDecision === 'deny')
3954
+ || parsed.decision === 'block';
3955
+ if (!alreadyDeny) {
3956
+ const sp = evaluateSelfProtection(input.tool_name, input.tool_input);
3957
+ if (sp && sp.action === 'block') {
3958
+ verdict = JSON.stringify({
3959
+ decision: 'block',
3960
+ reason: sp.message,
3961
+ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: sp.message },
3962
+ });
3963
+ } else if (sp && sp.action === 'warn') {
3964
+ verdict = JSON.stringify({
3965
+ hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: sp.message },
3966
+ });
3967
+ }
3968
+ }
3969
+ } catch (_e) { /* non-JSON engine output: pass through unchanged */ }
3970
+ process.stdout.write(verdict + '\n');
2787
3971
  process.exit(0);
2788
3972
  } catch (err) {
2789
3973
  process.stderr.write(`gate-check error: ${err.message}\n`);
@@ -2793,6 +3977,53 @@ switch (COMMAND) {
2793
3977
  });
2794
3978
  break;
2795
3979
  }
3980
+ case 'hermes-gate': {
3981
+ // Nous Research Hermes Agent `pre_tool_call` shell hook.
3982
+ // Hermes pipes each pending tool call as JSON to stdin and reads a decision from stdout;
3983
+ // {"decision":"block","reason":...} vetoes the call. We reuse the SAME gate pipeline as
3984
+ // `gate-check` (runAsync → secret guard, security scan, force-push / skill_manage / learned
3985
+ // prevention rules) and translate the verdict into Hermes's format.
3986
+ //
3987
+ // Hermes `pre_tool_call` is binary (block or allow) with no warn channel, and the whole point
3988
+ // of wiring it is to gate, so we run STRICT enforcement by default — otherwise ThumbGate's
3989
+ // warn-by-default posture would pass every deny through and the hook would block nothing.
3990
+ // Opt out with THUMBGATE_HERMES_WARN_ONLY=1; THUMBGATE_HOTFIX_BYPASS=1 still disables checks.
3991
+ // Wire it in ~/.hermes/config.yaml — see adapters/hermes/config.yaml.
3992
+ if (process.env.THUMBGATE_HERMES_WARN_ONLY !== '1' && process.env.THUMBGATE_HOTFIX_BYPASS !== '1') {
3993
+ process.env.THUMBGATE_STRICT_ENFORCEMENT = '1';
3994
+ }
3995
+ const { runAsync: hermesGateRun } = require(path.join(PKG_ROOT, 'scripts', 'gates-engine'));
3996
+ let hermesStdin = '';
3997
+ process.stdin.setEncoding('utf8');
3998
+ process.stdin.on('data', (chunk) => { hermesStdin += chunk; });
3999
+ process.stdin.on('end', async () => {
4000
+ try {
4001
+ const payload = JSON.parse(hermesStdin);
4002
+ // Hermes sends snake_case tool_name/tool_input — gates-engine reads these directly.
4003
+ const verdict = await hermesGateRun({ tool_name: payload.tool_name, tool_input: payload.tool_input });
4004
+ let parsed = {};
4005
+ try { parsed = JSON.parse(verdict); } catch (_e) { parsed = {}; }
4006
+ const hso = parsed.hookSpecificOutput || {};
4007
+ if (hso.permissionDecision === 'deny') {
4008
+ process.stdout.write(JSON.stringify({
4009
+ decision: 'block',
4010
+ reason: hso.permissionDecisionReason || 'Blocked by ThumbGate prevention rule.',
4011
+ }) + '\n');
4012
+ } else {
4013
+ // warn / no match → allow. The gate engine already logged the decision.
4014
+ process.stdout.write(JSON.stringify({}) + '\n');
4015
+ }
4016
+ process.exit(0);
4017
+ } catch (err) {
4018
+ // Hermes hooks fail OPEN on error/timeout — emit an explicit allow so a gate fault
4019
+ // never wedges the agent (reliability ≈ enforcement; keep this fast).
4020
+ process.stderr.write(`hermes-gate error: ${err.message}\n`);
4021
+ process.stdout.write(JSON.stringify({}) + '\n');
4022
+ process.exit(0);
4023
+ }
4024
+ });
4025
+ break;
4026
+ }
2796
4027
  case 'gate-stats':
2797
4028
  gateStats();
2798
4029
  break;
@@ -2847,6 +4078,29 @@ switch (COMMAND) {
2847
4078
  }
2848
4079
  break;
2849
4080
  }
4081
+ case 'audit': {
4082
+ const auditFile = process.argv[3];
4083
+ if (!auditFile) {
4084
+ console.error('Usage: npx thumbgate audit <path-to-transcript.txt>');
4085
+ process.exit(1);
4086
+ }
4087
+ const { runAudit } = require(path.join(PKG_ROOT, 'scripts', 'audit'));
4088
+ const { results, totalWaste, error } = runAudit(auditFile);
4089
+ if (error) {
4090
+ console.error(error);
4091
+ process.exit(1);
4092
+ }
4093
+ console.log('\n🔍 AI Bill Audit Results\n');
4094
+ if (results.length === 0) {
4095
+ console.log('✅ No repeat-offender patterns found. Your sessions are efficient!');
4096
+ } else {
4097
+ console.table(results);
4098
+ console.log('\n💰 Total estimated monthly waste: $' + totalWaste);
4099
+ console.log('\nBlock these mistakes permanently with ThumbGate Pro:');
4100
+ console.log(PRO_CHECKOUT_URL);
4101
+ }
4102
+ break;
4103
+ }
2850
4104
  case 'dashboard':
2851
4105
  dashboard();
2852
4106
  break;
@@ -2870,6 +4124,86 @@ switch (COMMAND) {
2870
4124
  case 'compact':
2871
4125
  compact();
2872
4126
  break;
4127
+ case 'subscribe': {
4128
+ // Capture an installer's email so we can send the 5-minute setup
4129
+ // guide + weekly tips. Drops to /v1/marketing/install-email on the
4130
+ // hosted endpoint; the server fires sendNewsletterWelcomeEmail via
4131
+ // Resend and persists the address for follow-up.
4132
+ //
4133
+ // Usage:
4134
+ // npx thumbgate subscribe you@company.com
4135
+ // npx thumbgate subscribe --email=you@company.com
4136
+ //
4137
+ // Never prompts interactively — postinstall must stay non-interactive
4138
+ // for CI safety. This is the deliberate opt-in path the banner points
4139
+ // at; if the operator runs it, they want the email.
4140
+ const args = process.argv.slice(3);
4141
+ let email = '';
4142
+ for (const arg of args) {
4143
+ if (arg.startsWith('--email=')) email = arg.slice('--email='.length).trim();
4144
+ else if (!arg.startsWith('--')) email = arg.trim();
4145
+ }
4146
+ if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
4147
+ console.error('Usage: npx thumbgate subscribe <email>');
4148
+ console.error(' npx thumbgate subscribe --email=you@company.com');
4149
+ console.error('');
4150
+ console.error('We send a 5-minute setup guide + weekly tips. One-click unsubscribe.');
4151
+ process.exit(1);
4152
+ }
4153
+ const endpoint = process.env.THUMBGATE_INSTALL_EMAIL_ENDPOINT
4154
+ || 'https://thumbgate.ai/v1/marketing/install-email';
4155
+ const payload = JSON.stringify({
4156
+ email,
4157
+ source: 'cli_subscribe',
4158
+ installId: (() => {
4159
+ try {
4160
+ const configPath = path.join(CWD, '.thumbgate', 'config.json');
4161
+ if (!fs.existsSync(configPath)) return null;
4162
+ return (JSON.parse(fs.readFileSync(configPath, 'utf8')).installId) || null;
4163
+ } catch { return null; }
4164
+ })(),
4165
+ cliVersion: pkgVersion(),
4166
+ });
4167
+ const url = new URL(endpoint);
4168
+ const transport = url.protocol === 'https:' ? require('node:https') : require('node:http');
4169
+ const req = transport.request({
4170
+ method: 'POST',
4171
+ hostname: url.hostname,
4172
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
4173
+ path: url.pathname + url.search,
4174
+ headers: {
4175
+ 'Content-Type': 'application/json',
4176
+ 'Content-Length': Buffer.byteLength(payload),
4177
+ 'User-Agent': `thumbgate-cli/${pkgVersion()}`,
4178
+ },
4179
+ timeout: 8000,
4180
+ }, (res) => {
4181
+ let body = '';
4182
+ res.on('data', (chunk) => { body += chunk; });
4183
+ res.on('end', () => {
4184
+ if (res.statusCode >= 200 && res.statusCode < 300) {
4185
+ console.log(`✓ Subscribed ${email}.`);
4186
+ console.log(' Check your inbox in ~30 seconds for the setup guide.');
4187
+ console.log(' Unsubscribe link is in every email.');
4188
+ process.exit(0);
4189
+ } else {
4190
+ console.error(`✗ Subscribe failed: HTTP ${res.statusCode}${body ? ` — ${body.slice(0, 200)}` : ''}`);
4191
+ process.exit(2);
4192
+ }
4193
+ });
4194
+ });
4195
+ req.on('error', (err) => {
4196
+ console.error(`✗ Subscribe failed: ${err.message}`);
4197
+ console.error(' Network issue? Try again, or email igor.ganapolsky@gmail.com directly.');
4198
+ process.exit(3);
4199
+ });
4200
+ req.on('timeout', () => {
4201
+ req.destroy(new Error('timeout after 8000ms'));
4202
+ });
4203
+ req.write(payload);
4204
+ req.end();
4205
+ break;
4206
+ }
2873
4207
  case 'checkin': {
2874
4208
  // User check-in command — asks how it's going after install
2875
4209
  const thumbgateDir = path.join(CWD, '.thumbgate');