thumbgate 1.27.20 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +63 -42
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +25 -8
- package/bin/postinstall.js +1 -1
- package/config/entitlement-public-keys.json +6 -0
- package/config/gates/default.json +1 -1
- package/config/github-about.json +2 -2
- package/config/merge-quality-checks.json +3 -0
- package/package.json +12 -4
- package/public/about.html +1 -4
- package/public/agent-manager.html +6 -6
- package/public/agents-cost-savings.html +3 -3
- package/public/ai-malpractice-prevention.html +1 -1
- package/public/blog.html +12 -9
- package/public/chatgpt-app.html +3 -3
- package/public/codex-enterprise.html +3 -3
- package/public/codex-plugin.html +5 -5
- package/public/compare.html +10 -10
- package/public/dashboard.html +1 -1
- package/public/federal.html +2 -2
- package/public/guide.html +14 -14
- package/public/index.html +81 -70
- package/public/install.html +14 -14
- package/public/learn.html +4 -17
- package/public/numbers.html +2 -2
- package/public/pricing.html +12 -19
- package/public/pro.html +2 -2
- package/scripts/agent-reward-model.js +13 -0
- package/scripts/bayes-optimal-gate.js +6 -1
- package/scripts/cli-feedback.js +17 -1
- package/scripts/commercial-offer.js +3 -3
- package/scripts/entitlement.js +250 -0
- package/scripts/export-databricks-bundle.js +5 -0
- package/scripts/export-dpo-pairs.js +6 -0
- package/scripts/export-hf-dataset.js +5 -0
- package/scripts/gates-engine.js +15 -3
- package/scripts/imperative-detector.js +85 -0
- package/scripts/intervention-policy.js +13 -0
- package/scripts/pr-manager.js +9 -22
- package/scripts/pro-local-dashboard.js +198 -0
- package/scripts/risk-scorer.js +6 -0
- package/scripts/seo-gsd.js +2 -2
- package/scripts/thompson-sampling.js +11 -2
- package/src/api/server.js +81 -10
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* entitlement.js — signed license entitlements for ThumbGate's paid tier.
|
|
5
|
+
*
|
|
6
|
+
* Replaces the old bypassable `tg_`/`tg_pro_` prefix check in license.js with a
|
|
7
|
+
* cryptographically-verifiable, offline-checkable license token.
|
|
8
|
+
*
|
|
9
|
+
* Token format (compact JWS-like, Ed25519 / "EdDSA"):
|
|
10
|
+
* base64url(header) "." base64url(payload) "." base64url(signature)
|
|
11
|
+
* header = { alg: "EdDSA", kid } // kid selects the public key
|
|
12
|
+
* payload = { tier, features[], exp, iat, customerId, keyId }
|
|
13
|
+
* sig = Ed25519 over `${b64url(header)}.${b64url(payload)}`
|
|
14
|
+
*
|
|
15
|
+
* The PRIVATE signing key lives only in the hosted billing service (or a local
|
|
16
|
+
* gitignored dev path). Only PUBLIC keys ship, in config/entitlement-public-keys.json,
|
|
17
|
+
* so any client can verify a license offline without contacting a server.
|
|
18
|
+
*
|
|
19
|
+
* Enforcement is opt-in via THUMBGATE_ENFORCE_ENTITLEMENTS so paid features can be
|
|
20
|
+
* gated without breaking existing users during rollout:
|
|
21
|
+
* - advisory (default): requireEntitlement returns {entitled:false, reason} and
|
|
22
|
+
* the caller may warn but proceed.
|
|
23
|
+
* - enforced (flag set): requireEntitlement throws EntitlementError when not entitled.
|
|
24
|
+
*
|
|
25
|
+
* LIMITATION — read before overselling this. This gate runs CLIENT-SIDE in an
|
|
26
|
+
* MIT, open-source, un-compiled package. It makes a license UN-FORGEABLE (you
|
|
27
|
+
* cannot mint a valid token without the private key), but it does NOT make the
|
|
28
|
+
* check UN-BYPASSABLE: anyone with the source can monkey-patch requireEntitlement
|
|
29
|
+
* or delete the gate. So this protects against fake keys and honest free-riding,
|
|
30
|
+
* and it is the correct authorization primitive for the HOSTED service — but the
|
|
31
|
+
* only real, un-bypassable protection for the crown-jewel intelligence is to run
|
|
32
|
+
* it SERVER-SIDE (client sends inputs, gets outputs, never sees the code/weights),
|
|
33
|
+
* with these tokens as the auth. Client-side gating is a speed bump, not a wall.
|
|
34
|
+
* See docs/COMMERCIALIZATION_STRATEGY.md.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const crypto = require('node:crypto');
|
|
38
|
+
const fs = require('node:fs');
|
|
39
|
+
const os = require('node:os');
|
|
40
|
+
const path = require('node:path');
|
|
41
|
+
|
|
42
|
+
const TIER_FEATURES = {
|
|
43
|
+
free: [],
|
|
44
|
+
pro: ['recall', 'lesson-search', 'unlimited-rules', 'data-export', 'hosted-dashboard', 'hosted-sync', 'learned-models'],
|
|
45
|
+
team: ['recall', 'lesson-search', 'unlimited-rules', 'data-export', 'hosted-dashboard', 'hosted-sync', 'learned-models', 'org-visibility'],
|
|
46
|
+
enterprise: ['recall', 'lesson-search', 'unlimited-rules', 'data-export', 'hosted-dashboard', 'hosted-sync', 'learned-models', 'org-visibility', 'sso', 'audit-log', 'compliance-export'],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
class EntitlementError extends Error {
|
|
50
|
+
constructor(message, code = 'entitlement_denied') {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = 'EntitlementError';
|
|
53
|
+
this.code = code;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const advisoryWarnings = new Set();
|
|
58
|
+
|
|
59
|
+
function b64urlEncode(buf) {
|
|
60
|
+
return Buffer.from(buf).toString('base64url');
|
|
61
|
+
}
|
|
62
|
+
function b64urlDecodeJson(str) {
|
|
63
|
+
return JSON.parse(Buffer.from(str, 'base64url').toString('utf8'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isTrueEnv(value) {
|
|
67
|
+
if (!value) return false;
|
|
68
|
+
const v = String(value).trim().toLowerCase();
|
|
69
|
+
return v === '1' || v === 'true' || v === 'yes' || v === 'on';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isEnforced(env = process.env) {
|
|
73
|
+
return isTrueEnv(env.THUMBGATE_ENFORCE_ENTITLEMENTS);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Load the shipped public keyset ({ activeKid, keys: {kid: pem} }). */
|
|
77
|
+
function loadTrustedKeys(root = path.resolve(__dirname, '..')) {
|
|
78
|
+
try {
|
|
79
|
+
const p = path.join(root, 'config', 'entitlement-public-keys.json');
|
|
80
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
81
|
+
return raw.keys || {};
|
|
82
|
+
} catch {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Verify a license token offline. Returns a normalized result — never throws on
|
|
89
|
+
* bad input (returns { valid:false, reason }).
|
|
90
|
+
* @param {string} token
|
|
91
|
+
* @param {{ trustedKeys?: Record<string,string>, now?: number }} [opts]
|
|
92
|
+
*/
|
|
93
|
+
function verifyLicense(token, opts = {}) {
|
|
94
|
+
const trustedKeys = opts.trustedKeys || loadTrustedKeys();
|
|
95
|
+
const now = opts.now || Math.floor(nowMs() / 1000);
|
|
96
|
+
if (typeof token !== 'string' || !token.trim()) {
|
|
97
|
+
return { valid: false, reason: 'missing_token' };
|
|
98
|
+
}
|
|
99
|
+
// Reject the legacy bypassable prefix keys outright.
|
|
100
|
+
if (/^tg_/.test(token.trim())) {
|
|
101
|
+
return { valid: false, reason: 'legacy_prefix_key_not_a_signed_license' };
|
|
102
|
+
}
|
|
103
|
+
const parts = token.trim().split('.');
|
|
104
|
+
if (parts.length !== 3) return { valid: false, reason: 'malformed_token' };
|
|
105
|
+
const [h, p, s] = parts;
|
|
106
|
+
let header;
|
|
107
|
+
let payload;
|
|
108
|
+
try {
|
|
109
|
+
header = b64urlDecodeJson(h);
|
|
110
|
+
payload = b64urlDecodeJson(p);
|
|
111
|
+
} catch {
|
|
112
|
+
return { valid: false, reason: 'undecodable_token' };
|
|
113
|
+
}
|
|
114
|
+
if (header.alg !== 'EdDSA' || !header.kid) return { valid: false, reason: 'bad_header' };
|
|
115
|
+
const pubPem = trustedKeys[header.kid];
|
|
116
|
+
if (!pubPem) return { valid: false, reason: 'unknown_key_id' };
|
|
117
|
+
|
|
118
|
+
let signatureOk = false;
|
|
119
|
+
try {
|
|
120
|
+
signatureOk = crypto.verify(
|
|
121
|
+
null,
|
|
122
|
+
Buffer.from(`${h}.${p}`),
|
|
123
|
+
crypto.createPublicKey(pubPem),
|
|
124
|
+
Buffer.from(s, 'base64url')
|
|
125
|
+
);
|
|
126
|
+
} catch {
|
|
127
|
+
return { valid: false, reason: 'signature_verify_error' };
|
|
128
|
+
}
|
|
129
|
+
if (!signatureOk) return { valid: false, reason: 'bad_signature' };
|
|
130
|
+
|
|
131
|
+
if (typeof payload.exp === 'number' && payload.exp < now) {
|
|
132
|
+
return { valid: false, reason: 'expired', tier: payload.tier };
|
|
133
|
+
}
|
|
134
|
+
const tier = payload.tier || 'free';
|
|
135
|
+
const features = Array.isArray(payload.features) && payload.features.length
|
|
136
|
+
? payload.features
|
|
137
|
+
: (TIER_FEATURES[tier] || []);
|
|
138
|
+
return {
|
|
139
|
+
valid: true,
|
|
140
|
+
tier,
|
|
141
|
+
features,
|
|
142
|
+
customerId: payload.customerId || null,
|
|
143
|
+
keyId: header.kid,
|
|
144
|
+
exp: payload.exp || null,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Sign a license token. PRIVATE-key operation — runs in the hosted billing
|
|
150
|
+
* service (or a local dev/setup script), never in the shipped client at runtime.
|
|
151
|
+
* @param {string} privateKeyPem
|
|
152
|
+
* @param {{ tier:string, features?:string[], customerId?:string, kid:string, expSeconds?:number, iat?:number, exp?:number }} claims
|
|
153
|
+
*/
|
|
154
|
+
function issueLicense(privateKeyPem, claims) {
|
|
155
|
+
const header = { alg: 'EdDSA', kid: claims.kid };
|
|
156
|
+
const iat = claims.iat || Math.floor(nowMs() / 1000);
|
|
157
|
+
const exp = claims.exp || (claims.expSeconds ? iat + claims.expSeconds : undefined);
|
|
158
|
+
const payload = {
|
|
159
|
+
tier: claims.tier,
|
|
160
|
+
features: claims.features || TIER_FEATURES[claims.tier] || [],
|
|
161
|
+
customerId: claims.customerId || null,
|
|
162
|
+
keyId: claims.kid,
|
|
163
|
+
iat,
|
|
164
|
+
...(exp ? { exp } : {}),
|
|
165
|
+
};
|
|
166
|
+
const h = b64urlEncode(JSON.stringify(header));
|
|
167
|
+
const p = b64urlEncode(JSON.stringify(payload));
|
|
168
|
+
const sig = crypto.sign(null, Buffer.from(`${h}.${p}`), crypto.createPrivateKey(privateKeyPem));
|
|
169
|
+
return `${h}.${p}.${b64urlEncode(sig)}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Resolve the active license token from env or the local config file. */
|
|
173
|
+
function resolveLicenseToken(env = process.env) {
|
|
174
|
+
if (env.THUMBGATE_LICENSE && env.THUMBGATE_LICENSE.trim()) return env.THUMBGATE_LICENSE.trim();
|
|
175
|
+
try {
|
|
176
|
+
const p = path.join(os.homedir(), '.config', 'thumbgate', 'license.json');
|
|
177
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
178
|
+
if (raw.license || raw.token) return String(raw.license || raw.token).trim();
|
|
179
|
+
} catch { /* no local license */ }
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Gate a paid feature. Advisory by default; throws EntitlementError only when
|
|
185
|
+
* THUMBGATE_ENFORCE_ENTITLEMENTS is set. Returns { entitled, tier, reason }.
|
|
186
|
+
* @param {string} feature
|
|
187
|
+
* @param {{ env?: NodeJS.ProcessEnv, trustedKeys?: object, token?: string }} [opts]
|
|
188
|
+
*/
|
|
189
|
+
function requireEntitlement(feature, opts = {}) {
|
|
190
|
+
const env = opts.env || process.env;
|
|
191
|
+
const token = opts.token !== undefined ? opts.token : resolveLicenseToken(env);
|
|
192
|
+
const result = verifyLicense(token, { trustedKeys: opts.trustedKeys });
|
|
193
|
+
const entitled = result.valid && result.features.includes(feature);
|
|
194
|
+
const decision = {
|
|
195
|
+
entitled,
|
|
196
|
+
tier: result.valid ? result.tier : 'free',
|
|
197
|
+
feature,
|
|
198
|
+
reason: entitled ? 'entitled' : (result.valid ? 'feature_not_in_tier' : result.reason),
|
|
199
|
+
enforced: isEnforced(env),
|
|
200
|
+
};
|
|
201
|
+
if (!entitled && decision.enforced) {
|
|
202
|
+
throw new EntitlementError(
|
|
203
|
+
`ThumbGate: "${feature}" requires a paid license (current tier: ${decision.tier}, reason: ${decision.reason}). `
|
|
204
|
+
+ `Get a license at https://thumbgate.ai/pricing, then set THUMBGATE_LICENSE or ~/.config/thumbgate/license.json.`,
|
|
205
|
+
decision.reason
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return decision;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Shared paid-feature wrapper for commercial entrypoints. In advisory mode it
|
|
213
|
+
* emits one warning per feature/label and lets the caller continue; in enforced
|
|
214
|
+
* mode `requireEntitlement()` throws before this function returns.
|
|
215
|
+
*/
|
|
216
|
+
function requirePaidFeature(feature, opts = {}) {
|
|
217
|
+
const decision = requireEntitlement(feature, opts);
|
|
218
|
+
const label = opts.label || feature;
|
|
219
|
+
const warningKey = `${feature}:${label}`;
|
|
220
|
+
if (!decision.entitled && !opts.silent && !advisoryWarnings.has(warningKey)) {
|
|
221
|
+
advisoryWarnings.add(warningKey);
|
|
222
|
+
console.error(`⚠️ ThumbGate: ${label} requires a paid license (tier: ${decision.tier}). Advisory mode — set THUMBGATE_ENFORCE_ENTITLEMENTS=1 to enforce. License: https://thumbgate.ai/pricing`);
|
|
223
|
+
}
|
|
224
|
+
return decision;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function requireLearnedModelsEntitlement(opts = {}) {
|
|
228
|
+
return requirePaidFeature('learned-models', {
|
|
229
|
+
...opts,
|
|
230
|
+
label: opts.label || 'learned-models intelligence',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Injectable clock (Date.now is fine at runtime; kept in one place for testability).
|
|
235
|
+
function nowMs() {
|
|
236
|
+
return Date.now();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = {
|
|
240
|
+
verifyLicense,
|
|
241
|
+
issueLicense,
|
|
242
|
+
requireEntitlement,
|
|
243
|
+
requirePaidFeature,
|
|
244
|
+
requireLearnedModelsEntitlement,
|
|
245
|
+
resolveLicenseToken,
|
|
246
|
+
loadTrustedKeys,
|
|
247
|
+
isEnforced,
|
|
248
|
+
TIER_FEATURES,
|
|
249
|
+
EntitlementError,
|
|
250
|
+
};
|
|
@@ -129,6 +129,11 @@ function timestampSlug() {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
function exportDatabricksBundle(feedbackDir = getDefaultFeedbackDir(), outputPath, options = {}) {
|
|
132
|
+
// Paid-feature gate (advisory by default; enforced via THUMBGATE_ENFORCE_ENTITLEMENTS=1).
|
|
133
|
+
const _ent = require('./entitlement').requireEntitlement('data-export');
|
|
134
|
+
if (!_ent.entitled) {
|
|
135
|
+
console.error(`⚠️ ThumbGate: Databricks bundle export is a paid feature (tier: ${_ent.tier}). Advisory mode — set THUMBGATE_ENFORCE_ENTITLEMENTS=1 to enforce. License: https://thumbgate.ai/pricing`);
|
|
136
|
+
}
|
|
132
137
|
const resolvedFeedbackDir = path.resolve(feedbackDir || getDefaultFeedbackDir());
|
|
133
138
|
const resolvedProofDir = path.resolve(options.proofDir || DEFAULT_PROOF_DIR);
|
|
134
139
|
const exportedAt = new Date().toISOString();
|
|
@@ -180,6 +180,12 @@ function toJSONL(pairs) {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
function exportDpoFromMemories(memories) {
|
|
183
|
+
// Paid-feature gate: DPO export is a commercial (Pro+) capability. Advisory by
|
|
184
|
+
// default; enforced when THUMBGATE_ENFORCE_ENTITLEMENTS=1 (then this throws).
|
|
185
|
+
const _ent = require('./entitlement').requireEntitlement('data-export');
|
|
186
|
+
if (!_ent.entitled) {
|
|
187
|
+
console.error(`⚠️ ThumbGate: DPO export is a paid feature (tier: ${_ent.tier}). Advisory mode — set THUMBGATE_ENFORCE_ENTITLEMENTS=1 to enforce. License: https://thumbgate.ai/pricing`);
|
|
188
|
+
}
|
|
183
189
|
const errors = memories.filter((m) => m.category === 'error');
|
|
184
190
|
const learnings = memories.filter((m) => m.category === 'learning');
|
|
185
191
|
const result = buildDpoPairs(errors, learnings);
|
|
@@ -177,6 +177,11 @@ function buildDatasetInfo({ traceCount, preferenceCount, exportedAt }) {
|
|
|
177
177
|
* @returns {Object} Export summary
|
|
178
178
|
*/
|
|
179
179
|
function exportHfDataset(options = {}) {
|
|
180
|
+
// Paid-feature gate (advisory by default; enforced via THUMBGATE_ENFORCE_ENTITLEMENTS=1).
|
|
181
|
+
const _ent = require('./entitlement').requireEntitlement('data-export');
|
|
182
|
+
if (!_ent.entitled) {
|
|
183
|
+
console.error(`⚠️ ThumbGate: HuggingFace dataset export is a paid feature (tier: ${_ent.tier}). Advisory mode — set THUMBGATE_ENFORCE_ENTITLEMENTS=1 to enforce. License: https://thumbgate.ai/pricing`);
|
|
184
|
+
}
|
|
180
185
|
const feedbackDir = options.feedbackDir || resolveFeedbackDir();
|
|
181
186
|
const outputDir = options.outputDir || path.join(feedbackDir, 'hf-dataset');
|
|
182
187
|
const includeProvenance = options.includeProvenance !== false;
|
package/scripts/gates-engine.js
CHANGED
|
@@ -151,10 +151,22 @@ const DESTRUCTIVE_OR_PRIVILEGE_BOUNDARY_PATTERN = /\b(?:rm\s+-rf|chmod\s+(?:\+x|
|
|
|
151
151
|
// far better than any single regex). Secret exfiltration and the security-vulnerability
|
|
152
152
|
// scan hard-deny on their OWN paths before this runs, so irreversible data-leak / supply
|
|
153
153
|
// chain risks stay blocked regardless of posture.
|
|
154
|
+
// Self-protective gates guard ThumbGate's own guardrails (kill/env/config/hooks). See the
|
|
155
|
+
// self-protect-binds-regardless-of-posture changeset for the full rationale.
|
|
156
|
+
function isSelfProtectGate(gateId) {
|
|
157
|
+
return typeof gateId === 'string' && gateId.startsWith('self-protect');
|
|
158
|
+
}
|
|
159
|
+
|
|
154
160
|
function applyEnforcementPosture(result) {
|
|
155
161
|
if (!result || (result.decision !== 'deny' && result.decision !== 'approve')) return result;
|
|
156
162
|
// Full hard enforcement opt-in: keep every deny.
|
|
157
163
|
if (process.env.THUMBGATE_STRICT_ENFORCEMENT === '1') return result;
|
|
164
|
+
// Self-protection binds regardless of posture: a warning is worthless once the guardrail is
|
|
165
|
+
// gone. Owner escape: THUMBGATE_SELF_PROTECT_OVERRIDE=1 (break-glass covers .claude/settings*
|
|
166
|
+
// but not this surface, so it needs its own escape — self-lockout, 2026-07-07).
|
|
167
|
+
if (isSelfProtectGate(result.gate) && process.env.THUMBGATE_SELF_PROTECT_OVERRIDE !== '1') {
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
158
170
|
// Honor the explicit strict-knowledge-conflict opt-in for that gate.
|
|
159
171
|
if (process.env.THUMBGATE_STRICT_KNOWLEDGE_CONFLICT === '1' && result.gate === 'knowledge-conflict-gate') return result;
|
|
160
172
|
// Warn-by-default: the gate still fired and is recorded; the action is allowed through
|
|
@@ -2672,12 +2684,12 @@ function buildBlockActionProCta() {
|
|
|
2672
2684
|
if (totalBlocks < 5) return null; // Too early — let them experience the product
|
|
2673
2685
|
|
|
2674
2686
|
if (totalBlocks < 25) {
|
|
2675
|
-
return '\n\n💡 Pro: keep this rule
|
|
2687
|
+
return '\n\n💡 Pro: keep this rule searchable, exportable, and visible → thumbgate.ai/go/pro';
|
|
2676
2688
|
}
|
|
2677
2689
|
if (totalBlocks < 100) {
|
|
2678
|
-
return `\n\n💡 ${totalBlocks} actions blocked. Pro
|
|
2690
|
+
return `\n\n💡 ${totalBlocks} actions blocked. Pro adds recall, dashboard proof, and exports → thumbgate.ai/go/pro ($19/mo)`;
|
|
2679
2691
|
}
|
|
2680
|
-
return `\n\n💡 ${totalBlocks} mistakes caught. Your team could use shared hosted enforcement → thumbgate.ai/go/pro`;
|
|
2692
|
+
return `\n\n💡 ${totalBlocks} mistakes caught. Your team could use Enterprise shared hosted enforcement → thumbgate.ai/go/pro`;
|
|
2681
2693
|
} catch (_) {
|
|
2682
2694
|
return null;
|
|
2683
2695
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* imperative-detector.js — detect an explicit NEVER / ALWAYS directive in a
|
|
5
|
+
* feedback message so the CLI can OFFER immediate enforcement (force-gate) when
|
|
6
|
+
* the user clearly asked for it.
|
|
7
|
+
*
|
|
8
|
+
* Why this exists: auto-promotion is deliberately occurrence-gated (a rule
|
|
9
|
+
* blocks only after a pattern repeats) — that accumulate-evidence-before-you-
|
|
10
|
+
* enforce default is what makes ThumbGate trustworthy rather than a one-signal
|
|
11
|
+
* lockout. But when a user types "never do X again", that is an explicit intent
|
|
12
|
+
* to guard NOW. This module recognizes that intent and lets the CLI SURFACE the
|
|
13
|
+
* one-shot path (force-gate) as an offer. It never blocks on its own — the user
|
|
14
|
+
* still confirms. Detecting intent ≠ acting on it.
|
|
15
|
+
*
|
|
16
|
+
* Pure and deterministic: no state, no I/O.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// A leading (or clause-initial) negative imperative → intent to forbid.
|
|
20
|
+
const NEVER_RE = /(?:^|[.;:,]\s*)(never|do\s*not|don'?t|stop\s+\w|must\s*not|no\s+longer|quit\s+\w|avoid\s+\w)/i;
|
|
21
|
+
// A leading (or clause-initial) positive imperative → intent to always do.
|
|
22
|
+
const ALWAYS_RE = /(?:^|[.;:,]\s*)(always|make\s+sure|ensure\s+\w|be\s+sure\s+to)/i;
|
|
23
|
+
|
|
24
|
+
function firstMeaningfulText(text) {
|
|
25
|
+
// Strip common feedback prefixes/quote marks so "❯ never …" still matches.
|
|
26
|
+
return String(text || '').trim().replace(/^[\s>❯"'`*-]+/, '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} text
|
|
31
|
+
* @returns {{isImperative:boolean, polarity:('never'|'always'|null), directive:string}}
|
|
32
|
+
*/
|
|
33
|
+
function detectImperative(text) {
|
|
34
|
+
const t = firstMeaningfulText(text);
|
|
35
|
+
if (!t) return { isImperative: false, polarity: null, directive: '' };
|
|
36
|
+
// Negative wins if both appear — a forbid directive is the actionable one.
|
|
37
|
+
if (NEVER_RE.test(t)) return { isImperative: true, polarity: 'never', directive: t };
|
|
38
|
+
if (ALWAYS_RE.test(t)) return { isImperative: true, polarity: 'always', directive: t };
|
|
39
|
+
return { isImperative: false, polarity: null, directive: '' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function shellQuote(text) {
|
|
43
|
+
return String(text).trim().slice(0, 140).replace(/"/g, "'").replace(/\s+/g, ' ');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build a suggestion for the capture confirmation. Returns null when there's no
|
|
48
|
+
* explicit directive to act on.
|
|
49
|
+
* - down + "never …" → OFFER immediate force-gate (the user asked to forbid it).
|
|
50
|
+
* - up + "always …" → clarify it's stored as guidance, not a hard block.
|
|
51
|
+
* @param {{signal?:string, text?:string}} opts
|
|
52
|
+
* @returns {{kind:string, message:string}|null}
|
|
53
|
+
*/
|
|
54
|
+
function suggestForceGate({ signal, text } = {}) {
|
|
55
|
+
const isDown = signal === 'down' || signal === 'negative' || signal === 'thumbs_down';
|
|
56
|
+
const det = detectImperative(text);
|
|
57
|
+
if (!det.isImperative) return null;
|
|
58
|
+
|
|
59
|
+
if (isDown && det.polarity === 'never') {
|
|
60
|
+
const ctx = shellQuote(text);
|
|
61
|
+
return {
|
|
62
|
+
kind: 'force-gate-offer',
|
|
63
|
+
message: 'You said "never" — block this immediately instead of waiting for it to recur:\n'
|
|
64
|
+
+ ` npx thumbgate force-gate --context="${ctx}" --action=block\n`
|
|
65
|
+
+ ' (or run /thumbgate-guard). Left alone, it auto-promotes to a gate after the pattern repeats.',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!isDown && det.polarity === 'always') {
|
|
70
|
+
return {
|
|
71
|
+
kind: 'always-note',
|
|
72
|
+
message: 'You said "always" — stored as an ALWAYS guidance principle. '
|
|
73
|
+
+ 'It is surfaced as context on future actions (guidance, not a hard block; positive patterns are not gate-enforced).',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
detectImperative,
|
|
82
|
+
suggestForceGate,
|
|
83
|
+
NEVER_RE,
|
|
84
|
+
ALWAYS_RE,
|
|
85
|
+
};
|
|
@@ -5,6 +5,7 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
7
7
|
const { getDecisionLogPath, readDecisionLog, collapseDecisionTimeline } = require('./decision-journal');
|
|
8
|
+
const { requireLearnedModelsEntitlement } = require('./entitlement');
|
|
8
9
|
|
|
9
10
|
const LABELS = ['allow', 'recall', 'verify', 'warn', 'deny'];
|
|
10
11
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
@@ -551,6 +552,10 @@ function summarizeTopTokens(model, limit = 5) {
|
|
|
551
552
|
}
|
|
552
553
|
|
|
553
554
|
function trainInterventionPolicy(examples, options = {}) {
|
|
555
|
+
requireLearnedModelsEntitlement({
|
|
556
|
+
...(options.entitlement || {}),
|
|
557
|
+
label: 'intervention-policy training',
|
|
558
|
+
});
|
|
554
559
|
const split = splitExamples(examples);
|
|
555
560
|
const evaluationModel = fitNaiveBayes(split.train);
|
|
556
561
|
const deployedModel = fitNaiveBayes(examples);
|
|
@@ -631,6 +636,10 @@ function buildRuntimeCandidate(params = {}) {
|
|
|
631
636
|
}
|
|
632
637
|
|
|
633
638
|
function getInterventionRecommendation(params = {}, options = {}) {
|
|
639
|
+
requireLearnedModelsEntitlement({
|
|
640
|
+
...(options.entitlement || {}),
|
|
641
|
+
label: 'intervention-policy recommendation',
|
|
642
|
+
});
|
|
634
643
|
const resolvedDir = resolveFeedbackDir({ feedbackDir: options.feedbackDir || params.feedbackDir });
|
|
635
644
|
let model = options.model || loadInterventionPolicy(resolvedDir);
|
|
636
645
|
const candidate = options.candidate || buildRuntimeCandidate(params);
|
|
@@ -693,6 +702,10 @@ function computeDailySeries(examples, dayCount = 14) {
|
|
|
693
702
|
}
|
|
694
703
|
|
|
695
704
|
function getInterventionPolicySummary(feedbackDir, options = {}) {
|
|
705
|
+
requireLearnedModelsEntitlement({
|
|
706
|
+
...(options.entitlement || {}),
|
|
707
|
+
label: 'intervention-policy summary',
|
|
708
|
+
});
|
|
696
709
|
const resolvedDir = resolveFeedbackDir({ feedbackDir });
|
|
697
710
|
const { examples, sourceCounts } = buildExamplesFromFeedbackDir(resolvedDir);
|
|
698
711
|
const model = loadInterventionPolicy(resolvedDir) || trainInterventionPolicy(examples);
|
package/scripts/pr-manager.js
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* PR Manager — High-Throughput Merge & Blocker Diagnosis
|
|
4
|
-
*
|
|
5
|
-
* Inspired by the 2026 GitHub 'Quick Access' update. Centralizes merge status
|
|
6
|
-
* detection and triggers autonomous self-healing for common blockers.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
2
|
'use strict';
|
|
10
3
|
|
|
11
4
|
const fs = require('node:fs');
|
|
@@ -34,6 +27,8 @@ const PASSING_BUCKETS = new Set((MERGE_QUALITY_CHECKS.passingBuckets || []).map(
|
|
|
34
27
|
const PENDING_BUCKETS = new Set((MERGE_QUALITY_CHECKS.pendingBuckets || []).map((value) => String(value || '').toLowerCase()));
|
|
35
28
|
const FAILING_BUCKETS = new Set((MERGE_QUALITY_CHECKS.failingBuckets || []).map((value) => String(value || '').toLowerCase()));
|
|
36
29
|
|
|
30
|
+
const SELF_REFERENTIAL_CHECKS = new Set(MERGE_QUALITY_CHECKS.selfReferentialChecks || []);
|
|
31
|
+
|
|
37
32
|
function assertSafeGhArgs(args) {
|
|
38
33
|
if (!Array.isArray(args) || args.length === 0) {
|
|
39
34
|
throw new Error('GH CLI args must be a non-empty array.');
|
|
@@ -119,9 +114,6 @@ function isMissingCurrentBranchPr(result, prNumber) {
|
|
|
119
114
|
|| /could not determine current branch:.*not on any branch/i.test(formatGhError(result));
|
|
120
115
|
}
|
|
121
116
|
|
|
122
|
-
/**
|
|
123
|
-
* Fetch granular PR status using GH CLI
|
|
124
|
-
*/
|
|
125
117
|
function getPrStatus(prNumber = '', runner = runGh) {
|
|
126
118
|
const normalizedPrNumber = normalizePrNumber(prNumber);
|
|
127
119
|
const args = ['pr', 'view'];
|
|
@@ -182,6 +174,9 @@ function summarizeChecks(checks = []) {
|
|
|
182
174
|
|
|
183
175
|
for (const check of checks) {
|
|
184
176
|
const name = check.name || 'unknown-check';
|
|
177
|
+
|
|
178
|
+
if (SELF_REFERENTIAL_CHECKS.has(name)) continue;
|
|
179
|
+
|
|
185
180
|
const bucket = String(check.bucket || '').toLowerCase();
|
|
186
181
|
if (bucket) {
|
|
187
182
|
if (FAILING_BUCKETS.has(bucket)) {
|
|
@@ -225,9 +220,6 @@ function sleep(ms) {
|
|
|
225
220
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
226
221
|
}
|
|
227
222
|
|
|
228
|
-
/**
|
|
229
|
-
* Diagnose and resolve blockers autonomously
|
|
230
|
-
*/
|
|
231
223
|
async function resolveBlockers(pr, runner = runGh) {
|
|
232
224
|
const title = pr.title || 'Untitled PR';
|
|
233
225
|
const mergeState = pr.mergeStateStatus || 'UNKNOWN';
|
|
@@ -241,7 +233,6 @@ async function resolveBlockers(pr, runner = runGh) {
|
|
|
241
233
|
return { status: 'skipped', reason: 'draft' };
|
|
242
234
|
}
|
|
243
235
|
|
|
244
|
-
// 1. Handle Outdated Branch (BEHIND)
|
|
245
236
|
if (pr.mergeStateStatus === 'BEHIND') {
|
|
246
237
|
console.log('[PR Manager] PR is behind main. Triggering auto-update...');
|
|
247
238
|
const update = runner(['pr', 'update-branch', pr.number.toString()]);
|
|
@@ -250,13 +241,11 @@ async function resolveBlockers(pr, runner = runGh) {
|
|
|
250
241
|
}
|
|
251
242
|
}
|
|
252
243
|
|
|
253
|
-
// 2. Handle Merge Conflicts (DIRTY)
|
|
254
244
|
if (pr.mergeStateStatus === 'DIRTY' || pr.mergeable === 'CONFLICTING') {
|
|
255
245
|
console.log('[PR Manager] CRITICAL: Merge conflicts detected. Manual intervention or advanced rebase required.');
|
|
256
246
|
return { status: 'blocked', reason: 'conflicts' };
|
|
257
247
|
}
|
|
258
248
|
|
|
259
|
-
// 3. Handle CI Failures
|
|
260
249
|
let checks = pr.statusCheckRollup || [];
|
|
261
250
|
let checkSource = 'statusCheckRollup';
|
|
262
251
|
|
|
@@ -282,7 +271,6 @@ async function resolveBlockers(pr, runner = runGh) {
|
|
|
282
271
|
return { status: 'blocked', reason: 'ci_pending', checks: checkSummary.pending, checkSource };
|
|
283
272
|
}
|
|
284
273
|
|
|
285
|
-
// 4. Handle Review Blockers
|
|
286
274
|
if (pr.reviewDecision === 'CHANGES_REQUESTED') {
|
|
287
275
|
console.log('[PR Manager] BLOCKED: Changes requested by reviewer.');
|
|
288
276
|
return { status: 'blocked', reason: 'changes_requested' };
|
|
@@ -293,8 +281,10 @@ async function resolveBlockers(pr, runner = runGh) {
|
|
|
293
281
|
return { status: 'blocked', reason: 'review_required' };
|
|
294
282
|
}
|
|
295
283
|
|
|
296
|
-
|
|
297
|
-
|
|
284
|
+
if (
|
|
285
|
+
(pr.mergeStateStatus === 'CLEAN' || pr.mergeStateStatus === 'UNSTABLE')
|
|
286
|
+
&& pr.mergeable === 'MERGEABLE'
|
|
287
|
+
) {
|
|
298
288
|
console.log('[PR Manager] SUCCESS: PR is ready for protected autonomous merge.');
|
|
299
289
|
return { status: 'ready' };
|
|
300
290
|
}
|
|
@@ -368,9 +358,6 @@ function submitTrunkMergeRequest(prNumber, runner = runGh) {
|
|
|
368
358
|
};
|
|
369
359
|
}
|
|
370
360
|
|
|
371
|
-
/**
|
|
372
|
-
* Perform autonomous merge
|
|
373
|
-
*/
|
|
374
361
|
function performMerge(prInput, runner = runGh, options = {}) {
|
|
375
362
|
const pr = (prInput && typeof prInput === 'object')
|
|
376
363
|
? prInput
|