thumbgate 1.37.1 → 1.37.3
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/.agents/skills/article-syndication-pipeline/SKILL.md +37 -0
- package/.agents/skills/ci-gha-buildkite-patterns-not-clone/SKILL.md +72 -0
- package/.agents/skills/cobble-hot-store-compare-not-clone/SKILL.md +80 -0
- package/.agents/skills/colab-compute-honesty-not-clone/SKILL.md +70 -0
- package/.agents/skills/comet-session-logger/SKILL.md +59 -0
- package/.agents/skills/datadog-llm-obs-compare-not-clone/SKILL.md +75 -0
- package/.agents/skills/deeppattern-discipline-honesty-not-clone/SKILL.md +73 -0
- package/.agents/skills/thumbgate-board-loop/SKILL.md +72 -0
- package/.agents/skills/thumbgate-daily-discoveries/SKILL.md +57 -0
- package/.agents/skills/token-shunt-honesty-not-clone/SKILL.md +67 -0
- package/.agents/skills/typesafe-typed-questions-not-clone/SKILL.md +86 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/future-agi/.mcp.json +1 -1
- package/adapters/future-agi/config.toml +1 -1
- package/adapters/future-agi/opencode.json +1 -1
- package/adapters/herdr/herdr-plugin.toml +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +257 -0
- package/config/gate-templates.json +96 -0
- package/package.json +55 -12
- package/public/blog/2026-09-23-datadog-llm-obs-four-practices.html +42 -0
- package/public/index.html +2 -2
- package/public/install.html +7 -7
- package/public/numbers.html +2 -2
- package/scripts/ci-gha-buildkite-patterns.js +393 -0
- package/scripts/cli-schema.js +90 -0
- package/scripts/cobble-hot-store-split.js +600 -0
- package/scripts/colab-compute-honesty.js +281 -0
- package/scripts/deeppattern-discipline-honesty.js +431 -0
- package/scripts/gates-engine.js +16 -5
- package/scripts/install-thumbgate-daily-publish-launchd.sh +83 -0
- package/scripts/llm-obs-honesty.js +478 -0
- package/scripts/thumbgate-board-loop.js +432 -0
- package/scripts/thumbgate-daily-discoveries-publish.js +413 -0
- package/scripts/thumbgate-daily-discoveries-publish.sh +18 -0
- package/scripts/token-shunt-honesty.js +502 -0
- package/scripts/typesafe-typed-questions.js +1138 -0
- package/server.json +2 -2
- package/skills/article-syndication-pipeline/SKILL.md +37 -0
- package/skills/comet-session-logger/SKILL.md +59 -0
- package/skills/thumbgate-daily-discoveries/SKILL.md +57 -0
- package/src/index.js +10 -0
- package/src/integrations/ideabrowser-connector.js +190 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Spotify Portal / shunt FORMAT steal — not a product clone.
|
|
6
|
+
*
|
|
7
|
+
* Sources:
|
|
8
|
+
* https://github.com/spotify/portal-ai-plugins
|
|
9
|
+
* GitHub App install 162279530 (Igor Spotify Portal) — catalog connector, not cash
|
|
10
|
+
*
|
|
11
|
+
* Transfers:
|
|
12
|
+
* 1. Untargeted Read above 350 lines is blocked (SHUNT_MIN_LINES)
|
|
13
|
+
* 2. Bare cat/head/tail/less/more of a large file is blocked; pipes to grep/rg pass
|
|
14
|
+
* 3. Return a slice to the frontier model; do not dump the whole file back
|
|
15
|
+
* 4. Do not delegate architecture/debug to Portal/AiKA/Gemini Flash
|
|
16
|
+
*
|
|
17
|
+
* Does NOT install shunt@portal, buy Spotify Portal, or sell ThumbGate through it.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
|
|
23
|
+
const SOURCE_URLS = Object.freeze([
|
|
24
|
+
'https://github.com/spotify/portal-ai-plugins',
|
|
25
|
+
'https://github.com/settings/installations/162279530',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const DEFAULT_MIN_LINES = 350;
|
|
29
|
+
const DEFAULT_SLICE_LINES = 80;
|
|
30
|
+
const REASONING_KINDS = Object.freeze(['reasoning', 'debug', 'architecture', 'safety']);
|
|
31
|
+
const DELEGABLE_KINDS = Object.freeze(['boilerplate', 'config', 'test_scaffold', 'large_read']);
|
|
32
|
+
const METERED_WORKERS = Object.freeze([
|
|
33
|
+
'gemini-2.5-flash', 'gemini_2_5_flash', 'gemini_flash', 'portal', 'aika',
|
|
34
|
+
]);
|
|
35
|
+
const FRONTIER_WORKERS = Object.freeze([
|
|
36
|
+
'hermes_main', 'hermes-main', 'grok', 'grok_4', 'grok_4_6', 'frontier',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const CLONE_PATTERNS = Object.freeze([
|
|
40
|
+
{ id: 'portal_plugin', re: /\b(install|vendor|clone)\b.{0,40}\b(shunt@portal|@spotify\/portal-cli|portal-ai-plugins)\b/i },
|
|
41
|
+
{ id: 'portal_sku', re: /\b(buy|subscribe|checkout)\b.{0,40}\bspotify portal\b/i },
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const PIPE_TARGET_RE = /\|\s*(grep|rg|awk|sed)\b/i;
|
|
45
|
+
const HEAD_LIMIT_RE = /\b(?:head|tail)\b.*(?:-n|--lines)\b/i;
|
|
46
|
+
const BARE_READER_RE = /^(?:cat|head|tail|less|more)\b/i;
|
|
47
|
+
|
|
48
|
+
function normalizeBoolean(value) {
|
|
49
|
+
if (value === true || value === 1) return true;
|
|
50
|
+
if (value === false || value === 0 || value == null) return false;
|
|
51
|
+
return /^(1|true|yes|on)$/i.test(String(value).trim());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function norm(value) {
|
|
55
|
+
return String(value || '').trim().toLowerCase().replace(/[.-]/g, '_');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function detectCloneAttempt(text) {
|
|
59
|
+
return CLONE_PATTERNS.filter((p) => p.re.test(String(text || ''))).map((p) => p.id);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function evaluateReadIntercept({ lineCount, targeted, threshold = DEFAULT_MIN_LINES } = {}) {
|
|
63
|
+
const lines = Number(lineCount) || 0;
|
|
64
|
+
const limit = Number(threshold) || DEFAULT_MIN_LINES;
|
|
65
|
+
if (lines > limit && !targeted) {
|
|
66
|
+
return {
|
|
67
|
+
action: 'block_full_read',
|
|
68
|
+
ok: false,
|
|
69
|
+
id: 'untargeted_bulk_read',
|
|
70
|
+
reason: `Untargeted read of ${lines} lines > ${limit}. Slice with offset/limit (default ${DEFAULT_SLICE_LINES}) instead of dumping the file into the frontier model.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
action: 'allow_read',
|
|
75
|
+
ok: true,
|
|
76
|
+
id: 'allow_read',
|
|
77
|
+
reason: 'Small file or targeted offset/limit.',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function countFileLines(filePath) {
|
|
82
|
+
if (!filePath) return null;
|
|
83
|
+
try {
|
|
84
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
85
|
+
if (!text) return 0;
|
|
86
|
+
return text.split(/\r?\n/).length;
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function stripWrappers(command) {
|
|
93
|
+
let cmd = String(command || '').trim();
|
|
94
|
+
cmd = cmd.replace(/^(?:env\s+)+/i, '');
|
|
95
|
+
cmd = cmd.replace(/^(?:\/(?:usr\/)?bin\/)/, '');
|
|
96
|
+
return cmd.trim();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function splitCompound(command) {
|
|
100
|
+
return String(command).split(/\s*(?:\|\||&&|;)\s*/).map((s) => s.trim()).filter(Boolean);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseHeadTailLimit(stage) {
|
|
104
|
+
const m = String(stage).match(/\b(?:head|tail)\b(?:\s+-n\s*|\s+--lines(?:=|\s+))(\d+)/i);
|
|
105
|
+
return m ? Number(m[1]) : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function extractReaderPath(stage) {
|
|
109
|
+
const tokens = String(stage).trim().split(/\s+/).filter((t) => !t.startsWith('-'));
|
|
110
|
+
if (tokens.length < 2) return null;
|
|
111
|
+
return tokens[tokens.length - 1];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function evaluateBashRead({ command, lineCount, cwd = process.cwd(), threshold = DEFAULT_MIN_LINES } = {}) {
|
|
115
|
+
const raw = String(command || '').trim();
|
|
116
|
+
const limit = Number(threshold) || DEFAULT_MIN_LINES;
|
|
117
|
+
if (!raw) {
|
|
118
|
+
return { action: 'skip_bash', ok: true, id: 'skip_bash', reason: 'No bash command.' };
|
|
119
|
+
}
|
|
120
|
+
const compounds = splitCompound(raw);
|
|
121
|
+
for (const part of compounds) {
|
|
122
|
+
const stages = part.split('|').map((s) => stripWrappers(s)).filter(Boolean);
|
|
123
|
+
const last = stages[stages.length - 1] || '';
|
|
124
|
+
const first = stages[0] || '';
|
|
125
|
+
const headCount = parseHeadTailLimit(first) ?? parseHeadTailLimit(last);
|
|
126
|
+
if (headCount != null) {
|
|
127
|
+
if (headCount > limit) {
|
|
128
|
+
return {
|
|
129
|
+
action: 'block_bare_reader',
|
|
130
|
+
ok: false,
|
|
131
|
+
id: 'bare_bulk_cat',
|
|
132
|
+
reason: `head/tail -n ${headCount} exceeds ${limit}-line slice. Cap --lines at the threshold.`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const lastIsBoundedFilter = /^(?:grep|rg)\b/i.test(last) && stages.length > 1;
|
|
138
|
+
if (lastIsBoundedFilter) continue;
|
|
139
|
+
const reader = first.match(/^(?:cat|head|tail|less|more)\b/i);
|
|
140
|
+
if (reader) {
|
|
141
|
+
let lines = Number.isFinite(Number(lineCount)) ? Number(lineCount) : null;
|
|
142
|
+
if (lines == null) {
|
|
143
|
+
const fileArg = extractReaderPath(first);
|
|
144
|
+
lines = countFileLines(fileArg ? path.resolve(cwd, fileArg) : null);
|
|
145
|
+
}
|
|
146
|
+
if (lines == null) {
|
|
147
|
+
return {
|
|
148
|
+
action: 'warn_unknown_size',
|
|
149
|
+
ok: false,
|
|
150
|
+
id: 'unknown_size',
|
|
151
|
+
reason: 'Bare reader without --lines and without a readable file. Do not invent a size; pass --lines or a path.',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (lines > limit) {
|
|
155
|
+
return {
|
|
156
|
+
action: 'block_bare_reader',
|
|
157
|
+
ok: false,
|
|
158
|
+
id: 'bare_bulk_cat',
|
|
159
|
+
reason: `Bare ${reader[0]} of ${lines} lines bypasses Read intercept. Pipe to grep/rg or use head -n <= ${limit}.`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { action: 'allow_bash', ok: true, id: 'allow_bash', reason: 'Not a bulk file dump.' };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function evaluateTaskRoute({ taskKind, model } = {}) {
|
|
168
|
+
const kind = norm(taskKind);
|
|
169
|
+
const worker = norm(model);
|
|
170
|
+
if (!kind && !worker) {
|
|
171
|
+
return { action: 'skip_route', ok: true, id: 'skip_route', reason: 'No task route to audit.' };
|
|
172
|
+
}
|
|
173
|
+
const metered = METERED_WORKERS.includes(worker) || worker.includes('flash');
|
|
174
|
+
if (REASONING_KINDS.includes(kind)) {
|
|
175
|
+
if (metered || worker.includes('local_slice') || worker.startsWith('portal') || worker.startsWith('aika')) {
|
|
176
|
+
return {
|
|
177
|
+
action: 'block_delegate_reason',
|
|
178
|
+
ok: false,
|
|
179
|
+
id: 'cheap_worker_reasoning',
|
|
180
|
+
reason: 'Do not delegate debug/architecture to Portal, AiKA, or Gemini Flash.',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (!FRONTIER_WORKERS.includes(worker)) {
|
|
184
|
+
return {
|
|
185
|
+
action: 'block_metered',
|
|
186
|
+
ok: false,
|
|
187
|
+
id: 'unknown_route',
|
|
188
|
+
reason: 'Reasoning needs an explicit frontier worker (hermes-main/grok). Missing or unrecognized models are denied.',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
action: 'allow_frontier',
|
|
193
|
+
ok: true,
|
|
194
|
+
id: 'allow_frontier',
|
|
195
|
+
reason: 'Reasoning stays on the capable route.',
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (DELEGABLE_KINDS.includes(kind)) {
|
|
199
|
+
if (metered) {
|
|
200
|
+
return {
|
|
201
|
+
action: 'block_metered',
|
|
202
|
+
ok: false,
|
|
203
|
+
id: 'metered_worker',
|
|
204
|
+
reason: 'Portal and Gemini Flash are outside the monthly cap. Use local_slice.',
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (worker === 'local_slice') {
|
|
208
|
+
return {
|
|
209
|
+
action: 'allow_delegate',
|
|
210
|
+
ok: true,
|
|
211
|
+
id: 'allow_delegate',
|
|
212
|
+
reason: 'Boilerplate and large reads use a local slice, not a frontier dump.',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (kind || worker) {
|
|
217
|
+
return {
|
|
218
|
+
action: 'block_metered',
|
|
219
|
+
ok: false,
|
|
220
|
+
id: 'unknown_route',
|
|
221
|
+
reason: 'Unknown task/worker is denied. Name boilerplate|config|test_scaffold|large_read + local_slice, or keep reasoning on frontier.',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return { action: 'skip_route', ok: true, id: 'skip_route', reason: 'No task route to audit.' };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function evaluateContextReturn({ sourceLines, returnedLines, threshold = DEFAULT_MIN_LINES } = {}) {
|
|
228
|
+
const source = Number(sourceLines) || 0;
|
|
229
|
+
const returned = Number(returnedLines) || 0;
|
|
230
|
+
const limit = Number(threshold) || DEFAULT_MIN_LINES;
|
|
231
|
+
if (!source && !returned) {
|
|
232
|
+
return { action: 'skip_return', ok: true, id: 'skip_return', reason: 'No return sizes.' };
|
|
233
|
+
}
|
|
234
|
+
if (source > limit && returned >= source) {
|
|
235
|
+
return {
|
|
236
|
+
action: 'block_full_dump',
|
|
237
|
+
ok: false,
|
|
238
|
+
id: 'full_file_dump',
|
|
239
|
+
reason: 'Return only the relevant slice to the frontier model.',
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
action: 'allow_slice',
|
|
244
|
+
ok: true,
|
|
245
|
+
id: 'allow_slice',
|
|
246
|
+
reason: 'Frontier context is a slice, not the whole file.',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function isTargetedRead(toolInput = {}) {
|
|
251
|
+
const offset = Number(toolInput.offset);
|
|
252
|
+
const limit = Number(toolInput.limit);
|
|
253
|
+
return Number.isFinite(offset) || (Number.isFinite(limit) && limit > 0);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function evaluatePreToolUse({
|
|
257
|
+
toolName,
|
|
258
|
+
toolInput = {},
|
|
259
|
+
cwd = process.cwd(),
|
|
260
|
+
threshold = DEFAULT_MIN_LINES,
|
|
261
|
+
} = {}) {
|
|
262
|
+
const name = String(toolName || '');
|
|
263
|
+
const input = toolInput && typeof toolInput === 'object' ? toolInput : {};
|
|
264
|
+
if (/^read$/i.test(name) || name === 'read_file') {
|
|
265
|
+
const rel = input.file_path || input.path || input.target_file;
|
|
266
|
+
const abs = rel ? path.resolve(cwd, String(rel)) : null;
|
|
267
|
+
const lines = countFileLines(abs);
|
|
268
|
+
if (lines == null) {
|
|
269
|
+
return {
|
|
270
|
+
action: 'warn_unknown_size',
|
|
271
|
+
ok: true,
|
|
272
|
+
id: 'unknown_size',
|
|
273
|
+
reason: 'Read path missing or unreadable; not inventing a line count.',
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return evaluateReadIntercept({
|
|
277
|
+
lineCount: lines,
|
|
278
|
+
targeted: isTargetedRead(input),
|
|
279
|
+
threshold,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if (/^bash$/i.test(name) || name === 'Shell') {
|
|
283
|
+
return evaluateBashRead({
|
|
284
|
+
command: input.command || input.cmd,
|
|
285
|
+
cwd,
|
|
286
|
+
threshold,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
action: 'skip_tool',
|
|
291
|
+
ok: true,
|
|
292
|
+
id: 'skip_tool',
|
|
293
|
+
reason: 'Not a Read/Bash dump surface.',
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function collectHaystack(options = {}) {
|
|
298
|
+
const parts = [
|
|
299
|
+
options.task,
|
|
300
|
+
options.query,
|
|
301
|
+
options['clone-portal'] ? 'install shunt@portal vendor @spotify/portal-cli buy Spotify Portal' : '',
|
|
302
|
+
];
|
|
303
|
+
if (Array.isArray(options.argv)) parts.push(...options.argv);
|
|
304
|
+
return parts.filter(Boolean).join(' ');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function buildTokenShuntHonestyReport(options = {}) {
|
|
308
|
+
const findings = [];
|
|
309
|
+
const cloneHits = [...new Set(detectCloneAttempt(collectHaystack(options)))];
|
|
310
|
+
if (cloneHits.length) {
|
|
311
|
+
findings.push({
|
|
312
|
+
severity: 'fail',
|
|
313
|
+
id: 'portal_clone_refused',
|
|
314
|
+
message: `Refusing Portal / shunt@portal clones (${cloneHits.join(', ')}). Lint local Read/bash intercepts; do not buy Spotify Portal.`,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const threshold = Number(options.threshold || options['min-lines']) || DEFAULT_MIN_LINES;
|
|
319
|
+
const lineCount = options.lines != null ? Number(options.lines) : null;
|
|
320
|
+
const targeted = normalizeBoolean(options.targeted);
|
|
321
|
+
const hasRead = lineCount != null && !Number.isNaN(lineCount);
|
|
322
|
+
const hasBash = Boolean(options.bash || options.command);
|
|
323
|
+
const hasRoute = Boolean(options['task-kind'] || options.model);
|
|
324
|
+
const hasReturn = options['source-lines'] != null || options['returned-lines'] != null;
|
|
325
|
+
|
|
326
|
+
if (hasRead) {
|
|
327
|
+
const d = evaluateReadIntercept({ lineCount, targeted, threshold });
|
|
328
|
+
findings.push({
|
|
329
|
+
severity: d.ok ? 'info' : 'fail',
|
|
330
|
+
id: d.id,
|
|
331
|
+
message: d.reason,
|
|
332
|
+
action: d.action,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
if (hasBash) {
|
|
336
|
+
const d = evaluateBashRead({
|
|
337
|
+
command: options.bash || options.command,
|
|
338
|
+
lineCount,
|
|
339
|
+
cwd: options.cwd,
|
|
340
|
+
threshold,
|
|
341
|
+
});
|
|
342
|
+
findings.push({
|
|
343
|
+
severity: d.ok ? 'info' : 'fail',
|
|
344
|
+
id: d.id,
|
|
345
|
+
message: d.reason,
|
|
346
|
+
action: d.action,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (hasRoute) {
|
|
350
|
+
const d = evaluateTaskRoute({
|
|
351
|
+
taskKind: options['task-kind'] || options.kind,
|
|
352
|
+
model: options.model,
|
|
353
|
+
});
|
|
354
|
+
findings.push({
|
|
355
|
+
severity: d.ok ? 'info' : 'fail',
|
|
356
|
+
id: d.id,
|
|
357
|
+
message: d.reason,
|
|
358
|
+
action: d.action,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
if (hasReturn) {
|
|
362
|
+
const d = evaluateContextReturn({
|
|
363
|
+
sourceLines: options['source-lines'],
|
|
364
|
+
returnedLines: options['returned-lines'],
|
|
365
|
+
threshold,
|
|
366
|
+
});
|
|
367
|
+
findings.push({
|
|
368
|
+
severity: d.ok ? 'info' : 'fail',
|
|
369
|
+
id: d.id,
|
|
370
|
+
message: d.reason,
|
|
371
|
+
action: d.action,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!cloneHits.length && !hasRead && !hasBash && !hasRoute && !hasReturn) {
|
|
376
|
+
findings.push({
|
|
377
|
+
severity: 'warn',
|
|
378
|
+
id: 'no_trace',
|
|
379
|
+
message: 'No --lines/--bash/--task-kind/--source-lines. Pass a Read/bash/return trace to lint. Complements PreToolUse; does not install Portal.',
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (normalizeBoolean(options['all-repos-write'])) {
|
|
384
|
+
findings.push({
|
|
385
|
+
severity: 'warn',
|
|
386
|
+
id: 'github_app_all_repos',
|
|
387
|
+
message: 'GitHub App All-repositories write is a Portal catalog connector, not a cash rail and not a license to spam buy links on every repo.',
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
let status = 'ready';
|
|
392
|
+
if (findings.some((f) => f.severity === 'fail')) status = 'fail';
|
|
393
|
+
else if (findings.some((f) => f.severity === 'warn')) status = 'ready_with_warnings';
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
name: 'thumbgate-token-shunt-honesty',
|
|
397
|
+
status,
|
|
398
|
+
ok: status !== 'fail',
|
|
399
|
+
minLines: threshold,
|
|
400
|
+
sliceLines: DEFAULT_SLICE_LINES,
|
|
401
|
+
compareNotClone: true,
|
|
402
|
+
never: [
|
|
403
|
+
'install shunt@portal or @spotify/portal-cli',
|
|
404
|
+
'buy Spotify Portal / click Contact Sales as the cash path',
|
|
405
|
+
'claim 90% token savings from installing the GitHub App',
|
|
406
|
+
'delegate architecture/debug to Flash/Portal/AiKA',
|
|
407
|
+
'dump a whole file back into the frontier context',
|
|
408
|
+
'sell ThumbGate through Portal (ECI pause on paid outreach)',
|
|
409
|
+
],
|
|
410
|
+
source: SOURCE_URLS,
|
|
411
|
+
disclaimer: 'FORMAT steal only. Not affiliated with Spotify. Local Read/bash intercepts; no Portal SKU. GitHub App 162279530 is not revenue.',
|
|
412
|
+
findings,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function formatTokenShuntHonestyReport(report) {
|
|
417
|
+
const lines = [
|
|
418
|
+
'ThumbGate token-shunt honesty (Spotify Portal FORMAT steal)',
|
|
419
|
+
`Status : ${report.status}`,
|
|
420
|
+
`ok : ${report.ok}`,
|
|
421
|
+
`MinLines : ${report.minLines} (untargeted Read above this is blocked)`,
|
|
422
|
+
];
|
|
423
|
+
if (report.findings?.length) {
|
|
424
|
+
lines.push('Findings:');
|
|
425
|
+
for (const f of report.findings) lines.push(` [${f.severity}] ${f.id}: ${f.message}`);
|
|
426
|
+
}
|
|
427
|
+
lines.push(`Never : ${(report.never || []).join('; ')}`);
|
|
428
|
+
lines.push(report.disclaimer);
|
|
429
|
+
return `${lines.join('\n')}\n`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function parseArgv(argv) {
|
|
433
|
+
const options = { argv };
|
|
434
|
+
for (const arg of argv) {
|
|
435
|
+
if (arg === '--json') options.json = true;
|
|
436
|
+
else if (arg === '--strict') options.strict = true;
|
|
437
|
+
else if (arg === '--targeted') options.targeted = true;
|
|
438
|
+
else if (arg === '--clone-portal') options['clone-portal'] = true;
|
|
439
|
+
else if (arg === '--all-repos-write') options['all-repos-write'] = true;
|
|
440
|
+
else if (arg.startsWith('--lines=')) options.lines = arg.slice('--lines='.length);
|
|
441
|
+
else if (arg.startsWith('--threshold=')) options.threshold = arg.slice('--threshold='.length);
|
|
442
|
+
else if (arg.startsWith('--min-lines=')) options['min-lines'] = arg.slice('--min-lines='.length);
|
|
443
|
+
else if (arg.startsWith('--bash=')) options.bash = arg.slice('--bash='.length);
|
|
444
|
+
else if (arg.startsWith('--command=')) options.command = arg.slice('--command='.length);
|
|
445
|
+
else if (arg.startsWith('--task-kind=')) options['task-kind'] = arg.slice('--task-kind='.length);
|
|
446
|
+
else if (arg.startsWith('--model=')) options.model = arg.slice('--model='.length);
|
|
447
|
+
else if (arg.startsWith('--source-lines=')) options['source-lines'] = arg.slice('--source-lines='.length);
|
|
448
|
+
else if (arg.startsWith('--returned-lines=')) options['returned-lines'] = arg.slice('--returned-lines='.length);
|
|
449
|
+
else if (arg.startsWith('--targeted=')) options.targeted = arg.slice('--targeted='.length);
|
|
450
|
+
}
|
|
451
|
+
return options;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function printHelp() {
|
|
455
|
+
process.stdout.write(`Usage: node scripts/token-shunt-honesty.js [options]
|
|
456
|
+
|
|
457
|
+
Spotify Portal FORMAT: intercept untargeted bulk reads.
|
|
458
|
+
Does not install shunt@portal or buy Portal.
|
|
459
|
+
|
|
460
|
+
Options:
|
|
461
|
+
--lines=N --targeted
|
|
462
|
+
--bash='cat file.js'
|
|
463
|
+
--task-kind=architecture --model=gemini-flash
|
|
464
|
+
--source-lines=N --returned-lines=N
|
|
465
|
+
--clone-portal Fail closed
|
|
466
|
+
--all-repos-write Warn: GitHub App is not cash
|
|
467
|
+
--json --strict
|
|
468
|
+
`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function main(argv = process.argv.slice(2)) {
|
|
472
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
473
|
+
printHelp();
|
|
474
|
+
return 0;
|
|
475
|
+
}
|
|
476
|
+
const options = parseArgv(argv);
|
|
477
|
+
const report = buildTokenShuntHonestyReport(options);
|
|
478
|
+
if (options.json) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
479
|
+
else process.stdout.write(formatTokenShuntHonestyReport(report));
|
|
480
|
+
if (options.strict && report.status !== 'ready') return 1;
|
|
481
|
+
if (report.status === 'fail') return 1;
|
|
482
|
+
return 0;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
module.exports = {
|
|
486
|
+
DEFAULT_MIN_LINES,
|
|
487
|
+
DEFAULT_SLICE_LINES,
|
|
488
|
+
SOURCE_URLS,
|
|
489
|
+
detectCloneAttempt,
|
|
490
|
+
evaluateReadIntercept,
|
|
491
|
+
evaluateBashRead,
|
|
492
|
+
evaluateTaskRoute,
|
|
493
|
+
evaluateContextReturn,
|
|
494
|
+
evaluatePreToolUse,
|
|
495
|
+
buildTokenShuntHonestyReport,
|
|
496
|
+
formatTokenShuntHonestyReport,
|
|
497
|
+
main,
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(__filename)) {
|
|
501
|
+
process.exitCode = main();
|
|
502
|
+
}
|