skillgauntlet 1.3.2 → 1.3.4

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.
@@ -20,7 +20,10 @@ import { renderResult } from '../lib/chart.js';
20
20
  import { makeProgress } from '../lib/progress.js';
21
21
  import { setLang, detectLang, errorText, t } from '../lib/i18n.js';
22
22
  import { resolveBin, backend, backendFor, usesOwnDeepseek, setNativeModels, HOSTS, checkModel, draftQueries, draftQuality, runTrial, requireHost, pingHost, setHostBin, binInstalled } from '../lib/trial.js';
23
- import { HOST_CLI, searchHost, installCommand, installHost } from '../lib/findhost.js';
23
+ import { HOST_CLI, searchHost, installCommand, installHost, LOGIN, loginCommand } from '../lib/findhost.js';
24
+ import { launchSpec } from '../lib/spawn.js';
25
+ import { ensureTool } from '../lib/github.js';
26
+ import { spawnSync as runLogin } from 'node:child_process';
24
27
  import { runGain } from '../lib/gain.js';
25
28
  import { ERROR_STREAK_STOP, executeTrials, finishRun, resumeRun } from '../lib/execute.js';
26
29
  import { cmdSubmit } from '../lib/submit.js';
@@ -145,12 +148,40 @@ function modelsOf(host, label) {
145
148
  }
146
149
  const HOST_LABEL = { 'claude-code': 'Claude Code', codebuddy: 'CodeBuddy', codewhale: 'Codewhale' };
147
150
 
