thumbgate 1.27.4 → 1.27.7

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 (104) 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 +2 -1
  9. package/.well-known/llms.txt +6 -2
  10. package/.well-known/mcp/server-card.json +1 -1
  11. package/README.md +49 -5
  12. package/adapters/claude/.mcp.json +2 -2
  13. package/adapters/letta/README.md +41 -0
  14. package/adapters/letta/thumbgate-letta-adapter.js +133 -0
  15. package/adapters/mcp/server-stdio.js +16 -1
  16. package/adapters/opencode/opencode.json +1 -1
  17. package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
  18. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
  19. package/bench/observability-eval-suite.json +26 -0
  20. package/bin/cli.js +230 -6
  21. package/bin/postinstall.js +1 -1
  22. package/commands/dashboard.md +15 -0
  23. package/commands/thumbgate-dashboard.md +15 -0
  24. package/config/gate-templates.json +84 -0
  25. package/config/gates/claim-verification.json +12 -0
  26. package/config/gates/default.json +20 -0
  27. package/config/github-about.json +1 -1
  28. package/config/model-candidates.json +50 -0
  29. package/config/post-deploy-marketing-pages.json +5 -0
  30. package/package.json +67 -25
  31. package/public/agent-manager.html +41 -1
  32. package/public/agents-cost-savings.html +1 -1
  33. package/public/ai-malpractice-prevention.html +2 -1
  34. package/public/assets/brand/github-social-preview.png +0 -0
  35. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  36. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  37. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  38. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  39. package/public/assets/brand/thumbgate-mark-inline.svg +15 -0
  40. package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
  41. package/public/assets/brand/thumbgate-mark-team.svg +26 -0
  42. package/public/assets/brand/thumbgate-mark.svg +15 -0
  43. package/public/assets/brand/thumbgate-wordmark.svg +20 -0
  44. package/public/assets/claude-thumbgate-statusbar.svg +8 -0
  45. package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
  46. package/public/assets/legal-intake-control-flow.svg +66 -0
  47. package/public/blog.html +1 -1
  48. package/public/brand/thumbgate-mark.svg +15 -0
  49. package/public/brand/thumbgate-og.svg +16 -0
  50. package/public/codex-enterprise.html +1 -1
  51. package/public/codex-plugin.html +1 -1
  52. package/public/compare.html +23 -3
  53. package/public/dashboard.html +316 -30
  54. package/public/federal.html +1 -1
  55. package/public/guide.html +5 -4
  56. package/public/index.html +167 -49
  57. package/public/js/buyer-intent.js +672 -0
  58. package/public/learn.html +88 -7
  59. package/public/lessons.html +2 -1
  60. package/public/numbers.html +3 -3
  61. package/public/pricing.html +63 -15
  62. package/public/pro.html +7 -7
  63. package/scripts/activation-quickstart.js +187 -0
  64. package/scripts/agent-memory-lifecycle.js +211 -0
  65. package/scripts/async-eval-observability.js +236 -0
  66. package/scripts/auto-promote-gates.js +75 -4
  67. package/scripts/billing.js +12 -1
  68. package/scripts/build-metadata.js +24 -3
  69. package/scripts/cli-schema.js +42 -10
  70. package/scripts/dashboard-chat.js +53 -7
  71. package/scripts/dashboard.js +12 -17
  72. package/scripts/export-databricks-bundle.js +5 -1
  73. package/scripts/export-dpo-pairs.js +7 -2
  74. package/scripts/feedback-aggregate.js +281 -0
  75. package/scripts/feedback-loop.js +121 -0
  76. package/scripts/filesystem-search.js +35 -10
  77. package/scripts/gates-engine.js +234 -7
  78. package/scripts/gemini-embedding-policy.js +2 -1
  79. package/scripts/hook-stop-anti-claim.js +227 -0
  80. package/scripts/hook-thumbgate-cache-updater.js +18 -2
  81. package/scripts/hybrid-feedback-context.js +1 -0
  82. package/scripts/lesson-inference.js +8 -3
  83. package/scripts/lesson-search.js +17 -1
  84. package/scripts/operational-integrity.js +39 -5
  85. package/scripts/plausible-domain-config.js +15 -2
  86. package/scripts/plausible-server-events.js +4 -4
  87. package/scripts/rate-limiter.js +12 -6
  88. package/scripts/secret-redaction.js +166 -0
  89. package/scripts/security-scanner.js +100 -0
  90. package/scripts/self-distill-agent.js +3 -1
  91. package/scripts/self-harness-optimizer.js +141 -0
  92. package/scripts/seo-gsd.js +635 -0
  93. package/scripts/statusline-cache-path.js +17 -2
  94. package/scripts/statusline-cache-read.js +57 -0
  95. package/scripts/statusline-local-stats.js +9 -1
  96. package/scripts/statusline-meta.js +5 -2
  97. package/scripts/statusline.sh +13 -1
  98. package/scripts/sync-telemetry-from-prod.js +374 -0
  99. package/scripts/telemetry-analytics.js +9 -0
  100. package/scripts/thumbgate-search.js +85 -19
  101. package/scripts/tool-contract-validator.js +76 -0
  102. package/scripts/vector-store.js +44 -0
  103. package/scripts/workspace-evolver.js +62 -2
  104. package/src/api/server.js +862 -146
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const DEFAULT_ENDPOINT = 'https://api.oraclestechnologies.com/v1/guardian/analyze';
5
+
6
+ function requireApiKey(env = process.env) {
7
+ const key = env.ETHICORE_API_KEY || env.GUARDIAN_API_KEY || env.ORACLES_GUARDIAN_API_KEY;
8
+ if (!key) {
9
+ throw new Error('ETHICORE_API_KEY env var is required');
10
+ }
11
+ return key;
12
+ }
13
+
14
+ async function analyzeText(text, options = {}) {
15
+ if (!String(text || '').trim()) {
16
+ throw new Error('analyzeText requires text');
17
+ }
18
+
19
+ const env = options.env || process.env;
20
+ const endpoint = options.endpoint || env.ETHICORE_GUARDIAN_ENDPOINT || DEFAULT_ENDPOINT;
21
+ const apiKey = options.apiKey || requireApiKey(env);
22
+ const fetchImpl = options.fetch || fetch;
23
+
24
+ const response = await fetchImpl(endpoint, {
25
+ method: 'POST',
26
+ headers: {
27
+ Authorization: `Bearer ${apiKey}`,
28
+ 'Content-Type': 'application/json',
29
+ },
30
+ body: JSON.stringify({ text }),
31
+ });
32
+
33
+ const bodyText = await response.text();
34
+ let body = bodyText;
35
+ try {
36
+ body = bodyText ? JSON.parse(bodyText) : {};
37
+ } catch {
38
+ // Keep non-JSON body for diagnostics.
39
+ }
40
+
41
+ if (!response.ok) {
42
+ throw new Error(`Ethicore Guardian API ${response.status}: ${typeof body === 'string' ? body : JSON.stringify(body)}`);
43
+ }
44
+
45
+ return body;
46
+ }
47
+
48
+ function createEthicorePolicyCheck(options = {}) {
49
+ return async function ethicorePolicyCheck(action = {}) {
50
+ const toolText = [
51
+ action.toolName,
52
+ action.actionType,
53
+ action.command,
54
+ action.path,
55
+ action.url,
56
+ action.input ? JSON.stringify(action.input) : '',
57
+ ].filter(Boolean).join('\n');
58
+
59
+ return analyzeText(toolText || JSON.stringify(action), options);
60
+ };
61
+ }
62
+
63
+ module.exports = {
64
+ DEFAULT_ENDPOINT,
65
+ analyzeText,
66
+ createEthicorePolicyCheck,
67
+ requireApiKey,
68
+ };
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { normalizeProviderAction } = require('../../scripts/provider-action-normalizer');
5
+
6
+ const BLOCK_DECISIONS = new Set([
7
+ 'block',
8
+ 'blocked',
9
+ 'deny',
10
+ 'denied',
11
+ 'disallow',
12
+ 'disallowed',
13
+ 'fail',
14
+ 'failed',
15
+ 'forbid',
16
+ 'forbidden',
17
+ 'reject',
18
+ 'rejected',
19
+ 'unsafe',
20
+ 'violation',
21
+ ]);
22
+
23
+ const REVIEW_DECISIONS = new Set([
24
+ 'approval',
25
+ 'approval-required',
26
+ 'approval_required',
27
+ 'approve',
28
+ 'human-review',
29
+ 'human_review',
30
+ 'manual-review',
31
+ 'manual_review',
32
+ 'review',
33
+ 'requires-approval',
34
+ 'requires_approval',
35
+ 'requires-review',
36
+ 'requires_review',
37
+ ]);
38
+
39
+ const ALLOW_DECISIONS = new Set([
40
+ 'accept',
41
+ 'accepted',
42
+ 'allow',
43
+ 'allowed',
44
+ 'ok',
45
+ 'pass',
46
+ 'passed',
47
+ 'permit',
48
+ 'permitted',
49
+ 'safe',
50
+ ]);
51
+
52
+ function asObject(value) {
53
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
54
+ }
55
+
56
+ function asArray(value) {
57
+ return Array.isArray(value) ? value : [];
58
+ }
59
+
60
+ function firstString(...values) {
61
+ for (const value of values) {
62
+ const text = String(value || '').trim();
63
+ if (text) return text;
64
+ }
65
+ return '';
66
+ }
67
+
68
+ function normalizeDecisionToken(value) {
69
+ return String(value || '')
70
+ .trim()
71
+ .toLowerCase()
72
+ .replace(/\s+/g, '-');
73
+ }
74
+
75
+ function normalizeEvidence(value) {
76
+ const direct = asArray(value.evidence);
77
+ const citations = asArray(value.citations);
78
+ const violations = asArray(value.violations);
79
+ const reasons = asArray(value.reasons);
80
+ const reasoning = asArray(value.reasoning);
81
+ const threatTypes = asArray(value.threat_types || value.threatTypes)
82
+ .map((threatType) => ({
83
+ code: String(threatType || '').trim(),
84
+ message: `Threat type: ${String(threatType || '').trim()}`,
85
+ source: 'guardian',
86
+ severity: firstString(value.threat_level, value.threatLevel),
87
+ }));
88
+ return [...direct, ...citations, ...violations, ...reasons, ...reasoning, ...threatTypes]
89
+ .map((entry) => {
90
+ if (typeof entry === 'string') return { text: entry };
91
+ const object = asObject(entry);
92
+ if (!Object.keys(object).length) return null;
93
+ return {
94
+ id: firstString(object.id, object.ruleId, object.rule_id, object.code),
95
+ text: firstString(object.text, object.reason, object.message, object.description, object.title),
96
+ source: firstString(object.source, object.provider, object.policy),
97
+ severity: firstString(object.severity, object.level),
98
+ raw: object,
99
+ };
100
+ })
101
+ .filter(Boolean);
102
+ }
103
+
104
+ function extractPolicyDecision(input = {}) {
105
+ const event = asObject(input);
106
+ for (const candidate of [
107
+ event.policyDecision,
108
+ event.policy_decision,
109
+ event.guardrailResult,
110
+ event.guardrail_result,
111
+ event.result,
112
+ ]) {
113
+ const object = asObject(candidate);
114
+ if (Object.keys(object).length) return object;
115
+ }
116
+ return event;
117
+ }
118
+
119
+ function classifyPolicyDecision(input = {}) {
120
+ const value = extractPolicyDecision(input);
121
+ const token = normalizeDecisionToken(firstString(
122
+ value.decision,
123
+ value.action,
124
+ value.status,
125
+ value.result,
126
+ value.verdict,
127
+ value.outcome,
128
+ value.effect,
129
+ value.recommended_action,
130
+ value.recommendedAction,
131
+ ));
132
+
133
+ if (
134
+ value.allowed === false
135
+ || value.is_safe === false
136
+ || value.isSafe === false
137
+ || value.accepted === false
138
+ || value.blocked === true
139
+ || value.denied === true
140
+ || BLOCK_DECISIONS.has(token)
141
+ ) {
142
+ return 'block';
143
+ }
144
+ if (
145
+ value.requiresApproval === true
146
+ || value.requires_approval === true
147
+ || value.reviewRequired === true
148
+ || value.review_required === true
149
+ || REVIEW_DECISIONS.has(token)
150
+ ) {
151
+ return 'approval_required';
152
+ }
153
+ if (
154
+ value.allowed === true
155
+ || value.is_safe === true
156
+ || value.isSafe === true
157
+ || value.accepted === true
158
+ || ALLOW_DECISIONS.has(token)
159
+ ) {
160
+ return 'allow';
161
+ }
162
+ return 'unknown';
163
+ }
164
+
165
+ function normalizePolicyDecision(input = {}, options = {}) {
166
+ const value = extractPolicyDecision(input);
167
+ const decision = classifyPolicyDecision(value);
168
+ const source = firstString(
169
+ options.source,
170
+ value.source,
171
+ value.provider,
172
+ value.engine,
173
+ value.policyEngine,
174
+ value.policy_engine,
175
+ 'policy-engine'
176
+ );
177
+ const reason = firstString(
178
+ value.reason,
179
+ value.message,
180
+ value.explanation,
181
+ value.summary,
182
+ asArray(value.reasoning).join('; '),
183
+ asArray(value.reasons).join('; '),
184
+ decision === 'unknown' ? 'Policy engine returned an unknown decision; approval required before execution.' : ''
185
+ );
186
+
187
+ return {
188
+ allowed: decision === 'allow',
189
+ blocked: decision === 'block',
190
+ approvalRequired: decision === 'approval_required' || decision === 'unknown',
191
+ decision,
192
+ reason,
193
+ source,
194
+ confidence: Number.isFinite(Number(value.confidence)) ? Number(value.confidence) : null,
195
+ policyId: firstString(value.policyId, value.policy_id, value.ruleId, value.rule_id, value.id),
196
+ severity: firstString(value.severity, value.level, value.threat_level, value.threatLevel),
197
+ score: Number.isFinite(Number(value.score))
198
+ ? Number(value.score)
199
+ : (Number.isFinite(Number(value.threat_score)) ? Number(value.threat_score) : null),
200
+ evidence: normalizeEvidence(value),
201
+ raw: value,
202
+ };
203
+ }
204
+
205
+ function normalizePolicyAction(input = {}) {
206
+ const event = asObject(input);
207
+ return {
208
+ ...normalizeProviderAction({
209
+ ...event,
210
+ provider: firstString(event.provider, event.agentRuntime, event.runtime, 'policy-engine'),
211
+ toolName: firstString(event.toolName, event.tool_name, event.name),
212
+ input: asObject(event.toolInput || event.input || event.arguments || event.args),
213
+ }),
214
+ policyContext: asObject(event.policyContext || event.policy_context),
215
+ };
216
+ }
217
+
218
+ function createPolicyEngineGuard({
219
+ policyCheck,
220
+ executeTool,
221
+ gateCheck,
222
+ onDecision,
223
+ source = 'policy-engine',
224
+ } = {}) {
225
+ if (typeof policyCheck !== 'function') {
226
+ throw new TypeError('createPolicyEngineGuard requires a policyCheck function');
227
+ }
228
+ if (typeof executeTool !== 'function') {
229
+ throw new TypeError('createPolicyEngineGuard requires an executeTool function');
230
+ }
231
+
232
+ return async function guardedPolicyTool(input = {}) {
233
+ const normalizedAction = normalizePolicyAction(input);
234
+ const policyDecision = normalizePolicyDecision(await policyCheck(normalizedAction), { source });
235
+ const gateDecision = typeof gateCheck === 'function'
236
+ ? normalizePolicyDecision(await gateCheck({ normalizedAction, policyDecision }), { source: 'thumbgate' })
237
+ : null;
238
+ const effectiveDecision = gateDecision && !gateDecision.allowed ? gateDecision : policyDecision;
239
+
240
+ if (typeof onDecision === 'function') {
241
+ await onDecision({ normalizedAction, policyDecision, gateDecision, effectiveDecision });
242
+ }
243
+
244
+ if (!effectiveDecision.allowed) {
245
+ const error = new Error(effectiveDecision.reason || 'ThumbGate blocked this action before execution.');
246
+ error.code = effectiveDecision.approvalRequired ? 'THUMBGATE_APPROVAL_REQUIRED' : 'THUMBGATE_BLOCKED';
247
+ error.thumbgate = { normalizedAction, policyDecision, gateDecision, effectiveDecision };
248
+ throw error;
249
+ }
250
+
251
+ return executeTool(input, { normalizedAction, policyDecision, gateDecision, effectiveDecision });
252
+ };
253
+ }
254
+
255
+ module.exports = {
256
+ createPolicyEngineGuard,
257
+ extractPolicyDecision,
258
+ normalizePolicyAction,
259
+ normalizePolicyDecision,
260
+ };
@@ -0,0 +1,26 @@
1
+ {
2
+ "cases": [
3
+ {
4
+ "id": "checkout-link-grounding",
5
+ "traceId": "trace_checkout_001",
6
+ "question": "Is the Pro checkout link working?",
7
+ "response": "The Pro checkout link is working because the checkout diagnostic confirms Stripe Pro checkout is reachable.",
8
+ "retrievedContexts": [
9
+ "Stripe Pro checkout diagnostic confirms the Pro checkout link is reachable and returns a valid checkout target.",
10
+ "The workflow sprint checkout is intake-led and should not expose a stale payment link."
11
+ ],
12
+ "reference": "The Pro checkout link is reachable when the Stripe diagnostic passes."
13
+ },
14
+ {
15
+ "id": "letta-tool-gate",
16
+ "traceId": "trace_letta_001",
17
+ "question": "Should Letta execute a force push tool call?",
18
+ "response": "ThumbGate blocks force push before execution for Letta tool calls.",
19
+ "retrievedContexts": [
20
+ "ThumbGate's Letta adapter wraps Letta tool calls and runs gate-check before the executor is called.",
21
+ "Force push to main is a blocked high-risk git action."
22
+ ],
23
+ "reference": "ThumbGate blocks high-risk Letta tool calls before execution."
24
+ }
25
+ ]
26
+ }
package/bin/cli.js CHANGED
@@ -61,14 +61,19 @@ const TRIAL_DAYS = 7;
61
61
 
