skillgauntlet 1.1.0 → 1.1.1
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/bin/skillgauntlet.js +4 -2
- package/lib/gain.js +10 -6
- package/lib/i18n.js +14 -1
- package/lib/trial.js +42 -0
- package/package.json +1 -1
package/bin/skillgauntlet.js
CHANGED
|
@@ -14,7 +14,7 @@ import { openRun, querySetHash } from '../lib/runs.js';
|
|
|
14
14
|
import { DEPTHS, estimate } from '../lib/depth.js';
|
|
15
15
|
import { ask, choose, select, confirm, say, closeUi } from '../lib/ui.js';
|
|
16
16
|
import { setLang, detectLang, errorText, t } from '../lib/i18n.js';
|
|
17
|
-
import { resolveBin, backend, backendFor, isDeepseekModel, HOSTS, checkModel, draftQueries, draftQuality, runTrial } from '../lib/trial.js';
|
|
17
|
+
import { resolveBin, backend, backendFor, isDeepseekModel, HOSTS, checkModel, draftQueries, draftQuality, runTrial, requireHost } from '../lib/trial.js';
|
|
18
18
|
import { runGain } from '../lib/gain.js';
|
|
19
19
|
import { ERROR_STREAK_STOP, executeTrials, finishRun, resumeRun } from '../lib/execute.js';
|
|
20
20
|
|
|
@@ -40,6 +40,7 @@ function writeOut(skillDir, name, md, sc) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
async function runTriggerLayer({ skillDir, skillName, depth, model, host = 'claude-code' }) {
|
|
43
|
+
requireHost(host);
|
|
43
44
|
const bin = resolveBin(HOSTS[host].bin);
|
|
44
45
|
const cacheFile = cacheFileFor(skillDir, depth);
|
|
45
46
|
const want = { perClause: depth.perClause, negatives: depth.negatives, skillFingerprint: fingerprint(skillDir) };
|
|
@@ -171,6 +172,7 @@ async function cmdResume(args) {
|
|
|
171
172
|
const full = path.resolve(dir);
|
|
172
173
|
const runMeta = JSON.parse(fs.readFileSync(path.join(full, 'meta.json'), 'utf8'));
|
|
173
174
|
const runHost = runMeta.host || 'claude-code';
|
|
175
|
+
requireHost(runHost);
|
|
174
176
|
const result = await resumeRun({
|
|
175
177
|
dir: full,
|
|
176
178
|
bin: resolveBin((HOSTS[runHost] || HOSTS['claude-code']).bin),
|
|
@@ -387,7 +389,7 @@ async function interactive() {
|
|
|
387
389
|
skillDir: target.dir, skillName: target.name, depth, model, host: { claude: 'claude-code', codebuddy: 'codebuddy', codewhale: 'codewhale' }[host],
|
|
388
390
|
});
|
|
389
391
|
} catch (err) {
|
|
390
|
-
say(` ${t('trialFail', { err: err
|
|
392
|
+
say(` ${t('trialFail', { err: errorText(err) })}`);
|
|
391
393
|
}
|
|
392
394
|
if (host === 'claude' && depth.layers.includes('gain')) {
|
|
393
395
|
say(` ${t('gainNote')}`);
|
package/lib/gain.js
CHANGED
|
@@ -21,6 +21,8 @@ import fs from 'node:fs';
|
|
|
21
21
|
import os from 'node:os';
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import { launchSpec } from './spawn.js';
|
|
25
|
+
import { narrowEnv } from './trial.js';
|
|
24
26
|
|
|
25
27
|
const HERE = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
26
28
|
const CONTRACT_DIR = path.join(HERE, 'contracts');
|
|
@@ -36,8 +38,8 @@ function copyDir(src, dst) {
|
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
export function loadContracts(skillName) {
|
|
40
|
-
const file = path.join(
|
|
41
|
+
export function loadContracts(skillName, dir = CONTRACT_DIR) {
|
|
42
|
+
const file = path.join(dir, `${skillName}.json`);
|
|
41
43
|
if (!fs.existsSync(file)) return null;
|
|
42
44
|
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
43
45
|
}
|
|
@@ -111,8 +113,10 @@ function runTask({ bin, model, task, skillDir, skillName, timeout }) {
|
|
|
111
113
|
if (model) args.push('--model', model);
|
|
112
114
|
|
|
113
115
|
return new Promise((resolve) => {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
// Not through a shell: cmd.exe would re-split the prompt on spaces (see spawn.js).
|
|
117
|
+
const launch = launchSpec(bin, args, 'claude');
|
|
118
|
+
const child = spawn(launch.cmd, launch.args, {
|
|
119
|
+
cwd: tmp, shell: launch.shell, env: narrowEnv(), windowsHide: true, stdio: 'ignore',
|
|
116
120
|
});
|
|
117
121
|
const timer = setTimeout(() => {
|
|
118
122
|
try {
|
|
@@ -128,8 +132,8 @@ function runTask({ bin, model, task, skillDir, skillName, timeout }) {
|
|
|
128
132
|
});
|
|
129
133
|
}
|
|
130
134
|
|
|
131
|
-
export async function runGain({ bin, skillDir, skillName, model, timeout = 600, onProgress }) {
|
|
132
|
-
const spec = loadContracts(skillName);
|
|
135
|
+
export async function runGain({ bin, skillDir, skillName, model, timeout = 600, onProgress, contractDir }) {
|
|
136
|
+
const spec = loadContracts(skillName, contractDir);
|
|
133
137
|
const hasContracts = spec?.contracts?.length || spec?.tasks?.some((t) => t.contracts?.length);
|
|
134
138
|
if (!spec || !hasContracts || !spec.tasks?.length) {
|
|
135
139
|
return { measurable: false, reasonKey: 'gain.noContracts' };
|
package/lib/i18n.js
CHANGED
|
@@ -124,6 +124,10 @@ const MESSAGES = {
|
|
|
124
124
|
|
|
125
125
|
层 0 总是最先跑:它免费,能在你花钱之前筛掉大部分问题。`,
|
|
126
126
|
modelRemap: '后端 {host} 会静默改写 Anthropic 模型名,"{model}" 测的不是你指定的模型;请传该后端自己的模型名',
|
|
127
|
+
hostMissing: '没有找到宿主 {host}(PATH 里没有 {bin})。{how};也可以重新运行并选择其他宿主(Claude Code、CodeBuddy、Codewhale、Codex)。',
|
|
128
|
+
installCodebuddy: '安装方法:npm install -g @tencent-ai/codebuddy-code',
|
|
129
|
+
installCodewhale: '安装方法:从官方发布页下载单文件 exe,改名为 codewhale.exe 并放进 PATH',
|
|
130
|
+
installClaude: '安装方法:npm install -g @anthropic-ai/claude-code',
|
|
127
131
|
|
|
128
132
|
// ---- report: shared ----
|
|
129
133
|
'report.path': '路径:',
|
|
@@ -364,6 +368,10 @@ Layers
|
|
|
364
368
|
|
|
365
369
|
Layer 0 always runs first: it is free, and it disqualifies most problems before you pay.`,
|
|
366
370
|
modelRemap: 'backend {host} remaps Anthropic model names, so "{model}" would test a different model; pass that backend\'s own model id',
|
|
371
|
+
hostMissing: 'Host {host} was not found ({bin} is not on PATH). {how}, or run again and pick another host (Claude Code, CodeBuddy, Codewhale, Codex).',
|
|
372
|
+
installCodebuddy: 'To install: npm install -g @tencent-ai/codebuddy-code',
|
|
373
|
+
installCodewhale: 'To install: download the single exe from the official release page, rename it codewhale.exe and put it on PATH',
|
|
374
|
+
installClaude: 'To install: npm install -g @anthropic-ai/claude-code',
|
|
367
375
|
|
|
368
376
|
'report.path': 'Path: ',
|
|
369
377
|
'report.skillMd': 'SKILL.md: {chars} chars ≈ {tokens} tokens; description {desc} chars',
|
|
@@ -580,6 +588,10 @@ Layer 0 always runs first: it is free, and it disqualifies most problems before
|
|
|
580
588
|
inputEnded: '入力が終了しました',
|
|
581
589
|
err: 'エラー:{msg}',
|
|
582
590
|
modelRemap: 'バックエンド {host} は Anthropic のモデル名を黙って書き換えるため、「{model}」では指定したモデルをテストできません。そのバックエンド自身のモデル名を指定してください',
|
|
591
|
+
hostMissing: 'ホスト {host} が見つかりません(PATH に {bin} がありません)。{how}。または、もう一度実行して別のホスト(Claude Code、CodeBuddy、Codewhale、Codex)を選んでください。',
|
|
592
|
+
installCodebuddy: 'インストール方法:npm install -g @tencent-ai/codebuddy-code',
|
|
593
|
+
installCodewhale: 'インストール方法:公式リリースページから単体 exe をダウンロードし、codewhale.exe に改名して PATH に置いてください',
|
|
594
|
+
installClaude: 'インストール方法:npm install -g @anthropic-ai/claude-code',
|
|
583
595
|
badSource: 'このアドレスを解釈できません:{spec}(ローカルディレクトリか owner/repo を指定してください)',
|
|
584
596
|
noSkillUnder: '{target} の下に SKILL.md が見つかりません',
|
|
585
597
|
resumeUsage: '使い方:skillgauntlet resume <実行ディレクトリ> [--model M] [--runs N] [--max-explore N]',
|
|
@@ -741,7 +753,8 @@ export function t(key, vars = {}) {
|
|
|
741
753
|
|
|
742
754
|
// An error either carries a message key (translated) or is plain text from the runtime.
|
|
743
755
|
export function errorText(err) {
|
|
744
|
-
|
|
756
|
+
if (!err.messageKey) return err.message;
|
|
757
|
+
return t(err.messageKey, err.howKey ? { ...err.vars, how: t(err.howKey) } : err.vars);
|
|
745
758
|
}
|
|
746
759
|
|
|
747
760
|
// Comma-separated word list from the table, e.g. the accepted yes answers.
|
package/lib/trial.js
CHANGED
|
@@ -80,6 +80,29 @@ export function resolveBin(name) {
|
|
|
80
80
|
|| name;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// Whether a host CLI can be found on PATH at all. resolveBin falls back to the bare name, which on Windows
|
|
84
|
+
// ends in cmd.exe's "not recognized" (or a spawn ENOENT) deep inside a trial; this asks up front.
|
|
85
|
+
export function binInstalled(name) {
|
|
86
|
+
try {
|
|
87
|
+
execFileSync(process.platform === 'win32' ? 'where' : 'which', [name], { stdio: 'ignore' });
|
|
88
|
+
return true;
|
|
89
|
+
} catch { return false; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const HOST_NAME = { 'claude-code': 'Claude Code', codebuddy: 'CodeBuddy', codewhale: 'Codewhale' };
|
|
93
|
+
const INSTALL_HINT = { 'claude-code': 'installClaude', codebuddy: 'installCodebuddy', codewhale: 'installCodewhale' };
|
|
94
|
+
|
|
95
|
+
// Throws an error with a translatable message when the host's CLI is not installed.
|
|
96
|
+
export function requireHost(host, probe = binInstalled) {
|
|
97
|
+
const spec = HOSTS[host];
|
|
98
|
+
if (!spec || probe(spec.bin)) return;
|
|
99
|
+
const err = new Error(`host ${host} not found: ${spec.bin} is not on PATH`);
|
|
100
|
+
err.messageKey = 'hostMissing';
|
|
101
|
+
err.vars = { host: HOST_NAME[host] || host, bin: spec.bin };
|
|
102
|
+
err.howKey = INSTALL_HINT[host];
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
|
|
83
106
|
// Variables a host (the Claude desktop app, the Agent SDK, an outer Claude Code session) sets for its own
|
|
84
107
|
// child processes. A trial inherits them otherwise, and they change what the model under test sees: under
|
|
85
108
|
// the desktop app the CLI adds Artifact, ArtifactComments and ArtifactData to the tool list, and the model
|
|
@@ -100,6 +123,25 @@ export function trialEnv(env = process.env) {
|
|
|
100
123
|
return out;
|
|
101
124
|
}
|
|
102
125
|
|
|
126
|
+
// An allow-list environment for runs where the model executes code (the gain layer): the host CLI gets what
|
|
127
|
+
// it needs to start and authenticate and nothing else, so an unrelated token in the user's shell is not
|
|
128
|
+
// reachable from a task. Names match case-insensitively (Windows spells it Path).
|
|
129
|
+
const ENV_ALLOW = new Set(['PATH', 'HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA',
|
|
130
|
+
'PROGRAMDATA', 'PROGRAMFILES', 'PROGRAMFILES(X86)', 'SYSTEMROOT', 'SYSTEMDRIVE', 'WINDIR', 'COMSPEC', 'PATHEXT',
|
|
131
|
+
'TEMP', 'TMP', 'TMPDIR', 'USER', 'USERNAME', 'LOGNAME', 'SHELL', 'LANG', 'LC_ALL', 'TERM', 'TZ', 'OS',
|
|
132
|
+
'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'ALL_PROXY',
|
|
133
|
+
'SSL_CERT_FILE', 'NODE_EXTRA_CA_CERTS', 'CLOUD_ML_REGION', 'DEEPSEEK_API_KEY']);
|
|
134
|
+
const ENV_ALLOW_PREFIX = ['ANTHROPIC_', 'AWS_', 'GOOGLE_', 'CODEBUDDY_'];
|
|
135
|
+
|
|
136
|
+
export function narrowEnv(env = process.env) {
|
|
137
|
+
const out = {};
|
|
138
|
+
for (const [k, v] of Object.entries(trialEnv(env))) {
|
|
139
|
+
const u = k.toUpperCase();
|
|
140
|
+
if (ENV_ALLOW.has(u) || ENV_ALLOW_PREFIX.some((p) => u.startsWith(p)) || HOST_ENV_KEEP.has(k)) out[k] = v;
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
103
145
|
function backendHost() {
|
|
104
146
|
const url = process.env.ANTHROPIC_BASE_URL;
|
|
105
147
|
if (!url) return 'anthropic';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillgauntlet",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
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": {
|