151
+ // The host did not answer its sign-in check. Show why, and let the user sign in with one press of enter: the host's own
152
+ // login runs on this terminal, then the check is made again. True when a sign-in was run (the caller checks again).
153
+ async function offerLogin(host, bin, err) {
154
+ const label = HOST_LABEL[host] || host;
155
+ const spec = LOGIN[host];
156
+ const cmd = loginCommand(host, path.basename(bin).replace(/\.(cmd|exe|bat)$/i, ''));
157
+ say(`\n${errorText(err)}`);
158
+ const pick = await choose(t('loginQ'), [
159
+ { label: t('loginNow', { host: label }), hint: cmd ? t('loginRuns', { cmd }) : '', value: 'now' },
160
+ { label: t('loginSkip'), value: 'skip' },
161
+ ], 1);
162
+ if (pick !== 'now') return false;
163
+ say(`\n${t(spec.hint)}\n`);
164
+ closeUi(); // hand the terminal to the host's own login
165
+ const launch = launchSpec(bin, spec.args, HOSTS[host].bin);
166
+ const r = runLogin(launch.cmd, launch.args, { stdio: 'inherit', shell: launch.shell });
167
+ say('');
168
+ return !r.error;
169
+ }
170
+
148
171
  async function runTriggerLayer({ skillDir, skillName, depth, model, host = 'claude-code' }) {
149
172
  requireHost(host);
150
173
  const bin = resolveBin(HOSTS[host].bin);
151
- if (!hostChecked.has(host)) {
152
- withTicker(waitText(t('hostPing', { host: HOST_LABEL[host] || host })), () => pingHost(host, { bin, model }));
153
- hostChecked.add(host);
174
+ while (!hostChecked.has(host)) {
175
+ try {
176
+ withTicker(waitText(t('hostPing', { host: HOST_LABEL[host] || host })), () => pingHost(host, { bin, model }));
177
+ hostChecked.add(host);
178
+ } catch (err) {
179
+ // Not signed in is the common reason: offer the sign-in right here, then check again.
180
+ if (err.messageKey !== 'hostNotReady' || !LOGIN[host] || !hasTerminal() || !(await offerLogin(host, bin, err))) {
181
+ if (err.messageKey === 'hostNotReady') err.hostDown = true;
182
+ throw err;
183
+ }
184
+ }
154
185
  }
155
186
  const cacheFile = cacheFileFor(skillDir, depth);
156
187
  const want = { perClause: depth.perClause, negatives: depth.negatives, skillFingerprint: fingerprint(skillDir) };
@@ -345,6 +376,8 @@ async function pickRemote() {
345
376
  say(` ${t('needRepo')}`);
346
377
  continue;
347
378
  }
379
+ // cloning needs git; a machine without it is offered the install here, not given a raw error
380
+ if (hasTerminal() && !(await ensureTool('git', { pick: choose, out: say }))) return [];
348
381
  let resolved;
349
382
  try {
350
383
  resolved = withTicker(waitText(t('waitCloning'), { slowAfter: 600 }), () => resolveSpec(parsed.repo));
@@ -600,14 +633,16 @@ async function interactive() {
600
633
  let probe = null;
601
634
 
602
635
  if (host === 'claude' || host === 'codebuddy' || host === 'codewhale') {
636
+ let hostDown = false; // the host did not answer the sign-in check: nothing else will work either
603
637
  try {
604
638
  trial = await runTriggerLayer({
605
639
  skillDir: target.dir, skillName: target.name, depth, model, host: { claude: 'claude-code', codebuddy: 'codebuddy', codewhale: 'codewhale' }[host],
606
640
  });
607
641
  } catch (err) {
608
642
  say(` ${t('trialFail', { err: errorText(err) })}`);
643
+ hostDown = Boolean(err.hostDown) || err.messageKey === 'hostNotReady';
609
644
  }
610
- if (host === 'claude' && depth.layers.includes('gain')) {
645
+ if (host === 'claude' && depth.layers.includes('gain') && !hostDown) {
611
646
  say(` ${t('gainNote')}`);
612
647
  const g = await runGain({
613
648
  bin: resolveBin('claude'),
package/lib/findhost.js CHANGED
@@ -81,3 +81,12 @@ export function installHost(host, run = (cmd) => execSync(cmd, { stdio: 'inherit
81
81
  if (!cmd) throw new Error('no installer for this host');
82
82
  try { run(cmd); } catch (err) { throw new Error(String(err.message || err).split(/\r?\n/)[0]); }
83
83
  }
84
+
85
+ // How to sign in to a host from this terminal. Claude Code and Codewhale have a login command; CodeBuddy has none, its
86
+ // sign-in is the /login command inside its own interface, so its CLI is opened and the user is told what to type.
87
+ export const LOGIN = {
88
+ 'claude-code': { args: ['auth', 'login'], hint: 'loginHintBrowser' },
89
+ codewhale: { args: ['login'], hint: 'loginHintBrowser' },
90
+ codebuddy: { args: [], hint: 'loginHintCodebuddy' },
91
+ };
92
+ export const loginCommand = (host, bin) => (LOGIN[host] ? `${bin} ${LOGIN[host].args.join(' ')}`.trim() : null);
package/lib/github.js ADDED
@@ -0,0 +1,175 @@
1
+ // What a machine with nothing installed needs before a result can be submitted, and what happens after the pull request
2
+ // is opened: git and the GitHub CLI (installed when the user presses enter), a GitHub sign-in (the CLI's own browser
3
+ // flow), the one-time CLA the bot asks for, and the wait for the merge. Every outside call goes through `exec`, `pick`
4
+ // and `run`, so the whole thing is tested with fakes.
5
+ import fs from 'node:fs';
6
+ import { execFileSync, spawnSync } from 'node:child_process';
7
+ import { t } from './i18n.js';
8
+
9
+ const quiet = (cmd, args) => execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, timeout: 120000 });
10
+ // a long-running, interactive command on the user's terminal (an installer, a browser sign-in)
11
+ const live = (cmd, args) => spawnSync(cmd, args, { stdio: 'inherit', shell: process.platform === 'win32', windowsHide: false });
12
+
13
+ export const TOOLS = {
14
+ git: { url: 'https://git-scm.com/downloads', pkg: { winget: 'Git.Git', choco: 'git', scoop: 'git', brew: 'git', apt: 'git', dnf: 'git', pacman: 'git', zypper: 'git', apk: 'git' } },
15
+ gh: { url: 'https://cli.github.com', pkg: { winget: 'GitHub.cli', choco: 'gh', scoop: 'gh', brew: 'gh', apt: 'gh', dnf: 'gh', pacman: 'github-cli', zypper: 'gh', apk: 'github-cli' } },
16
+ };
17
+
18
+ // The package managers to look for on each system, in the order they are preferred.
19
+ export const MANAGERS = { win32: ['winget', 'choco', 'scoop'], darwin: ['brew'], linux: ['apt-get', 'dnf', 'pacman', 'zypper', 'apk', 'brew'] };
20
+
21
+ // The steps that install `pkg` with a manager. The system-wide ones need root, so they are run through sudo unless the
22
+ // user already is root; a `sudo` prompt for the password then appears on the user's own terminal.
23
+ function stepsFor(manager, pkg, id, asRoot) {
24
+ const root = (args) => (asRoot ? args : ['sudo', ...args]);
25
+ switch (manager) {
26
+ case 'winget': return [['winget', 'install', '--id', id, '-e', '--accept-source-agreements', '--accept-package-agreements']];
27
+ case 'choco': return [['choco', 'install', pkg, '-y']];
28
+ case 'scoop': return [['scoop', 'install', pkg]];
29
+ case 'brew': return [['brew', 'install', pkg]];
30
+ case 'apt-get': return [root(['apt-get', 'update']), root(['apt-get', 'install', '-y', pkg])];
31
+ case 'dnf': return [root(['dnf', 'install', '-y', pkg])];
32
+ case 'pacman': return [root(['pacman', '-S', '--noconfirm', pkg])];
33
+ case 'zypper': return [root(['zypper', '--non-interactive', 'install', pkg])];
34
+ case 'apk': return [root(['apk', 'add', pkg])];
35
+ default: return null;
36
+ }
37
+ }
38
+
39
+ // What this system can run to install a tool, or null when it cannot be done here. `ctx.have(cmd)` says whether a command
40
+ // exists; `ctx.uid` is the user id (0 is root). The first package manager found wins.
41
+ export function installPlan(tool, platform = process.platform, ctx = {}) {
42
+ const spec = TOOLS[tool];
43
+ if (!spec) return null;
44
+ const have = ctx.have || (() => false);
45
+ const asRoot = ctx.uid === 0;
46
+ const manager = (MANAGERS[platform] || []).find((m) => have(m));
47
+ if (!manager) return null;
48
+ const key = manager === 'apt-get' ? 'apt' : manager;
49
+ const pkg = spec.pkg[key];
50
+ if (!pkg) return null;
51
+ const sudoNeeded = !['winget', 'choco', 'scoop', 'brew'].includes(manager) && !asRoot;
52
+ if (sudoNeeded && !have('sudo')) return null; // neither root nor sudo: nothing that can be done from here
53
+ return { manager, steps: stepsFor(manager, pkg, pkg, asRoot), sudo: sudoNeeded };
54
+ }
55
+
56
+ // Where a fresh install lands when the running process was started before it existed (its PATH is out of date).
57
+ export function knownDirs(platform = process.platform) {
58
+ if (platform === 'win32') return ['C:\\Program Files\\Git\\cmd', 'C:\\Program Files\\GitHub CLI', `${process.env.LOCALAPPDATA || ''}\\Programs\\Git\\cmd`];
59
+ if (platform === 'darwin') return ['/opt/homebrew/bin', '/usr/local/bin'];
60
+ return [];
61
+ }
62
+
63
+ export function addKnownDirsToPath(platform = process.platform, exists = fs.existsSync, env = process.env) {
64
+ const key = Object.keys(env).find((k) => k.toLowerCase() === 'path') || 'PATH';
65
+ const sep = platform === 'win32' ? ';' : ':'; // the separator of the system being described, not of the one running
66
+ const have = String(env[key] || '').split(sep).map((d) => d.toLowerCase());
67
+ const add = knownDirs(platform).filter((d) => d && exists(d) && !have.includes(d.toLowerCase()));
68
+ if (add.length) env[key] = `${add.join(sep)}${sep}${env[key] || ''}`;
69
+ return add;
70
+ }
71
+
72
+ export const has = (tool, exec = quiet) => { try { exec(tool, ['--version']); return true; } catch { return false; } };
73
+
74
+ const line = (plan) => plan.steps.map((st) => st.join(' ')).join(' && ');
75
+
76
+ // One tool: present, or offered for install with enter. True when it is usable afterwards.
77
+ export async function ensureTool(tool, { exec = quiet, run = live, pick, out, platform = process.platform, exists = fs.existsSync, ctx = {} } = {}) {
78
+ if (has(tool, exec)) return true;
79
+ addKnownDirsToPath(platform, exists);
80
+ if (has(tool, exec)) return true;
81
+ const plan = installPlan(tool, platform, { have: (c) => has(c, exec), uid: typeof process.getuid === 'function' ? process.getuid() : undefined, ...ctx });
82
+ out(`\n${t('toolMissing', { tool })}`);
83
+ if (!plan) {
84
+ out(t('toolNoAuto', { tool, url: TOOLS[tool].url }));
85
+ return false;
86
+ }
87
+ if (plan.sudo) out(t('toolSudo'));
88
+ const choice = await pick(t('toolQ', { tool }), [
89
+ { label: t('toolInstallNow', { tool }), hint: t('toolRuns', { cmd: line(plan) }), value: 'now' },
90
+ { label: t('toolInstallSelf'), value: 'self' },
91
+ ], 1);
92
+ if (choice !== 'now') return false;
93
+ out(`\n${t('toolInstalling', { tool })}\n`);
94
+ for (const [cmd, ...args] of plan.steps) {
95
+ const r = run(cmd, args);
96
+ if (r?.error || (r && r.status !== 0 && r.status != null)) {
97
+ out(t('toolInstallFailed', { tool, url: TOOLS[tool].url }));
98
+ return false;
99
+ }
100
+ }
101
+ addKnownDirsToPath(platform, exists);
102
+ if (has(tool, exec)) {
103
+ out(t('toolInstalled', { tool }));
104
+ return true;
105
+ }
106
+ out(t('toolStillMissing', { tool }));
107
+ return false;
108
+ }
109
+
110
+ export const signedIn = (exec = quiet) => { try { exec('gh', ['auth', 'status']); return true; } catch { return false; } };
111
+
112
+ // The GitHub sign-in: the CLI's own browser flow (it shows a one-time code and opens the browser after enter).
113
+ export async function ensureSignedIn({ exec = quiet, run = live, pick, out } = {}) {
114
+ while (!signedIn(exec)) {
115
+ out(`\n${t('ghLoginNeeded')}`);
116
+ const choice = await pick(t('ghLoginQ'), [
117
+ { label: t('ghLoginNow'), hint: t('toolRuns', { cmd: 'gh auth login -h github.com -p https -w' }), value: 'now' },
118
+ { label: t('ghLoginSkip'), value: 'skip' },
119
+ ], 1);
120
+ if (choice !== 'now') return false;
121
+ out(`\n${t('ghLoginHint')}\n`);
122
+ run('gh', ['auth', 'login', '-h', 'github.com', '-p', 'https', '-w']);
123
+ }
124
+ return true;
125
+ }
126
+
127
+ // Everything submit needs on a machine with nothing on it. False when the user chose to stop or it cannot be done here.
128
+ export async function ensureGithub(env = {}) {
129
+ for (const tool of ['git', 'gh']) if (!(await ensureTool(tool, env))) return false;
130
+ return ensureSignedIn(env);
131
+ }
132
+
133
+ // ------------------------------------------------------------------------------------------------ after the PR is opened
134
+
135
+ // The state of the CLA bot's check among a pull request's checks, or null when it has not appeared yet.
136
+ export function claState(rollup) {
137
+ const e = (rollup || []).find((c) => (c.context || c.name) === 'license/cla');
138
+ return e ? String(e.state || e.conclusion || '').toUpperCase() : null;
139
+ }
140
+
141
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
142
+
143
+ // Show where the pull request stands until it is merged, needs a person, or the wait runs out. The first time the CLA
144
+ // check is not green the contributor is given the link and asked to come back with enter: signing is a web page that
145
+ // belongs to them and cannot be done for them.
146
+ export async function watchPullRequest(url, { exec = quiet, pick, out, upstream = 'SkillGauntlet/SkillGauntlet', wait = sleep, every = 10000, rounds = 60 } = {}) {
147
+ const number = String(url).split('/').pop();
148
+ let askedCla = false;
149
+ let shown = '';
150
+ out(`\n${t('prWaiting')}`);
151
+ for (let i = 0; i < rounds; i += 1) {
152
+ let view;
153
+ try { view = JSON.parse(exec('gh', ['pr', 'view', url, '--json', 'state,mergedAt,labels,statusCheckRollup'])); } catch { view = null; }
154
+ if (view) {
155
+ if (view.state === 'MERGED' || view.mergedAt) { out(`\n${t('prMerged', { url })}`); return 'merged'; }
156
+ if (view.state === 'CLOSED') { out(`\n${t('prClosed', { url })}`); return 'closed'; }
157
+ if ((view.labels || []).some((l) => l.name === 'needs-review')) { out(`\n${t('prNeedsReview', { url })}`); return 'review'; }
158
+ const cla = claState(view.statusCheckRollup);
159
+ if (cla && cla !== 'SUCCESS' && !askedCla) {
160
+ askedCla = true;
161
+ out(`\n${t('prCla', { url: `https://cla-assistant.io/${upstream}?pullRequest=${number}` })}`);
162
+ const choice = await pick(t('prClaQ'), [
163
+ { label: t('prClaDone'), value: 'wait' },
164
+ { label: t('prLater'), value: 'later' },
165
+ ], 1);
166
+ if (choice === 'later') { out(t('prLaterNote', { url })); return 'later'; }
167
+ }
168
+ const status = cla ? `${t('prStepCla')} ${cla === 'SUCCESS' ? '✓' : '…'}` : t('prStepChecks');
169
+ if (status !== shown) { out(` ${status}`); shown = status; }
170
+ }
171
+ await wait(every);
172
+ }
173
+ out(`\n${t('prTimeout', { url, m: Math.round((rounds * every) / 60000) })}`);
174
+ return 'timeout';
175
+ }
package/lib/i18n.js CHANGED
@@ -95,7 +95,7 @@ const MESSAGES = {
95
95
  staticDone: '静态层:{verdicts}',
96
96
  drafting: '正在出题(一次模型调用,可能要几分钟)…',
97
97
  hostPing: '正在确认 {host} 已登录…',
98
- hostNotReady: '{host} 没有响应,多半是还没登录。请先在它的命令行里输入 /login,登录后再重新运行。({why})',
98
+ hostNotReady: '{host} 没能完成一次试调用:{why}\n如果是没登录,请先在它的命令行里输入 /login,登录后再重新运行。',
99
99
  drafted: '出了 {q} 道题,{c} 条触发条款(已缓存,下次直接复用)',
100
100
  queriesReused: '复用缓存的 {n} 道题({file}),省一次出题调用',
101
101
  negativesChecked: '{n} 条负样本都填出了可核实的借用词',
@@ -133,6 +133,40 @@ const MESSAGES = {
133
133
  'chart.wrongTitle': '不该触发却触发的 {n} 次',
134
134
  modelAccountHint: '你的账号现在可用',
135
135
  modelCatalogHint: 'CodeBuddy 目录里有,账号不一定能用',
136
+ loginQ: '要现在登录吗?',
137
+ loginNow: '现在登录 {host}(按回车)',
138
+ loginRuns: '会运行 {cmd}',
139
+ loginSkip: '先不登录,跳过触发层',
140
+ loginHintBrowser: '接下来会运行宿主自己的登录命令,一般在浏览器里完成;登录结束后会自动回到这里并重新检查。',
141
+ loginHintCodebuddy: '接下来会打开 CodeBuddy 的界面:请在里面输入 /login 完成登录,再输入 /exit 退出,就会回到这里并重新检查。',
142
+ toolMissing: '这台电脑上没有 {tool},提交结果需要它。',
143
+ toolNoAuto: '这个系统没法自动安装 {tool},请按 {url} 的说明装好,再重新运行。',
144
+ toolQ: '现在安装 {tool} 吗?',
145
+ toolInstallNow: '现在安装 {tool}(按回车)',
146
+ toolRuns: '会运行 {cmd}',
147
+ toolInstallSelf: '我自己装,稍后重新运行',
148
+ toolInstalling: '正在安装 {tool},输出就在下面,请等它结束…',
149
+ toolInstalled: '{tool} 装好了。',
150
+ toolInstallFailed: '{tool} 没装成功。请按 {url} 的说明自己装好,再重新运行。',
151
+ toolStillMissing: '装完之后还是找不到 {tool},请关掉终端重新打开,再重新运行。',
152
+ ghLoginNeeded: '还没登录 GitHub。提交结果要用你的 GitHub 账号开一个公开的 PR。',
153
+ ghLoginQ: '现在登录 GitHub 吗?',
154
+ ghLoginNow: '现在登录 GitHub(按回车)',
155
+ ghLoginSkip: '先不登录,只保存结果',
156
+ ghLoginHint: '接下来会显示一个一次性代码并打开浏览器:在网页里输入代码、点授权,终端会自动继续。',
157
+ prWaiting: '正在等 PR 的检查和合并…',
158
+ prStepCla: 'CLA 签署',
159
+ prStepChecks: '自动检查进行中',
160
+ prCla: '这是你第一次提交,需要签一次贡献者协议(CLA)。请在浏览器里打开:\n {url}\n用 GitHub 账号登录并点同意即可。',
161
+ prClaQ: '签好了吗?',
162
+ prClaDone: '我已经签好了(按回车继续等)',
163
+ prLater: '稍后再看(PR 已开,不影响)',
164
+ prLaterNote: 'PR 还在:{url}。签好 CLA 后会自动检查并合并,之后随时在浏览器里看。',
165
+ prMerged: '已合并 ✓ 这是你的提交记录:{url}',
166
+ prClosed: '这个 PR 已被关闭:{url}',
167
+ prNeedsReview: '没有自动合并,已转给维护者审核(常见原因:GitHub 账号注册不满 30 天,或今天已提交超过 5 条)。你不需要再做什么:{url}',
168
+ prTimeout: '等了 {m} 分钟还没合并,PR 还在:{url},之后随时在浏览器里看。',
169
+ toolSudo: '这一步要用管理员权限安装:下面可能会让你输入这台电脑的登录密码(输入时不会显示字符,输完按回车)。',
136
170
  modelUnknown: '{host} 的模型清单里没有“{model}”。它现在支持:{list}。请重新选一个,或选“默认模型”。',
137
171
  updateAvail: '有新版本 {latest}(你现在用的是 {current})。',
138
172
  updateQ: '要更新吗?',
@@ -493,7 +527,7 @@ const MESSAGES = {
493
527
  staticDone: 'Static: {verdicts}',
494
528
  drafting: 'Drafting questions (one model call, may take a few minutes)…',
495
529
  hostPing: 'Checking that {host} is signed in…',
496
- hostNotReady: '{host} did not answer, most likely because it is not signed in. Run /login in its CLI first, then run this again. ({why})',
530
+ hostNotReady: '{host} could not complete a test call: {why}\nIf it is not signed in, run /login in its CLI first, then run this again.',
497
531
  drafted: '{q} queries over {c} trigger clauses (cached for reuse)',
498
532
  queriesReused: 'reusing {n} cached questions ({file}), one drafting call saved',
499
533
  negativesChecked: 'all {n} negatives name verifiable borrowed wording',
@@ -531,6 +565,40 @@ const MESSAGES = {
531
565
  'chart.wrongTitle': '{n} times it triggered when it should not have',
532
566
  modelAccountHint: 'usable with your account now',
533
567
  modelCatalogHint: 'in CodeBuddy\'s catalog; your account may not have it',
568
+ loginQ: 'Sign in now?',
569
+ loginNow: 'Sign in to {host} now (press enter)',
570
+ loginRuns: 'runs {cmd}',
571
+ loginSkip: 'Not now, skip the trigger layer',
572
+ loginHintBrowser: 'The host\'s own login command runs next, usually finishing in your browser; when it ends you come back here and the check is made again.',
573
+ loginHintCodebuddy: 'CodeBuddy\'s interface opens next: type /login there to sign in, then /exit to leave; you come back here and the check is made again.',
574
+ toolMissing: '{tool} is not installed on this computer, and submitting needs it.',
575
+ toolNoAuto: 'This system cannot install {tool} automatically; install it from {url}, then run this again.',
576
+ toolQ: 'Install {tool} now?',
577
+ toolInstallNow: 'Install {tool} now (press enter)',
578
+ toolRuns: 'runs {cmd}',
579
+ toolInstallSelf: 'I will install it myself and run this again',
580
+ toolInstalling: 'Installing {tool}; its output is shown below, please wait for it to finish…',
581
+ toolInstalled: '{tool} is installed.',
582
+ toolInstallFailed: '{tool} did not install. Install it yourself from {url}, then run this again.',
583
+ toolStillMissing: '{tool} still was not found after installing. Close this terminal, open a new one and run this again.',
584
+ ghLoginNeeded: 'You are not signed in to GitHub. Submitting opens a public pull request from your GitHub account.',
585
+ ghLoginQ: 'Sign in to GitHub now?',
586
+ ghLoginNow: 'Sign in to GitHub now (press enter)',
587
+ ghLoginSkip: 'Not now, just keep the results',
588
+ ghLoginHint: 'A one-time code is shown next and your browser opens: type the code there and authorize, and this terminal carries on by itself.',
589
+ prWaiting: 'Waiting for the pull request\'s checks and merge…',
590
+ prStepCla: 'CLA signed',
591
+ prStepChecks: 'automatic checks running',
592
+ prCla: 'This is your first submission, so you sign the contributor agreement (CLA) once. Open this in your browser:\n {url}\nSign in with your GitHub account and agree.',
593
+ prClaQ: 'Signed?',
594
+ prClaDone: 'I have signed it (press enter to keep waiting)',
595
+ prLater: 'Look later (the pull request is open either way)',
596
+ prLaterNote: 'The pull request is still there: {url}. Once the CLA is signed it is checked and merged automatically; look any time in the browser.',
597
+ prMerged: 'Merged ✓ Here is your submission record: {url}',
598
+ prClosed: 'This pull request was closed: {url}',
599
+ prNeedsReview: 'It was not merged automatically and has gone to a maintainer for review (common reasons: the GitHub account is under 30 days old, or more than 5 submissions today). You need do nothing more: {url}',
600
+ prTimeout: 'Waited {m} minutes and it is not merged yet. The pull request is still there: {url}; look any time in the browser.',
601
+ toolSudo: 'This install needs administrator rights: you may be asked for this computer\'s password below (nothing is shown as you type; press enter when done).',
534
602
  modelUnknown: '{host} does not list a model called "{model}". It currently supports: {list}. Pick another, or choose the default model.',
535
603
  updateAvail: 'A newer version {latest} is available (you are on {current}).',
536
604
  updateQ: 'Update?',
@@ -886,7 +954,7 @@ Layer 0 always runs first: it is free, and it disqualifies most problems before
886
954
  staticDone: '静的チェック:{verdicts}',
887
955
  drafting: 'テスト用の質問を作成中(モデル呼び出し 1 回、数分かかることがあります)…',
888
956
  hostPing: '{host} にサインイン済みか確認中…',
889
- hostNotReady: '{host} から応答がありません。サインインしていない可能性が高いです。先にその CLI で /login を実行してから、もう一度実行してください。({why})',
957
+ hostNotReady: '{host} はテスト呼び出しを完了できませんでした:{why}\nサインインしていない場合は、先にその CLI で /login を実行してから、もう一度実行してください。',
890
958
  drafted: '{c} 件の発動条項について {q} 問を作成しました(キャッシュ済み、次回は再利用)',
891
959
  queriesReused: 'キャッシュ済みの {n} 問({file})を再利用、質問作成の呼び出しを 1 回節約',
892
960
  negativesChecked: '否定例 {n} 件すべてに検証可能な借用語があります',
@@ -977,6 +1045,40 @@ Layer 0 always runs first: it is free, and it disqualifies most problems before
977
1045
  'chart.wrongTitle': '発動すべきでないのに発動した {n} 回',
978
1046
  modelAccountHint: 'このアカウントで今使えます',
979
1047
  modelCatalogHint: 'CodeBuddy のカタログにあります(アカウントで使えるとは限りません)',
1048
+ loginQ: '今すぐサインインしますか?',
1049
+ loginNow: '{host} に今すぐサインイン(Enter)',
1050
+ loginRuns: '{cmd} を実行します',
1051
+ loginSkip: '今はしない(発動率の測定をスキップ)',
1052
+ loginHintBrowser: '次にホスト自身のログインコマンドを実行します(通常はブラウザで完了)。終わるとここに戻り、再度確認します。',
1053
+ loginHintCodebuddy: '次に CodeBuddy の画面が開きます。そこで /login と入力してサインインし、/exit で終了するとここに戻って再度確認します。',
1054
+ toolMissing: 'このコンピューターに {tool} がありません。結果の提出に必要です。',
1055
+ toolNoAuto: 'このシステムでは {tool} を自動インストールできません。{url} の手順でインストールしてから、もう一度実行してください。',
1056
+ toolQ: '今すぐ {tool} をインストールしますか?',
1057
+ toolInstallNow: '{tool} を今すぐインストール(Enter)',
1058
+ toolRuns: '{cmd} を実行します',
1059
+ toolInstallSelf: '自分でインストールして、あとでもう一度実行する',
1060
+ toolInstalling: '{tool} をインストール中です。出力は下に表示されます。終わるまでお待ちください…',
1061
+ toolInstalled: '{tool} をインストールしました。',
1062
+ toolInstallFailed: '{tool} をインストールできませんでした。{url} の手順で自分でインストールしてから、もう一度実行してください。',
1063
+ toolStillMissing: 'インストール後も {tool} が見つかりません。ターミナルを閉じて開き直し、もう一度実行してください。',
1064
+ ghLoginNeeded: 'GitHub にサインインしていません。提出はあなたの GitHub アカウントで公開プルリクエストを作成します。',
1065
+ ghLoginQ: '今すぐ GitHub にサインインしますか?',
1066
+ ghLoginNow: 'GitHub に今すぐサインイン(Enter)',
1067
+ ghLoginSkip: '今はしない(結果は保存のみ)',
1068
+ ghLoginHint: '次にワンタイムコードが表示されブラウザが開きます。そこでコードを入力して承認すると、このターミナルは自動で続行します。',
1069
+ prWaiting: 'プルリクエストのチェックとマージを待っています…',
1070
+ prStepCla: 'CLA 署名',
1071
+ prStepChecks: '自動チェック実行中',
1072
+ prCla: '初めての提出なので、貢献者契約(CLA)に一度だけ署名します。ブラウザで次を開いてください:\n {url}\nGitHub アカウントでサインインして同意します。',
1073
+ prClaQ: '署名しましたか?',
1074
+ prClaDone: '署名しました(Enter で待機を続ける)',
1075
+ prLater: '後で見る(プルリクエストは作成済みです)',
1076
+ prLaterNote: 'プルリクエストはそのまま残っています:{url}。CLA に署名すると自動でチェック・マージされます。いつでもブラウザで確認できます。',
1077
+ prMerged: 'マージされました ✓ あなたの提出記録:{url}',
1078
+ prClosed: 'このプルリクエストはクローズされました:{url}',
1079
+ prNeedsReview: '自動マージされず、メンテナーのレビューに回りました(主な理由:GitHub アカウントが作成 30 日未満、または本日 5 件を超える提出)。これ以上の操作は不要です:{url}',
1080
+ prTimeout: '{m} 分待ちましたがまだマージされていません。プルリクエストは残っています:{url}。いつでもブラウザで確認できます。',
1081
+ toolSudo: 'このインストールには管理者権限が必要です。このコンピューターのパスワードの入力を求められることがあります(入力中は何も表示されません。入力後に Enter)。',
980
1082
  modelUnknown: '{host} のモデル一覧に「{model}」はありません。現在対応:{list}。別のものを選ぶか、既定のモデルを選んでください。',
981
1083
  updateAvail: '新しいバージョン {latest} があります(現在は {current})。',
982
1084
  updateQ: '更新しますか?',
package/lib/submit.js CHANGED
@@ -7,7 +7,8 @@ import fs from 'node:fs';
7
7
  import os from 'node:os';
8
8
  import path from 'node:path';
9
9
  import { execFileSync } from 'node:child_process';
10
- import { say, confirm } from './ui.js';
10
+ import { say, confirm, choose, hasTerminal } from './ui.js';
11
+ import { ensureGithub, watchPullRequest } from './github.js';
11
12
  import { errorText, t } from './i18n.js';
12
13
  import {
13
14
  SubmitError, buildPackage, gitFacts, gradeOfSummary, normalizeRepo, skillShaAtCommit, slug, writePackage,
@@ -139,6 +140,9 @@ export async function cmdSubmit(args, { exec = defaultExec, verifyRemote = skill
139
140
  const opts = parseSubmitArgs(args);
140
141
  const runDir = path.resolve(opts.dir);
141
142
  const skillDir = opts['skill-dir'] ? path.resolve(opts['skill-dir']) : path.resolve(runDir, '..', '..', '..');
143
+ // On a terminal, make sure git, the GitHub CLI and a sign-in exist before anything else: a machine with nothing on
144
+ // it gets each offered with enter. Skipped when the contributor's name was given, and under test fakes.
145
+ if (hasTerminal() && !opts['github-user'] && exec === defaultExec) await ensureGithub({ pick: choose, out: say });
142
146
  const gh = ghState(exec);
143
147
  const contributor = opts['github-user'] || gh.user;
144
148
  if (!contributor) throw new SubmitError('submitNoUser');
@@ -196,6 +200,8 @@ export async function cmdSubmit(args, { exec = defaultExec, verifyRemote = skill
196
200
  const url = openPullRequest({ pkgDir, pkg, gh, exec });
197
201
  say(`\n${t('submitPrOpened', { url })}`);
198
202
  say(t('submitNext'));
203
+ // the CLA link and the wait for the merge, with enter to continue (under test fakes there is nothing to wait for)
204
+ if (hasTerminal() && exec === defaultExec) await watchPullRequest(url, { pick: choose, out: say });
199
205
  return { pkgDir, pkg, url };
200
206
  } catch (err) {
201
207
  say(`\n${t('submitGhFail', { msg: String(err.stderr || err.message).trim().split(/\r?\n/).slice(-3).join(' ') })}`);
package/lib/trial.js CHANGED
@@ -124,18 +124,27 @@ const firstLine = (text) => String(text || '').split(LINE_BREAK).map((l) => l.tr
124
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
125
  // stdout and its complaint on stderr (CodeBuddy does for a custom model without an endpoint url), so a clean exit
126
126
  // status proves nothing; an empty or unreadable stdout is the failure.
127
+ // A CLI that fails in --output-format json still prints one JSON result: the reason is its `result` text ("Not logged in
128
+ // · Please run /login"), which sits after a long block of usage fields, so the raw first line is no reason at all.
129
+ // null when the text is not JSON; otherwise the reason ('' when the result is not an error).
130
+ function jsonProblem(out) {
131
+ let parsed;
132
+ try { parsed = JSON.parse(out); } catch { return null; }
133
+ const last = Array.isArray(parsed) ? parsed.filter((m) => m?.type === 'result').pop() : parsed;
134
+ if (!last || typeof last !== 'object') return '';
135
+ if (!last.is_error) return '';
136
+ return firstLine(typeof last.result === 'string' ? last.result : JSON.stringify(last.result ?? '')) || firstLine(last.error) || 'the CLI reported an error';
137
+ }
138
+
127
139
  export function hostAnswerProblem(r, timeout) {
128
140
  if (r.error) return r.error.code === 'ETIMEDOUT' ? `no answer within ${timeout}s` : String(r.error.message).split(LINE_BREAK)[0];
129
141
  if (r.signal) return `no answer within ${timeout}s`;
130
142
  const out = String(r.stdout || '');
131
- if (r.status !== 0) return firstLine(r.stderr) || firstLine(out) || `exit status ${r.status}`;
143
+ const fromJson = out.trim() ? jsonProblem(out) : null;
144
+ if (r.status !== 0) return firstLine(r.stderr) || fromJson || firstLine(out).slice(0, 160) || `exit status ${r.status}`;
132
145
  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;
146
+ if (fromJson === null) return firstLine(out).slice(0, 160);
147
+ return fromJson || null;
139
148
  }
140
149
 
141
150
  export function pingHost(host, { bin, model = null, timeout = 90, spawn = spawnSync } = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillgauntlet",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
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": {