62
62
  function checkoutUrlFor(source, content) {
63
63
  try {
64
- const url = new URL(PRO_CHECKOUT_URL);
64
+ const base = content === 'capture_feedback'
65
+ ? 'https://buy.stripe.com/7sYfZhaiE1eSbO99uj3sI0d'
66
+ : PRO_CHECKOUT_URL;
67
+ const url = new URL(base);
65
68
  url.searchParams.set('utm_source', source || 'cli');
66
69
  url.searchParams.set('utm_medium', 'cli');
67
70
  url.searchParams.set('utm_campaign', 'pro_conversion');
68
71
  if (content) url.searchParams.set('utm_content', content);
69
72
  return url.toString();
70
73
  } catch (_) {
71
- return PRO_CHECKOUT_URL;
74
+ return content === 'capture_feedback'
75
+ ? 'https://buy.stripe.com/7sYfZhaiE1eSbO99uj3sI0d'
76
+ : PRO_CHECKOUT_URL;
72
77
  }
73
78
  }
74
79
 
@@ -565,6 +570,20 @@ function setupCodex() {
565
570
  }
566
571
 
567
572
  function setupGemini() {
573
+ // Try to import custom commands as a Gemini plugin if the CLI is installed
574
+ const { execSync } = require('child_process');
575
+ let pluginImported = false;
576
+ for (const binName of ['agy', 'gemini']) {
577
+ try {
578
+ execSync(`${binName} plugin import "${PKG_ROOT}" --force`, { stdio: 'ignore' });
579
+ console.log(` Gemini: imported thumbgate plugin via ${binName}`);
580
+ pluginImported = true;
581
+ break;
582
+ } catch (err) {
583
+ // ignore errors if command doesn't exist or fails
584
+ }
585
+ }
586
+
568
587
  const settingsPath = path.join(HOME, '.gemini', 'settings.json');
569
588
  if (fs.existsSync(settingsPath)) {
570
589
  const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
@@ -585,13 +604,14 @@ function setupGemini() {
585
604
  }
586
605
  }
587
606
 
588
- if (!changed) return false;
607
+ if (!changed) return pluginImported;
589
608
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
590
609
  console.log(' Gemini: updated ~/.gemini/settings.json');
591
610
  return true;
592
611
  }
