skillgauntlet 1.1.1 → 1.3.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/LICENSE +201 -201
- package/README.md +44 -1
- package/bin/skillgauntlet.js +380 -131
- package/lib/automerge.js +107 -0
- package/lib/browse.js +58 -0
- package/lib/chart.js +160 -0
- package/lib/depth.js +4 -1
- package/lib/discover.js +27 -2
- package/lib/findhost.js +83 -0
- package/lib/i18n.js +405 -39
- package/lib/models.js +46 -0
- package/lib/report.js +1 -1
- package/lib/schema.js +90 -0
- package/lib/scrub.js +186 -0
- package/lib/static.js +24 -13
- package/lib/submission.js +325 -0
- package/lib/submit.js +208 -0
- package/lib/ticker.js +39 -0
- package/lib/trial.js +65 -13
- package/lib/ui.js +40 -2
- package/lib/update.js +73 -0
- package/package.json +4 -2
- package/schemas/submission-1.json +210 -0
package/lib/trial.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// answers without calling any tool -> not_triggered, counted apart
|
|
9
9
|
//
|
|
10
10
|
// NOT YET VERIFIED against a live host: written from the Python runner, exercised only in dry-run.
|
|
11
|
-
import { execFileSync, spawn } from 'node:child_process';
|
|
11
|
+
import { execFileSync, spawn, spawnSync } from 'node:child_process';
|
|
12
12
|
import fs from 'node:fs';
|
|
13
13
|
import os from 'node:os';
|
|
14
14
|
import path from 'node:path';
|
|
@@ -31,12 +31,19 @@ export const HOSTS = {
|
|
|
31
31
|
export function backendFor(host, model) {
|
|
32
32
|
if (host === 'codewhale') return 'codewhale:api.deepseek.com';
|
|
33
33
|
if (host !== 'codebuddy') return backendHost();
|
|
34
|
-
return
|
|
34
|
+
return usesOwnDeepseek('codebuddy', model) ? 'codebuddy:api.deepseek.com' : 'codebuddy';
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
// A model id starting with "deepseek" means the DeepSeek API, whichever host drives it.
|
|
38
38
|
export const isDeepseekModel = (model) => /^deepseek/i.test(String(model || ''));
|
|
39
39
|
|
|
40
|
+
// A DeepSeek model that the host lists itself (CodeBuddy's own deepseek-v4.1-flash) is served by the host's account
|
|
41
|
+
// and needs no key of ours. Only a deepseek-* name the host does not list takes the package's own DeepSeek route
|
|
42
|
+
// (a models.json entry in the trial directory and DEEPSEEK_API_KEY from the environment).
|
|
43
|
+
const nativeModels = new Map();
|
|
44
|
+
export const setNativeModels = (host, list) => nativeModels.set(host, new Set(list || []));
|
|
45
|
+
export const usesOwnDeepseek = (host, model) => isDeepseekModel(model) && !nativeModels.get(host)?.has(model);
|
|
46
|
+
|
|
40
47
|
// DeepSeek's integration guide for WorkBuddy/CodeBuddy: a models.json entry whose key is read from the
|
|
41
48
|
// environment (${DEEPSEEK_API_KEY}), so the file holds no secret. It is written into the trial's own
|
|
42
49
|
// directory (project level), never into the user's ~/.codebuddy.
|
|
@@ -66,7 +73,12 @@ const LINE_BREAK = /\r?\n/;
|
|
|
66
73
|
|
|
67
74
|
// On Windows `where` returns the extension-less shim first (a shell script Node cannot exec), then the
|
|
68
75
|
// .cmd that it can -- with shell: true. Prefer a real executable, then a cmd shim, then whatever is left.
|
|
76
|
+
// A host CLI found somewhere off PATH (or just installed) and accepted by the user, keyed by its command name.
|
|
77
|
+
const binOverride = new Map();
|
|
78
|
+
export const setHostBin = (name, file) => binOverride.set(name, file);
|
|
79
|
+
|
|
69
80
|
export function resolveBin(name) {
|
|
81
|
+
if (binOverride.has(name)) return binOverride.get(name);
|
|
70
82
|
const finder = process.platform === 'win32' ? 'where' : 'which';
|
|
71
83
|
let found = [];
|
|
72
84
|
try {
|
|
@@ -83,6 +95,7 @@ export function resolveBin(name) {
|
|
|
83
95
|
// Whether a host CLI can be found on PATH at all. resolveBin falls back to the bare name, which on Windows
|
|
84
96
|
// ends in cmd.exe's "not recognized" (or a spawn ENOENT) deep inside a trial; this asks up front.
|
|
85
97
|
export function binInstalled(name) {
|
|
98
|
+
if (binOverride.has(name)) return true;
|
|
86
99
|
try {
|
|
87
100
|
execFileSync(process.platform === 'win32' ? 'where' : 'which', [name], { stdio: 'ignore' });
|
|
88
101
|
return true;
|
|
@@ -103,6 +116,44 @@ export function requireHost(host, probe = binInstalled) {
|
|
|
103
116
|
throw err;
|
|
104
117
|
}
|
|
105
118
|
|
|
119
|
+
// One tiny call before the real work: a host that is not signed in retries silently for minutes, so the user
|
|
120
|
+
// would stare at a frozen screen. A short timeout turns that into an immediate, actionable message.
|
|
121
|
+
// The first non-empty line of some CLI output, for an error message.
|
|
122
|
+
const firstLine = (text) => String(text || '').split(LINE_BREAK).map((l) => l.trim()).find(Boolean) || '';
|
|
123
|
+
|
|
124
|
+
// What a host CLI answered, as an error reason: the CLI's own words when it gave any. A CLI can exit 0 with nothing on
|
|
125
|
+
// stdout and its complaint on stderr (CodeBuddy does for a custom model without an endpoint url), so a clean exit
|
|
126
|
+
// status proves nothing; an empty or unreadable stdout is the failure.
|
|
127
|
+
export function hostAnswerProblem(r, timeout) {
|
|
128
|
+
if (r.error) return r.error.code === 'ETIMEDOUT' ? `no answer within ${timeout}s` : String(r.error.message).split(LINE_BREAK)[0];
|
|
129
|
+
if (r.signal) return `no answer within ${timeout}s`;
|
|
130
|
+
const out = String(r.stdout || '');
|
|
131
|
+
if (r.status !== 0) return firstLine(r.stderr) || firstLine(out) || `exit status ${r.status}`;
|
|
132
|
+
if (!out.trim()) return firstLine(r.stderr) || 'the CLI answered with nothing';
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(out);
|
|
135
|
+
const last = Array.isArray(parsed) ? parsed.filter((m) => m?.type === 'result').pop() : parsed;
|
|
136
|
+
if (last?.is_error) return firstLine(last.result) || 'the CLI reported an error';
|
|
137
|
+
} catch { return firstLine(out); }
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function pingHost(host, { bin, model = null, timeout = 90, spawn = spawnSync } = {}) {
|
|
142
|
+
const spec = HOSTS[host];
|
|
143
|
+
if (!spec || host === 'codewhale') return;
|
|
144
|
+
const args = ['-p', 'Reply with the single word OK.', '--output-format', 'json', '--tools', '', '--no-session-persistence'];
|
|
145
|
+
if (model && !usesOwnDeepseek(host, model)) args.push('--model', model); // the package's own DeepSeek route has no host-side name
|
|
146
|
+
const launch = launchSpec(bin, args, spec.bin);
|
|
147
|
+
const r = spawn(launch.cmd, launch.args, { encoding: 'utf8', timeout: timeout * 1000, shell: launch.shell, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
148
|
+
const why = hostAnswerProblem(r, timeout);
|
|
149
|
+
if (why) {
|
|
150
|
+
const e = new Error(`host ${host} did not answer: ${why}`);
|
|
151
|
+
e.messageKey = 'hostNotReady';
|
|
152
|
+
e.vars = { host: HOST_NAME[host] || host, why: why.slice(0, 200) };
|
|
153
|
+
throw e;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
106
157
|
// Variables a host (the Claude desktop app, the Agent SDK, an outer Claude Code session) sets for its own
|
|
107
158
|
// child processes. A trial inherits them otherwise, and they change what the model under test sees: under
|
|
108
159
|
// the desktop app the CLI adds Artifact, ArtifactComments and ArtifactData to the tool list, and the model
|
|
@@ -189,7 +240,7 @@ export function runTrial({ bin, skillDir, skillName, query, model, timeout = 180
|
|
|
189
240
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sg-'));
|
|
190
241
|
const spec = HOSTS[host];
|
|
191
242
|
copyDir(skillDir, path.join(tmp, ...spec.skillsDir, skillName));
|
|
192
|
-
if (host === 'codebuddy' &&
|
|
243
|
+
if (host === 'codebuddy' && usesOwnDeepseek('codebuddy', model)) writeModelsJson(tmp, model);
|
|
193
244
|
const args = host === 'codebuddy'
|
|
194
245
|
? ['-p', query, '--output-format', 'stream-json', '--verbose', '--setting-sources', 'project', '--strict-mcp-config',
|
|
195
246
|
'--no-session-persistence', '--tools', spec.tools]
|
|
@@ -362,23 +413,24 @@ ${text}`;
|
|
|
362
413
|
const launch = launchSpec(bin, args, HOSTS[host].bin);
|
|
363
414
|
// CodeBuddy reads a custom model from the working directory's .codebuddy/models.json, so a DeepSeek draft
|
|
364
415
|
// runs in a scratch directory that has one.
|
|
365
|
-
const scratch = host === 'codebuddy' &&
|
|
416
|
+
const scratch = host === 'codebuddy' && usesOwnDeepseek('codebuddy', model) ? fs.mkdtempSync(path.join(os.tmpdir(), 'sg-draft-')) : null;
|
|
366
417
|
if (scratch) writeModelsJson(scratch, model);
|
|
367
|
-
let
|
|
418
|
+
let r;
|
|
368
419
|
try {
|
|
369
|
-
|
|
420
|
+
r = spawnSync(launch.cmd, launch.args, {
|
|
370
421
|
encoding: 'utf8', timeout: timeout * 1000, maxBuffer: 32 * 1024 * 1024,
|
|
371
|
-
shell: launch.shell, windowsHide: true, ...(scratch ? { cwd: scratch } : {}),
|
|
422
|
+
shell: launch.shell, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'], ...(scratch ? { cwd: scratch } : {}),
|
|
372
423
|
});
|
|
373
|
-
} catch (err) {
|
|
374
|
-
// The default message of a failed execFileSync is the whole command line, which here is the drafting prompt.
|
|
375
|
-
const why = err.code === 'ETIMEDOUT' || err.killed
|
|
376
|
-
? `no answer within ${timeout}s (a rejected key or an unreachable endpoint retries silently until then)`
|
|
377
|
-
: String(err.stderr || err.message).split(/\r?\n/).map((l) => l.trim()).find(Boolean) || 'exit status ' + err.status;
|
|
378
|
-
throw new Error(`the drafting call failed: ${why}`);
|
|
379
424
|
} finally {
|
|
380
425
|
if (scratch) { try { fs.rmSync(scratch, { recursive: true, force: true }); } catch { /* best effort */ } }
|
|
381
426
|
}
|
|
427
|
+
// The CLI's own words, not "Unexpected end of JSON input": an empty answer with a complaint on stderr is common.
|
|
428
|
+
const problem = hostAnswerProblem(r, timeout);
|
|
429
|
+
if (problem) {
|
|
430
|
+
const hint = r.error?.code === 'ETIMEDOUT' || r.signal ? ' (a rejected key or an unreachable endpoint retries silently until then)' : '';
|
|
431
|
+
throw new Error(`the drafting call failed: ${problem}${hint}`);
|
|
432
|
+
}
|
|
433
|
+
const out = r.stdout;
|
|
382
434
|
const parsed = JSON.parse(out);
|
|
383
435
|
// Claude Code answers with one result object; CodeBuddy Code with the whole message list, whose last
|
|
384
436
|
// element is the result.
|
package/lib/ui.js
CHANGED
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
// the run mid-flow.
|
|
7
7
|
import readline, { emitKeypressEvents } from 'node:readline';
|
|
8
8
|
import { stdin as input, stdout as output } from 'node:process';
|
|
9
|
-
import { words } from './i18n.js';
|
|
9
|
+
import { words, t } from './i18n.js';
|
|
10
|
+
|
|
11
|
+
// Thrown by a list when the user presses ← or Esc: the caller goes back one question.
|
|
12
|
+
export class Back extends Error {
|
|
13
|
+
constructor() { super('back'); this.name = 'Back'; }
|
|
14
|
+
}
|
|
15
|
+
let backEnabled = false;
|
|
16
|
+
export const setBack = (on) => { backEnabled = on; };
|
|
10
17
|
|
|
11
18
|
let rl = null;
|
|
12
19
|
const queue = [];
|
|
@@ -37,10 +44,22 @@ export function pauseUi() {
|
|
|
37
44
|
}
|
|
38
45
|
}
|
|
39
46
|
|
|
47
|
+
// On Windows a terminal stdin that was ever put in raw mode can keep the process alive after the last prompt (the
|
|
48
|
+
// program handled Ctrl+C, then sat there with no prompt back). Let go of it explicitly.
|
|
40
49
|
export function closeUi() {
|
|
41
50
|
pauseUi();
|
|
51
|
+
if (input.isTTY) {
|
|
52
|
+
try {
|
|
53
|
+
if (input.setRawMode) input.setRawMode(false);
|
|
54
|
+
input.pause();
|
|
55
|
+
input.unref?.();
|
|
56
|
+
input.destroy?.();
|
|
57
|
+
} catch { /* already closed */ }
|
|
58
|
+
}
|
|
42
59
|
}
|
|
43
60
|
|
|
61
|
+
export const hasTerminal = () => Boolean(input.isTTY);
|
|
62
|
+
|
|
44
63
|
export function say(line = '') {
|
|
45
64
|
output.write(`${line}\n`);
|
|
46
65
|
}
|
|
@@ -61,6 +80,9 @@ export async function ask(question, def = '') {
|
|
|
61
80
|
}
|
|
62
81
|
|
|
63
82
|
export async function confirm(question, def = false) {
|
|
83
|
+
if (input.isTTY) {
|
|
84
|
+
return select(question, [{ label: t('ui.no'), value: false }, { label: t('ui.yes'), value: true }], def ? 1 : 0, { hotkeys: { y: 1, n: 0 } });
|
|
85
|
+
}
|
|
64
86
|
const hint = def ? 'Y/n' : 'y/N';
|
|
65
87
|
for (;;) {
|
|
66
88
|
const a = (await ask(`${question} (${hint})`, '')).toLowerCase();
|
|
@@ -86,7 +108,9 @@ export async function choose(title, options, def = 1) {
|
|
|
86
108
|
}
|
|
87
109
|
}
|
|
88
110
|
|
|
89
|
-
|
|
111
|
+
// opts.clear: erase the list once an entry is chosen, so a caller that shows the next level right away redraws
|
|
112
|
+
// in the same place instead of stacking lists down the terminal.
|
|
113
|
+
export async function select(title, options, defIndex = 0, opts = {}) {
|
|
90
114
|
if (!input.isTTY) {
|
|
91
115
|
say('');
|
|
92
116
|
say(title);
|
|
@@ -108,6 +132,7 @@ export async function select(title, options, defIndex = 0) {
|
|
|
108
132
|
const WINDOW = 8;
|
|
109
133
|
const width = Math.min(48, Math.max(...options.map((o) => o.label.length)));
|
|
110
134
|
let first = true;
|
|
135
|
+
let height = 0;
|
|
111
136
|
const render = () => {
|
|
112
137
|
const size = Math.min(WINDOW, options.length);
|
|
113
138
|
const start = Math.min(Math.max(0, idx - Math.floor(size / 2)), Math.max(0, options.length - size));
|
|
@@ -118,6 +143,8 @@ export async function select(title, options, defIndex = 0) {
|
|
|
118
143
|
const body = i === idx ? `\x1b[7m▶ ${label} \x1b[0m` : ` ${label} `;
|
|
119
144
|
lines.push(`${body}${o.hint ? ` ${o.hint}` : ''}`);
|
|
120
145
|
}
|
|
146
|
+
if (backEnabled && !opts.noBack) lines.push(`\x1b[2m${t('backHint')}\x1b[0m`);
|
|
147
|
+
height = lines.length;
|
|
121
148
|
if (first) {
|
|
122
149
|
output.write(`\n${lines.join('\n')}\n`);
|
|
123
150
|
first = false;
|
|
@@ -142,10 +169,21 @@ export async function select(title, options, defIndex = 0) {
|
|
|
142
169
|
render();
|
|
143
170
|
} else if (key.name === 'return' || key.name === 'enter') {
|
|
144
171
|
cleanup();
|
|
172
|
+
if (opts.clear) output.write(`\x1b[${height + 1}A\x1b[J`);
|
|
145
173
|
resolve(options[idx].value);
|
|
174
|
+
} else if ((key.name === 'escape' || key.name === 'left') && backEnabled && !opts.noBack) {
|
|
175
|
+
cleanup();
|
|
176
|
+
output.write(`\x1b[${height + 1}A\x1b[J`);
|
|
177
|
+
reject(new Back());
|
|
146
178
|
} else if (key.ctrl && key.name === 'c') {
|
|
147
179
|
cleanup();
|
|
148
180
|
reject(new Error('interrupted'));
|
|
181
|
+
} else if (ch && opts.hotkeys && Object.hasOwn(opts.hotkeys, ch.toLowerCase())) {
|
|
182
|
+
// a hotkey (y / n) chooses at once
|
|
183
|
+
idx = opts.hotkeys[ch.toLowerCase()];
|
|
184
|
+
render();
|
|
185
|
+
cleanup();
|
|
186
|
+
resolve(options[idx].value);
|
|
149
187
|
} else if (ch && /^[1-9]$/.test(ch) && Number(ch) <= options.length) {
|
|
150
188
|
idx = Number(ch) - 1;
|
|
151
189
|
render();
|
package/lib/update.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// A check for a newer published version before an interactive session. It must never get in the way: any failure
|
|
2
|
+
// (offline, slow registry, odd reply, unwritable cache) means "no update known" and the session goes on with the
|
|
3
|
+
// version that is running.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const REGISTRY = 'https://registry.npmjs.org/skillgauntlet/latest';
|
|
10
|
+
const CHECK_EVERY_MS = 12 * 60 * 60 * 1000;
|
|
11
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
export function currentVersion() {
|
|
14
|
+
try { return JSON.parse(fs.readFileSync(path.join(here, '..', 'package.json'), 'utf8')).version; } catch { return null; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// a.b.c with an optional pre-release tail; a pre-release is older than its release.
|
|
18
|
+
export function isNewer(latest, current) {
|
|
19
|
+
const parse = (v) => {
|
|
20
|
+
const m = String(v).trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
|
|
21
|
+
return m ? { n: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] || null } : null;
|
|
22
|
+
};
|
|
23
|
+
const a = parse(latest);
|
|
24
|
+
const b = parse(current);
|
|
25
|
+
if (!a || !b) return false;
|
|
26
|
+
for (let i = 0; i < 3; i += 1) if (a.n[i] !== b.n[i]) return a.n[i] > b.n[i];
|
|
27
|
+
return Boolean(b.pre) && !a.pre;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// A copy that runs from a git checkout is a developer's own; it is not nagged about releases.
|
|
31
|
+
export function isCheckout() {
|
|
32
|
+
return fs.existsSync(path.join(here, '..', '.git'));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function cachePath() {
|
|
36
|
+
return path.join(process.env.SKILLGAUNTLET_CACHE || path.join(os.homedir(), '.cache', 'skillgauntlet'), 'update-check.json');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readCache(file) {
|
|
40
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return {}; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeCache(file, data) {
|
|
44
|
+
try {
|
|
45
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
46
|
+
fs.writeFileSync(file, `${JSON.stringify(data)}\n`);
|
|
47
|
+
} catch { /* the cache is an optimisation */ }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// { current, latest } when a newer version is known and has not been declined, else null. Never throws.
|
|
51
|
+
export async function checkForUpdate({ fetchLatest, now = Date.now(), file = cachePath(), current = currentVersion(), env = process.env } = {}) {
|
|
52
|
+
try {
|
|
53
|
+
if (!current || env.SG_NO_UPDATE_CHECK || env.CI) return null;
|
|
54
|
+
const cache = readCache(file);
|
|
55
|
+
let latest = cache.latest;
|
|
56
|
+
if (!latest || !cache.checkedAt || now - cache.checkedAt > CHECK_EVERY_MS) {
|
|
57
|
+
const get = fetchLatest || (async () => {
|
|
58
|
+
const res = await fetch(REGISTRY, { signal: AbortSignal.timeout(3000), headers: { accept: 'application/json' } });
|
|
59
|
+
if (!res.ok) throw new Error(`registry ${res.status}`);
|
|
60
|
+
return (await res.json()).version;
|
|
61
|
+
});
|
|
62
|
+
latest = await get();
|
|
63
|
+
writeCache(file, { ...cache, latest, checkedAt: now });
|
|
64
|
+
}
|
|
65
|
+
if (!isNewer(latest, current) || cache.declined === latest) return null;
|
|
66
|
+
return { current, latest };
|
|
67
|
+
} catch { return null; }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Remember that the user said no to this version, so the question is not asked again until a newer one appears.
|
|
71
|
+
export function declineUpdate(latest, file = cachePath()) {
|
|
72
|
+
writeCache(file, { ...readCache(file), declined: latest });
|
|
73
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillgauntlet",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Check an agent skill before you install it: context cost, permission surface, code risk, instruction hijacking, description quality, trigger rate and gain.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"bin",
|
|
11
11
|
"lib",
|
|
12
12
|
"contracts",
|
|
13
|
+
"schemas",
|
|
13
14
|
"README.md",
|
|
14
15
|
"LICENSE",
|
|
15
16
|
"LICENSE-DATA"
|
|
@@ -20,7 +21,8 @@
|
|
|
20
21
|
"scripts": {
|
|
21
22
|
"start": "node ./bin/skillgauntlet.js",
|
|
22
23
|
"static": "node ./bin/skillgauntlet.js static",
|
|
23
|
-
"test": "node --test tests/*.test.mjs"
|
|
24
|
+
"test": "node --test tests/*.test.mjs",
|
|
25
|
+
"leaderboard": "node scripts/build-leaderboard.mjs"
|
|
24
26
|
},
|
|
25
27
|
"keywords": [
|
|
26
28
|
"agent",
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/SkillGauntlet/SkillGauntlet/blob/main/schemas/submission-1.json",
|
|
4
|
+
"title": "SkillGauntlet community submission, version 1",
|
|
5
|
+
"description": "One submission directory holds meta.json, raw.jsonl, summary.json and queries.json. Definitions: meta, rawLine, summary, queries.",
|
|
6
|
+
"$defs": {
|
|
7
|
+
"sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
|
|
8
|
+
"commit": { "type": "string", "pattern": "^[0-9a-f]{40}$" },
|
|
9
|
+
"isoTime": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$" },
|
|
10
|
+
"shortText": { "type": "string", "maxLength": 300 },
|
|
11
|
+
"rate": { "type": ["number", "null"], "minimum": 0, "maximum": 1 },
|
|
12
|
+
"interval": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"minItems": 2,
|
|
15
|
+
"maxItems": 2,
|
|
16
|
+
"items": { "type": ["number", "null"], "minimum": 0, "maximum": 1 }
|
|
17
|
+
},
|
|
18
|
+
"meta": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"required": [
|
|
21
|
+
"schema", "runId", "runnerVersion", "skill", "host", "backend", "model", "runs", "maxExplore",
|
|
22
|
+
"querySets", "platform", "startedAt", "finishedAt", "trials", "interrupted", "submission"
|
|
23
|
+
],
|
|
24
|
+
"additionalProperties": false,
|
|
25
|
+
"properties": {
|
|
26
|
+
"schema": { "const": "skillgauntlet/run/1" },
|
|
27
|
+
"runId": { "type": "string", "pattern": "^[0-9]{8}-[0-9]{6}(-[0-9]+)?$" },
|
|
28
|
+
"runnerVersion": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+([-+][0-9A-Za-z.-]+)?$" },
|
|
29
|
+
"skill": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$" },
|
|
30
|
+
"host": { "enum": ["claude-code", "codebuddy", "codewhale"] },
|
|
31
|
+
"backend": { "type": "string", "maxLength": 100 },
|
|
32
|
+
"model": { "type": ["string", "null"], "maxLength": 100 },
|
|
33
|
+
"runs": { "type": "integer", "minimum": 1, "maximum": 20 },
|
|
34
|
+
"maxExplore": { "type": "integer", "minimum": 0, "maximum": 20 },
|
|
35
|
+
"depth": { "type": "object", "maxProperties": 8 },
|
|
36
|
+
"querySets": {
|
|
37
|
+
"type": "array",
|
|
38
|
+
"minItems": 1,
|
|
39
|
+
"maxItems": 4,
|
|
40
|
+
"items": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"required": ["id", "queries", "sha256"],
|
|
43
|
+
"properties": {
|
|
44
|
+
"id": { "type": "string", "maxLength": 40 },
|
|
45
|
+
"source": { "type": "string", "maxLength": 40 },
|
|
46
|
+
"file": { "type": "string", "maxLength": 120 },
|
|
47
|
+
"queries": { "type": "integer", "minimum": 1 },
|
|
48
|
+
"clauses": { "type": "integer", "minimum": 0 },
|
|
49
|
+
"sha256": { "type": "string", "pattern": "^[0-9a-f]{16}$" },
|
|
50
|
+
"negativesQuality": { "type": "object", "maxProperties": 4 }
|
|
51
|
+
},
|
|
52
|
+
"additionalProperties": false
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"skillFingerprint": { "type": ["string", "null"], "pattern": "^[0-9a-f]{16}$" },
|
|
56
|
+
"platform": { "$ref": "#/$defs/shortText" },
|
|
57
|
+
"startedAt": { "$ref": "#/$defs/isoTime" },
|
|
58
|
+
"finishedAt": { "$ref": "#/$defs/isoTime" },
|
|
59
|
+
"trials": { "type": "integer", "minimum": 1 },
|
|
60
|
+
"interrupted": { "const": false },
|
|
61
|
+
"resumedAt": { "type": "array", "maxItems": 50, "items": { "$ref": "#/$defs/isoTime" } },
|
|
62
|
+
"modelResolved": { "type": ["string", "null"], "maxLength": 100 },
|
|
63
|
+
"apiKeySource": { "type": ["string", "null"], "maxLength": 40 },
|
|
64
|
+
"claudeCodeVersion": { "type": ["string", "null"], "maxLength": 100 },
|
|
65
|
+
"loadedSkills": { "type": "array", "maxItems": 300, "items": { "type": "string", "maxLength": 100 } },
|
|
66
|
+
"tools": { "type": "array", "maxItems": 300, "items": { "type": "string", "maxLength": 100 } },
|
|
67
|
+
"incompleteTrials": { "type": "integer", "minimum": 0 },
|
|
68
|
+
"submission": { "$ref": "#/$defs/submission" }
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"submission": {
|
|
72
|
+
"type": "object",
|
|
73
|
+
"required": ["schema", "contributor", "skill", "host", "model", "queryPack", "environment", "submittedAt"],
|
|
74
|
+
"additionalProperties": false,
|
|
75
|
+
"properties": {
|
|
76
|
+
"schema": { "const": "skillgauntlet/submission/1" },
|
|
77
|
+
"contributor": { "type": "string", "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$" },
|
|
78
|
+
"skill": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"required": ["name", "repo", "commit", "subdir", "skillMdSha256"],
|
|
81
|
+
"additionalProperties": false,
|
|
82
|
+
"properties": {
|
|
83
|
+
"name": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$" },
|
|
84
|
+
"repo": { "type": "string", "pattern": "^https://github\\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$" },
|
|
85
|
+
"commit": { "$ref": "#/$defs/commit" },
|
|
86
|
+
"subdir": { "type": "string", "maxLength": 200, "pattern": "^[A-Za-z0-9_./-]*$" },
|
|
87
|
+
"skillMdSha256": { "$ref": "#/$defs/sha256" }
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"host": {
|
|
91
|
+
"type": "object",
|
|
92
|
+
"required": ["id", "version"],
|
|
93
|
+
"additionalProperties": false,
|
|
94
|
+
"properties": {
|
|
95
|
+
"id": { "enum": ["claude-code", "codebuddy", "codewhale"] },
|
|
96
|
+
"version": { "type": ["string", "null"], "maxLength": 100 }
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"model": {
|
|
100
|
+
"type": "object",
|
|
101
|
+
"required": ["requested", "resolved"],
|
|
102
|
+
"additionalProperties": false,
|
|
103
|
+
"properties": {
|
|
104
|
+
"requested": { "type": ["string", "null"], "maxLength": 100 },
|
|
105
|
+
"resolved": { "type": ["string", "null"], "maxLength": 100 }
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"queryPack": {
|
|
109
|
+
"type": "object",
|
|
110
|
+
"required": ["id", "sha256", "queries"],
|
|
111
|
+
"additionalProperties": false,
|
|
112
|
+
"properties": {
|
|
113
|
+
"id": { "type": "string", "maxLength": 40 },
|
|
114
|
+
"sha256": { "$ref": "#/$defs/sha256" },
|
|
115
|
+
"queries": { "type": "integer", "minimum": 1 }
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"environment": {
|
|
119
|
+
"type": "object",
|
|
120
|
+
"description": "Used to group and explain results, never to prove anything: every field is self-reported.",
|
|
121
|
+
"required": ["id", "platform", "hostVersion", "backend", "loadedSkills", "toolCount"],
|
|
122
|
+
"additionalProperties": false,
|
|
123
|
+
"properties": {
|
|
124
|
+
"id": { "type": "string", "pattern": "^[0-9a-f]{12}$" },
|
|
125
|
+
"platform": { "$ref": "#/$defs/shortText" },
|
|
126
|
+
"hostVersion": { "type": ["string", "null"], "maxLength": 100 },
|
|
127
|
+
"backend": { "type": "string", "maxLength": 100 },
|
|
128
|
+
"loadedSkills": { "type": "array", "maxItems": 300, "items": { "type": "string", "maxLength": 100 } },
|
|
129
|
+
"toolCount": { "type": "integer", "minimum": 0 }
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"submittedAt": { "$ref": "#/$defs/isoTime" }
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"rawLine": {
|
|
136
|
+
"type": "object",
|
|
137
|
+
"required": ["host", "verdict", "queryId", "run", "query", "shouldTrigger"],
|
|
138
|
+
"properties": {
|
|
139
|
+
"host": { "enum": ["claude-code", "codebuddy", "codewhale"] },
|
|
140
|
+
"verdict": { "enum": ["triggered", "not_triggered", "other_skill", "error", "timeout"] },
|
|
141
|
+
"invokedSkill": { "type": ["string", "null"], "maxLength": 200 },
|
|
142
|
+
"firstTool": { "type": ["string", "null"], "maxLength": 200 },
|
|
143
|
+
"tools": { "type": "array", "maxItems": 100, "items": { "type": "string", "maxLength": 200 } },
|
|
144
|
+
"exploreSteps": { "type": "integer", "minimum": 0 },
|
|
145
|
+
"error": { "type": ["string", "null"], "maxLength": 400 },
|
|
146
|
+
"events": { "type": "object", "maxProperties": 10 },
|
|
147
|
+
"queryId": { "type": "string", "maxLength": 40 },
|
|
148
|
+
"run": { "type": "integer", "minimum": 0, "maximum": 50 },
|
|
149
|
+
"query": { "type": "string", "minLength": 1, "maxLength": 4000 },
|
|
150
|
+
"shouldTrigger": { "type": "boolean" }
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"summary": {
|
|
154
|
+
"type": "object",
|
|
155
|
+
"required": ["trials", "recall", "fpr", "questions", "host", "runs", "recallCi", "fprCi"],
|
|
156
|
+
"properties": {
|
|
157
|
+
"trials": { "type": "integer", "minimum": 1 },
|
|
158
|
+
"recall": { "$ref": "#/$defs/rate" },
|
|
159
|
+
"fpr": { "$ref": "#/$defs/rate" },
|
|
160
|
+
"noToolRate": { "$ref": "#/$defs/rate" },
|
|
161
|
+
"failedTrials": { "type": "integer", "minimum": 0 },
|
|
162
|
+
"incomplete": { "type": "integer", "minimum": 0 },
|
|
163
|
+
"model": { "type": ["string", "null"], "maxLength": 100 },
|
|
164
|
+
"backend": { "type": "string", "maxLength": 100 },
|
|
165
|
+
"host": { "enum": ["claude-code", "codebuddy", "codewhale"] },
|
|
166
|
+
"runs": { "type": "integer", "minimum": 1, "maximum": 20 },
|
|
167
|
+
"recallCi": { "$ref": "#/$defs/interval" },
|
|
168
|
+
"fprCi": { "$ref": "#/$defs/interval" },
|
|
169
|
+
"questions": {
|
|
170
|
+
"type": "array",
|
|
171
|
+
"minItems": 1,
|
|
172
|
+
"maxItems": 2000,
|
|
173
|
+
"items": {
|
|
174
|
+
"type": "object",
|
|
175
|
+
"required": ["query", "shouldTrigger", "hits", "n"],
|
|
176
|
+
"properties": {
|
|
177
|
+
"query": { "type": "string", "maxLength": 4000 },
|
|
178
|
+
"shouldTrigger": { "type": "boolean" },
|
|
179
|
+
"hits": { "type": "integer", "minimum": 0 },
|
|
180
|
+
"n": { "type": "integer", "minimum": 1 }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
"queries": {
|
|
187
|
+
"type": "object",
|
|
188
|
+
"required": ["schema", "queries"],
|
|
189
|
+
"properties": {
|
|
190
|
+
"schema": { "const": "skillgauntlet/queries/1" },
|
|
191
|
+
"queries": {
|
|
192
|
+
"type": "array",
|
|
193
|
+
"minItems": 1,
|
|
194
|
+
"maxItems": 2000,
|
|
195
|
+
"items": {
|
|
196
|
+
"type": "object",
|
|
197
|
+
"required": ["id", "query", "shouldTrigger"],
|
|
198
|
+
"properties": {
|
|
199
|
+
"id": { "type": "string", "maxLength": 40 },
|
|
200
|
+
"query": { "type": "string", "minLength": 1, "maxLength": 4000 },
|
|
201
|
+
"shouldTrigger": { "type": "boolean" },
|
|
202
|
+
"clause": { "type": ["string", "null"], "maxLength": 300 },
|
|
203
|
+
"note": { "type": ["string", "null"], "maxLength": 600 }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|