skillgauntlet 1.3.8 → 1.3.9

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/lib/i18n.js CHANGED
@@ -275,6 +275,7 @@ const MESSAGES = {
275
275
  submitPrOpened: 'PR 已开:{url}',
276
276
  submitNext: '接下来:机器人要求时签 CLA,然后等 PR 里的 CI 留言。留言会写通过,或者精确写出缺什么。合并由维护者操作。',
277
277
  submitGhFail: 'GitHub 步骤失败:{msg}',
278
+ submitRetry: '这看起来是 GitHub 的临时故障,已经打包好的内容不会丢。现在重试吗?',
278
279
  modelRemap: '后端 {host} 会静默改写 Anthropic 模型名,"{model}" 测的不是你指定的模型;请传该后端自己的模型名',
279
280
  hostMissing: '没有找到宿主 {host}(PATH 里没有 {bin})。{how};也可以重新运行并选择其他宿主(Claude Code、CodeBuddy、Codewhale、Codex)。',
280
281
  installCodebuddy: '安装方法:npm install -g @tencent-ai/codebuddy-code',
@@ -709,6 +710,7 @@ Layer 0 always runs first: it is free, and it disqualifies most problems before
709
710
  submitPrOpened: 'Pull request opened: {url}',
710
711
  submitNext: 'Next: sign the CLA if the bot asks, then wait for the CI comment on the pull request. It says pass, or exactly what is missing. A maintainer does the merging.',
711
712
  submitGhFail: 'GitHub step failed: {msg}',
713
+ submitRetry: 'This looks like a temporary GitHub problem, and nothing already packaged is lost. Retry now?',
712
714
  modelRemap: 'backend {host} remaps Anthropic model names, so "{model}" would test a different model; pass that backend\'s own model id',
713
715
  hostMissing: 'Host {host} was not found ({bin} is not on PATH). {how}, or run again and pick another host (Claude Code, CodeBuddy, Codewhale, Codex).',
714
716
  installCodebuddy: 'To install: npm install -g @tencent-ai/codebuddy-code',
@@ -1019,6 +1021,7 @@ Layer 0 always runs first: it is free, and it disqualifies most problems before
1019
1021
  submitPrOpened: 'プルリクエストを開きました:{url}',
1020
1022
  submitNext: '次に、ボットに求められたら CLA に署名し、プルリクエストへの CI コメントを待ちます。コメントには合格か、何が足りないかが正確に書かれます。マージはメンテナが行います。',
1021
1023
  submitGhFail: 'GitHub の手順で失敗しました:{msg}',
1024
+ submitRetry: 'GitHub の一時的な問題のようです。パッケージ済みの内容は失われません。今すぐ再試行しますか?',
1022
1025
  modelRemap: 'バックエンド {host} は Anthropic のモデル名を黙って書き換えるため、「{model}」では指定したモデルをテストできません。そのバックエンド自身のモデル名を指定してください',
1023
1026
  hostMissing: 'ホスト {host} が見つかりません(PATH に {bin} がありません)。{how}。または、もう一度実行して別のホスト(Claude Code、CodeBuddy、Codewhale、Codex)を選んでください。',
1024
1027
  installCodebuddy: 'インストール方法:npm install -g @tencent-ai/codebuddy-code',
package/lib/submit.js CHANGED
@@ -108,12 +108,42 @@ function cloneWithRetry(exec, url, dir, { tries = 5, waitMs = 3000 } = {}) {
108
108
  throw last;
109
109
  }
110
110
 
111
+ const defaultSleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
112
+
113
+ // Server-side hiccups and network drops are worth another try; auth or validation errors are not.
114
+ export function isTransient(err) {
115
+ const s = `${(err && err.stderr) || ''} ${(err && err.message) || ''}`;
116
+ return /HTTP 5\d\d|\b50[0-4]\b|timed? ?out|ETIMEDOUT|ECONNRESET|EAI_AGAIN|ENOTFOUND|unable to access|early EOF|RPC failed|Could not resolve host|rate limit/i.test(s);
117
+ }
118
+
119
+ // Run one GitHub step, retrying transient failures a few times with a growing pause.
120
+ export function withRetry(fn, { attempts = 3, sleep = defaultSleep } = {}) {
121
+ for (let i = 1; ; i += 1) {
122
+ try { return fn(); } catch (err) {
123
+ if (i >= attempts || !isTransient(err)) throw err;
124
+ sleep(2000 * i);
125
+ }
126
+ }
127
+ }
128
+
129
+ // GitHub sometimes answers a fork request with a 5xx and creates the fork anyway, so look before giving up.
130
+ function ensureFork({ gh, exec, sleep }) {
131
+ const exists = () => { try { exec('gh', ['repo', 'view', `${gh.user}/${UPSTREAM.split('/')[1]}`, '--json', 'name']); return true; } catch { return false; } };
132
+ try {
133
+ withRetry(() => exec('gh', ['repo', 'fork', UPSTREAM, '--clone=false']), { sleep });
134
+ } catch (err) {
135
+ for (let i = 0; i < 4 && !exists(); i += 1) sleep(3000);
136
+ if (!exists()) throw err;
137
+ }
138
+ }
139
+
111
140
  // Fork, branch, commit, push, open. Every GitHub call goes through `exec` so tests can fake them.
112
- export function openPullRequest({ pkgDir, pkg, gh, exec = defaultExec, say: out = say, cloneWait = 3000 }) {
141
+ export function openPullRequest({ pkgDir, pkg, gh, exec = defaultExec, say: out = say, cloneWait = 3000, sleep = defaultSleep }) {
113
142
  const work = fs.mkdtempSync(path.join(os.tmpdir(), 'sg-pr-'));
143
+ const retry = (fn) => withRetry(fn, { sleep });
114
144
  try {
115
145
  out(t('submitForking', { upstream: UPSTREAM }));
116
- exec('gh', ['repo', 'fork', UPSTREAM, '--clone=false']);
146
+ ensureFork({ gh, exec, sleep });
117
147
  out(t('submitPreparing'));
118
148
  const repoDir = path.join(work, 'repo');
119
149
  const forkUrl = `https://github.com/${gh.user}/${UPSTREAM.split('/')[1]}.git`;
@@ -136,16 +166,23 @@ export function openPullRequest({ pkgDir, pkg, gh, exec = defaultExec, say: out
136
166
  const ident = ['-c', `user.name=${gh.user}`, '-c', `user.email=${gh.id}+${gh.user}@users.noreply.github.com`];
137
167
  g(...ident, 'commit', '-q', '-F', msg);
138
168
  out(t('submitPushing'));
139
- g('-c', 'credential.helper=', '-c', 'credential.https://github.com.helper=!gh auth git-credential', 'push', '-q', 'origin', branch);
169
+ // a branch name left by an earlier attempt that died after the push is simply replaced (it is the contributor's own fork)
170
+ retry(() => g('-c', 'credential.helper=', '-c', 'credential.https://github.com.helper=!gh auth git-credential', 'push', '-q', '--force', 'origin', branch));
140
171
  out(t('submitOpening'));
141
172
  const body = path.join(work, 'body.md');
142
173
  fs.writeFileSync(body, prBody(pkg));
143
- const url = exec('gh', [
144
- 'pr', 'create', '--repo', UPSTREAM, '--head', `${gh.user}:${branch}`, '--base', 'main',
145
- '--title', `Submission: ${m.skill} on ${m.submission.host.id} / ${m.submission.model.resolved || m.submission.model.requested}`,
146
- '--body-file', body,
147
- ]).trim().split(/\s+/).pop();
148
- return url;
174
+ try {
175
+ return retry(() => exec('gh', [
176
+ 'pr', 'create', '--repo', UPSTREAM, '--head', `${gh.user}:${branch}`, '--base', 'main',
177
+ '--title', `Submission: ${m.skill} on ${m.submission.host.id} / ${m.submission.model.resolved || m.submission.model.requested}`,
178
+ '--body-file', body,
179
+ ])).trim().split(/\s+/).pop();
180
+ } catch (err) {
181
+ // an earlier attempt may have opened it before the connection dropped: reuse that pull request
182
+ const dup = /already exists[\s\S]*?(https:\/\/github\.com\/\S+\/pull\/\d+)/i.exec(String(err.stderr || err.message));
183
+ if (dup) return dup[1];
184
+ throw err;
185
+ }
149
186
  } finally {
150
187
  fs.rmSync(work, { recursive: true, force: true });
151
188
  }
@@ -216,18 +253,22 @@ export async function cmdSubmit(args, { exec = defaultExec, verifyRemote = skill
216
253
  say(t('submitManual', { upstream: UPSTREAM, dir: pkgDir, path: pkg.path.join('/') }));
217
254
  return { pkgDir, pkg };
218
255
  }
219
- try {
220
- const url = openPullRequest({ pkgDir, pkg, gh, exec });
221
- say(`\n${t('submitPrOpened', { url })}`);
222
- say(t('submitNext'));
223
- // the CLA link and the wait for the merge, with enter to continue (under test fakes there is nothing to wait for)
224
- if (hasTerminal() && exec === defaultExec) await watchPullRequest(url, { pick: choose, out: say });
225
- return { pkgDir, pkg, url };
226
- } catch (err) {
227
- say(`\n${t('submitGhFail', { msg: String(err.stderr || err.message).trim().split(/\r?\n/).slice(-3).join(' ') })}`);
228
- say(t('submitManual', { upstream: UPSTREAM, dir: pkgDir, path: pkg.path.join('/') }));
229
- process.exitCode = 1;
230
- return { pkgDir, pkg };
256
+ for (;;) {
257
+ try {
258
+ const url = openPullRequest({ pkgDir, pkg, gh, exec });
259
+ say(`\n${t('submitPrOpened', { url })}`);
260
+ say(t('submitNext'));
261
+ // the CLA link and the wait for the merge, with enter to continue (under test fakes there is nothing to wait for)
262
+ if (hasTerminal() && exec === defaultExec) await watchPullRequest(url, { pick: choose, out: say });
263
+ return { pkgDir, pkg, url };
264
+ } catch (err) {
265
+ say(`\n${t('submitGhFail', { msg: String(err.stderr || err.message).trim().split(/\r?\n/).slice(-3).join(' ') })}`);
266
+ // Enter retries: a non-technical contributor should not have to start over after a GitHub hiccup.
267
+ if (!opts.yes && isTransient(err) && hasTerminal() && await confirm(t('submitRetry'), true)) continue;
268
+ say(t('submitManual', { upstream: UPSTREAM, dir: pkgDir, path: pkg.path.join('/') }));
269
+ process.exitCode = 1;
270
+ return { pkgDir, pkg };
271
+ }
231
272
  }
232
273
  }
233
274
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillgauntlet",
3
- "version": "1.3.8",
3
+ "version": "1.3.9",
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": {