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.
@@ -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.message })}`);
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(CONTRACT_DIR, `${skillName}.json`);
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
- const child = spawn(bin, args, {
115
- cwd: tmp, shell: /\.(cmd|bat)$/i.test(bin), env: process.env, windowsHide: true,
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
- return err.messageKey ? t(err.messageKey, err.vars) : err.message;
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.0",
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": {