593
612
  // Fallback: project-level .gemini/settings.json
594
- return mergeMcpJson(path.join(CWD, '.gemini', 'settings.json'), 'Gemini', 'project');
613
+ const mcpChanged = mergeMcpJson(path.join(CWD, '.gemini', 'settings.json'), 'Gemini', 'project');
614
+ return pluginImported || mcpChanged;
595
615
  }
596
616
 
597
617
  function setupAmp() {
@@ -837,6 +857,14 @@ function quickStart() {
837
857
  console.log('');
838
858
  }
839
859
 
860
+ // Activation walkthrough (guided first rule + live demonstrated block).
861
+ // Implementation lives in scripts/activation-quickstart.js so it can be unit
862
+ // tested without executing the CLI's top-level command switch. `init` is
863
+ // deliberately untouched — this is an additive, separate command.
864
+ function quickstart() {
865
+ return require(path.join(PKG_ROOT, 'scripts', 'activation-quickstart')).quickstart();
866
+ }
867
+
840
868
  function init(cliArgs = parseArgs(process.argv.slice(3))) {
841
869
  const args = { ...cliArgs };
842
870
  if (args.help || args.h) {
@@ -910,6 +938,32 @@ function init(cliArgs = parseArgs(process.argv.slice(3))) {
910
938
  // Always create .mcp.json (project-level MCP config used by Claude, Codex, Cursor)
911
939
  mergeMcpJson(path.join(CWD, '.mcp.json'), 'MCP');
912
940
 
941
+ // Copy custom slash commands (.claude/commands/*.md) to the project's config directories
942
+ const pkgCommandsDir = path.join(PKG_ROOT, '.claude', 'commands');
943
+ if (fs.existsSync(pkgCommandsDir)) {
944
+ const targets = [
945
+ path.join(CWD, '.claude', 'commands'),
946
+ path.join(CWD, '.gemini', 'commands'),
947
+ path.join(CWD, '.antigravitycli', 'commands')
948
+ ];
949
+ for (const projectCommandsDir of targets) {
950
+ if (!fs.existsSync(projectCommandsDir)) {
951
+ fs.mkdirSync(projectCommandsDir, { recursive: true });
952
+ }
953
+ try {
954
+ const files = fs.readdirSync(pkgCommandsDir);
955
+ for (const file of files) {
956
+ if (file.endsWith('.md')) {
957
+ fs.copyFileSync(path.join(pkgCommandsDir, file), path.join(projectCommandsDir, file));
958
+ }
959
+ }
960
+ } catch (err) {
961
+ console.log(` Failed to copy custom commands to ${path.relative(CWD, projectCommandsDir)}: ${err.message}`);
962
+ }
963
+ }
964
+ console.log('Scaffolded custom slash commands directories (.claude, .gemini, .antigravitycli)');
965
+ }
966
+
913
967
  // Auto-detect and configure platform-specific locations
914
968
  console.log('');
915
969
  console.log('Detecting platforms...');
@@ -1815,6 +1869,7 @@ function modelCandidatesCmd() {
1815
1869
  const maxCandidates = args.max ? Number(args.max) : undefined;
1816
1870
  const { reportPath, report } = writeModelCandidatesReport(undefined, {
1817
1871
  workload: args.workload,
1872
+ workloadFile: args['workload-file'] || args.workloadFile,
1818
1873
  provider: args.provider,
1819
1874
  family: args.family,
1820
1875
  gateway: args.gateway,
@@ -2133,6 +2188,26 @@ function pulse() {
2133
2188
  });
2134
2189
  }
2135
2190
 
2191
+ function checkUpdateCmd() {
2192
+ const { checkUpdate } = require(path.join(PKG_ROOT, 'scripts', 'check-update'));
2193
+ const args = parseArgs(process.argv.slice(3));
2194
+ checkUpdate({ verbose: !args.json, force: args.force }).then((res) => {
2195
+ if (args.json) {
2196
+ console.log(JSON.stringify(res, null, 2));
2197
+ }
2198
+ process.exit(0);
2199
+ }).catch((err) => {
2200
+ console.error(err && err.message ? err.message : err);
2201
+ process.exit(1);
2202
+ });
2203
+ }
2204
+
2205
+ function selfUpdateCmd() {
2206
+ const { selfUpdate } = require(path.join(PKG_ROOT, 'scripts', 'check-update'));
2207
+ const success = selfUpdate();
2208
+ process.exit(success ? 0 : 1);
2209
+ }
2210
+
2136
2211
  function dispatchBrief() {
2137
2212
  const args = parseArgs(process.argv.slice(3));
2138
2213
  const {
@@ -3096,6 +3171,7 @@ const SUBCOMMAND_HELP = {
3096
3171
  lessons: 'Usage: npx thumbgate lessons [--query="..."] [--limit=N]\n\nSearch the lesson database (Pro feature).',
3097
3172
  search: 'Usage: npx thumbgate search <query>\n\nSearch ThumbGate knowledge base (Pro feature).',
3098
3173
  'gate-check': 'Usage: npx thumbgate gate-check\n\nPreToolUse hook interface: reads tool call JSON from stdin, outputs gate verdict.',
3174
+ '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.',
3099
3175
  '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.',
3100
3176
  serve: 'Usage: npx thumbgate serve\n\nStart the MCP stdio server. This is for agent runtimes, not the local HTTP dashboard.',
3101
3177
  mcp: 'Usage: npx thumbgate mcp\n\nAlias for `thumbgate serve`.',
@@ -3112,6 +3188,7 @@ const SUBCOMMAND_HELP = {
3112
3188
  '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.',
3113
3189
  '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.',
3114
3190
  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).',
3191
+ '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.',
3115
3192
  };
3116
3193
 
3117
3194
  if (_wantsHelp && COMMAND && SUBCOMMAND_HELP[COMMAND]) {
@@ -3241,7 +3318,90 @@ function cmdBrain(args = {}) {
3241
3318
  return 0;
3242
3319
  }
3243
3320
 
3321
+ async function teamSync() {
3322
+ const { execSync } = require('child_process');
3323
+
3324
+ // Verify we are in a Git repo
3325
+ try {
3326
+ execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore', cwd: CWD });
3327
+ } catch (err) {
3328
+ console.error('❌ Error: The current directory is not a Git repository.');
3329
+ process.exit(1);
3330
+ }
3331
+
3332
+ console.log('🔄 Checking shared prevention rules status...');
3333
+
3334
+ const rulesRelative = '.thumbgate/prevention-rules.md';
3335
+ const brainRelative = '.thumbgate/BRAIN.md';
3336
+ const rulesPath = path.join(CWD, rulesRelative);
3337
+
3338
+ if (!fs.existsSync(rulesPath)) {
3339
+ console.log('⚠️ No local prevention rules file found to sync.');
3340
+ }
3341
+
3342
+ let statusOutput = '';
3343
+ try {
3344
+ statusOutput = execSync('git status --porcelain', { encoding: 'utf8', cwd: CWD }) || '';
3345
+ } catch (_) {}
3346
+ const hasLocalChanges = statusOutput.includes(rulesRelative) || statusOutput.includes(brainRelative) || statusOutput.includes('.thumbgate/');
3347
+
3348
+ if (hasLocalChanges) {
3349
+ console.log('📝 Local changes detected in prevention rules. Committing locally...');
3350
+ try {
3351
+ const filesToAdd = [rulesRelative, brainRelative].filter(f => fs.existsSync(path.join(CWD, f)));
3352
+ if (filesToAdd.length > 0) {
3353
+ const filesStr = filesToAdd.map(f => `"${f}"`).join(' ');
3354
+ execSync(`git add -f ${filesStr}`, { cwd: CWD });
3355
+ execSync('git commit -m "chore(thumbgate): update shared prevention rules [skip ci]"', { cwd: CWD });
3356
+ console.log('✅ Local rules committed successfully.');
3357
+ } else {
3358
+ console.log('✨ No local rules files exist to commit.');
3359
+ }
3360
+ } catch (e) {
3361
+ console.log('✨ No changes to commit (already staged/clean).');
3362
+ }
3363
+ } else {
3364
+ console.log('✨ No local rules changes to commit.');
3365
+ }
3366
+
3367
+ // Pull from remote
3368
+ console.log('📥 Pulling rules from teammate remote (git pull --rebase)...');
3369
+ try {
3370
+ execSync('git pull --rebase', { stdio: 'inherit', cwd: CWD });
3371
+ } catch (pullErr) {
3372
+ console.error('❌ Git pull failed. Please resolve conflicts manually.');
3373
+ process.exit(1);
3374
+ }
3375
+
3376
+ // Push to remote
3377
+ console.log('📤 Pushing rules to teammate remote (git push)...');
3378
+ try {
3379
+ execSync('git push', { stdio: 'inherit', cwd: CWD });
3380
+ } catch (pushErr) {
3381
+ console.error('❌ Git push failed. You may need to run git push manually.');
3382
+ process.exit(1);
3383
+ }
3384
+
3385
+ // Rebuild the context brain (.thumbgate/BRAIN.md) from the newly merged rules
3386
+ console.log('🧠 Rebuilding local context brain from merged rules...');
3387
+ try {
3388
+ const brainArgs = { write: true };
3389
+ cmdBrain(brainArgs);
3390
+ } catch (brainErr) {
3391
+ console.warn(`⚠️ Failed to rebuild context brain: ${brainErr.message}`);
3392
+ }
3393
+
3394
+ console.log('\n🚀 Team rules synchronization complete! Your agents now share team-wide learning.');
3395
+ }
3396
+
3244
3397
  switch (COMMAND) {
3398
+ case 'team-sync':
3399
+ case 'git-sync':
3400
+ teamSync().catch((err) => {
3401
+ console.error(err && err.message ? err.message : err);
3402
+ process.exit(1);
3403
+ });
3404
+ break;
3245
3405
  case '--version':
3246
3406
  case '-v':
3247
3407
  case 'version':
@@ -3251,6 +3411,10 @@ switch (COMMAND) {
3251
3411
  init();
3252
3412
  upgradeNudge();
3253
3413
  break;
3414
+ case 'quickstart':
3415
+ case 'first-rule':
3416
+ quickstart();
3417
+ break;
3254
3418
  case 'quick-start':
3255
3419
  quickStart();
3256
3420
  break;
@@ -3323,8 +3487,13 @@ switch (COMMAND) {
3323
3487
  break;
3324
3488
  }
3325
3489
  case 'brain': {
3326
- const brainArgs = parseArgs(process.argv.slice(3));
3327
- process.exitCode = cmdBrain(brainArgs);
3490
+ const sub = process.argv.slice(3).find((arg) => !arg.startsWith('--'));
3491
+ if (sub && ['init', 'context', 'remember', 'check', 'cleanup', 'status'].includes(sub)) {
3492
+ brain();
3493
+ } else {
3494
+ const brainArgs = parseArgs(process.argv.slice(3));
3495
+ process.exitCode = cmdBrain(brainArgs);
3496
+ }
3328
3497
  break;
3329
3498
  }
3330
3499
  case 'billing:setup':
@@ -3694,6 +3863,14 @@ switch (COMMAND) {
3694
3863
  case 'pulse':
3695
3864
  pulse();
3696
3865
  break;
3866
+ case 'check-update':
3867
+ case 'upgrade-check':
3868
+ checkUpdateCmd();
3869
+ break;
3870
+ case 'self-update':
3871
+ case 'upgrade-cli':
3872
+ selfUpdateCmd();
3873
+ break;
3697
3874
  case 'dispatch':
3698
3875
  case 'dispatch-brief':
3699
3876
  dispatchBrief();
@@ -3719,6 +3896,53 @@ switch (COMMAND) {
3719
3896
  });
3720
3897
  break;
3721
3898
  }
3899
+ case 'hermes-gate': {
3900
+ // Nous Research Hermes Agent `pre_tool_call` shell hook.
3901
+ // Hermes pipes each pending tool call as JSON to stdin and reads a decision from stdout;
3902
+ // {"decision":"block","reason":...} vetoes the call. We reuse the SAME gate pipeline as
3903
+ // `gate-check` (runAsync → secret guard, security scan, force-push / skill_manage / learned
3904
+ // prevention rules) and translate the verdict into Hermes's format.
3905
+ //
3906
+ // Hermes `pre_tool_call` is binary (block or allow) with no warn channel, and the whole point
3907
+ // of wiring it is to gate, so we run STRICT enforcement by default — otherwise ThumbGate's
3908
+ // warn-by-default posture would pass every deny through and the hook would block nothing.
3909
+ // Opt out with THUMBGATE_HERMES_WARN_ONLY=1; THUMBGATE_HOTFIX_BYPASS=1 still disables checks.
3910
+ // Wire it in ~/.hermes/config.yaml — see adapters/hermes/config.yaml.
3911
+ if (process.env.THUMBGATE_HERMES_WARN_ONLY !== '1' && process.env.THUMBGATE_HOTFIX_BYPASS !== '1') {
3912
+ process.env.THUMBGATE_STRICT_ENFORCEMENT = '1';
3913
+ }
3914
+ const { runAsync: hermesGateRun } = require(path.join(PKG_ROOT, 'scripts', 'gates-engine'));
3915
+ let hermesStdin = '';
3916
+ process.stdin.setEncoding('utf8');
3917
+ process.stdin.on('data', (chunk) => { hermesStdin += chunk; });
3918
+ process.stdin.on('end', async () => {
3919
+ try {
3920
+ const payload = JSON.parse(hermesStdin);
3921
+ // Hermes sends snake_case tool_name/tool_input — gates-engine reads these directly.
3922
+ const verdict = await hermesGateRun({ tool_name: payload.tool_name, tool_input: payload.tool_input });
3923
+ let parsed = {};
3924
+ try { parsed = JSON.parse(verdict); } catch (_e) { parsed = {}; }
3925
+ const hso = parsed.hookSpecificOutput || {};
3926
+ if (hso.permissionDecision === 'deny') {
3927
+ process.stdout.write(JSON.stringify({
3928
+ decision: 'block',
3929
+ reason: hso.permissionDecisionReason || 'Blocked by ThumbGate prevention rule.',
3930
+ }) + '\n');
3931
+ } else {
3932
+ // warn / no match → allow. The gate engine already logged the decision.
3933
+ process.stdout.write(JSON.stringify({}) + '\n');
3934
+ }
3935
+ process.exit(0);
3936
+ } catch (err) {
3937
+ // Hermes hooks fail OPEN on error/timeout — emit an explicit allow so a gate fault
3938
+ // never wedges the agent (reliability ≈ enforcement; keep this fast).
3939
+ process.stderr.write(`hermes-gate error: ${err.message}\n`);
3940
+ process.stdout.write(JSON.stringify({}) + '\n');
3941
+ process.exit(0);
3942
+ }
3943
+ });
3944
+ break;
3945
+ }
3722
3946
  case 'gate-stats':
3723
3947
  gateStats();
3724
3948
  break;