skillgauntlet 1.1.0 → 1.2.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/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
- export async function select(title, options, defIndex = 0) {
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillgauntlet",
3
- "version": "1.1.0",
3
+ "version": "1.2.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
+ }