@icntswm/skillcheck 1.0.0 → 1.0.2
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/README.md +5 -1
- package/dist/agents/claude.js +60 -7
- package/dist/cli.js +9 -1
- package/dist/describe.js +6 -5
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
[](LICENSE)
|
|
4
4
|
[](https://www.npmjs.com/package/@icntswm/skillcheck)
|
|
5
5
|

|
|
6
|
-

|
|
7
6
|
|
|
8
7
|
**Regression tests for Claude Code skills.**
|
|
9
8
|
|
|
@@ -135,6 +134,11 @@ stops Claude Code before the first model call, so it costs nothing.
|
|
|
135
134
|
Works with Claude Code. Agents sit behind a small adapter interface, so others
|
|
136
135
|
that support skills can be added.
|
|
137
136
|
|
|
137
|
+
Requests can be in any language: `run` asks the model itself. `lint` compares
|
|
138
|
+
words, so it is tuned for English and Russian, works roughly for other
|
|
139
|
+
languages that put spaces between words, and is of little use for Chinese,
|
|
140
|
+
Japanese or Korean.
|
|
141
|
+
|
|
138
142
|
## License
|
|
139
143
|
|
|
140
144
|
[MIT](LICENSE)
|
package/dist/agents/claude.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { rmSync } from "node:fs";
|
|
2
3
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
4
|
import * as os from "node:os";
|
|
4
5
|
import * as path from "node:path";
|
|
@@ -41,6 +42,10 @@ export class ClaudeStream {
|
|
|
41
42
|
get sawInit() {
|
|
42
43
|
return this.sawInitEvent;
|
|
43
44
|
}
|
|
45
|
+
/** True after the result event: the run is over, nothing more will come. */
|
|
46
|
+
get finished() {
|
|
47
|
+
return this.sawResult;
|
|
48
|
+
}
|
|
44
49
|
/** Feed one stdout line; true means the run can be stopped early. */
|
|
45
50
|
push(line) {
|
|
46
51
|
if (!line.startsWith("{"))
|
|
@@ -195,6 +200,48 @@ function spawnErrorMessage(e, bin) {
|
|
|
195
200
|
return `${bin} not found`;
|
|
196
201
|
return `spawn failed: ${err.message}`;
|
|
197
202
|
}
|
|
203
|
+
/** Live runs by process group id, with their tmp dirs; see abortActiveRuns. */
|
|
204
|
+
const active = new Map();
|
|
205
|
+
function groupAlive(pid) {
|
|
206
|
+
try {
|
|
207
|
+
process.kill(-pid, 0);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Kill every running claude group and remove its tmp dir. Runs are detached
|
|
216
|
+
* process groups, so Ctrl+C on skillcheck does not reach them: without this
|
|
217
|
+
* they keep running (and billing) after skillcheck exits. Groups still alive
|
|
218
|
+
* after graceMs get SIGKILL, so a process that ignores SIGTERM dies too.
|
|
219
|
+
*/
|
|
220
|
+
export async function abortActiveRuns(graceMs = KILL_GRACE_MS) {
|
|
221
|
+
const runs = [...active];
|
|
222
|
+
active.clear();
|
|
223
|
+
for (const [pid] of runs) {
|
|
224
|
+
try {
|
|
225
|
+
process.kill(-pid, "SIGTERM");
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// already exited
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const deadline = Date.now() + graceMs;
|
|
232
|
+
while (runs.some(([pid]) => groupAlive(pid)) && Date.now() < deadline) {
|
|
233
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
234
|
+
}
|
|
235
|
+
for (const [pid, workdir] of runs) {
|
|
236
|
+
try {
|
|
237
|
+
process.kill(-pid, "SIGKILL");
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// already exited
|
|
241
|
+
}
|
|
242
|
+
rmSync(workdir, { recursive: true, force: true });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
198
245
|
const LOGIN_HINT = " — with --config-dir, auth comes from ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN (claude setup-token)";
|
|
199
246
|
/** A fresh config dir has no credentials; point the user at the env vars. */
|
|
200
247
|
function withLoginHint(error, configDir) {
|
|
@@ -286,11 +333,13 @@ export class ClaudeAdapter {
|
|
|
286
333
|
let stderrText = "";
|
|
287
334
|
let error = null;
|
|
288
335
|
let stoppedEarly = false;
|
|
336
|
+
let stopping = false;
|
|
289
337
|
let settled = false;
|
|
290
338
|
let killTimer;
|
|
291
339
|
let timeoutTimer;
|
|
292
340
|
// detached => child.pid is the process group id; ESRCH means it is gone
|
|
293
341
|
const killGroup = (pid) => {
|
|
342
|
+
clearTimeout(timeoutTimer); // the run is ending anyway, a late timeout must not overwrite its outcome
|
|
294
343
|
try {
|
|
295
344
|
process.kill(-pid, "SIGTERM");
|
|
296
345
|
}
|
|
@@ -311,6 +360,8 @@ export class ClaudeAdapter {
|
|
|
311
360
|
if (settled)
|
|
312
361
|
return;
|
|
313
362
|
settled = true;
|
|
363
|
+
if (child?.pid)
|
|
364
|
+
active.delete(child.pid);
|
|
314
365
|
if (killTimer)
|
|
315
366
|
clearTimeout(killTimer);
|
|
316
367
|
if (timeoutTimer)
|
|
@@ -334,6 +385,8 @@ export class ClaudeAdapter {
|
|
|
334
385
|
return;
|
|
335
386
|
}
|
|
336
387
|
const proc = child;
|
|
388
|
+
if (proc.pid)
|
|
389
|
+
active.set(proc.pid, workdir);
|
|
337
390
|
proc.on("error", (e) => {
|
|
338
391
|
if (!error)
|
|
339
392
|
error = spawnErrorMessage(e, bin);
|
|
@@ -342,11 +395,9 @@ export class ClaudeAdapter {
|
|
|
342
395
|
proc.stdout?.on("data", (chunk) => {
|
|
343
396
|
if (!stdoutSeen && chunk.trim() !== "")
|
|
344
397
|
stdoutSeen = true;
|
|
345
|
-
tail
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const line = tail.slice(0, nl);
|
|
349
|
-
tail = tail.slice(nl + 1);
|
|
398
|
+
const lines = (tail + chunk).split("\n");
|
|
399
|
+
tail = lines.pop(); // the unfinished last line waits for the next chunk
|
|
400
|
+
for (const line of lines) {
|
|
350
401
|
const stop = stream.push(line); // always parse: stop and abandon only gate the kill
|
|
351
402
|
if (error)
|
|
352
403
|
continue; // first failure wins, the group is already being killed
|
|
@@ -356,8 +407,10 @@ export class ClaudeAdapter {
|
|
|
356
407
|
if (proc.pid)
|
|
357
408
|
killGroup(proc.pid);
|
|
358
409
|
}
|
|
359
|
-
else if (!
|
|
360
|
-
|
|
410
|
+
else if (!stopping && ((control.earlyStop && stop) || control.stop?.(stream) === true)) {
|
|
411
|
+
stopping = true;
|
|
412
|
+
// after the result event the run is complete: the kill only reaps the process
|
|
413
|
+
stoppedEarly = !stream.finished;
|
|
361
414
|
if (proc.pid)
|
|
362
415
|
killGroup(proc.pid);
|
|
363
416
|
}
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { readFile } from "node:fs/promises";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
|
-
import { ClaudeAdapter, DEFAULT_DIRECTIVE } from "./agents/claude.js";
|
|
7
|
+
import { abortActiveRuns, ClaudeAdapter, DEFAULT_DIRECTIVE } from "./agents/claude.js";
|
|
8
8
|
import { BATCH_SCHEMA, buildBatchPrompt, parseBatchAnswer } from "./batch.js";
|
|
9
9
|
import { Budget } from "./budget.js";
|
|
10
10
|
import { BUILTIN_SKILLS, ConfigError, findDefaultCasesFile, knownSkillNames, loadSuite, unknownNames, } from "./cases.js";
|
|
@@ -667,6 +667,14 @@ function invokedDirectly() {
|
|
|
667
667
|
}
|
|
668
668
|
}
|
|
669
669
|
if (invokedDirectly()) {
|
|
670
|
+
// agent runs are detached process groups that Ctrl+C does not reach
|
|
671
|
+
for (const [signal, code] of [["SIGINT", 130], ["SIGTERM", 143]]) {
|
|
672
|
+
process.once(signal, () => {
|
|
673
|
+
// a second Ctrl+C during the grace period exits at once
|
|
674
|
+
process.once(signal, () => process.exit(code));
|
|
675
|
+
void abortActiveRuns().finally(() => process.exit(code));
|
|
676
|
+
});
|
|
677
|
+
}
|
|
670
678
|
main(process.argv.slice(2)).then((code) => {
|
|
671
679
|
process.exitCode = code;
|
|
672
680
|
});
|
package/dist/describe.js
CHANGED
|
@@ -194,7 +194,8 @@ function pluginDocs(configRoot, cwd) {
|
|
|
194
194
|
const installPath = entry.installPath;
|
|
195
195
|
if (typeof installPath !== "string" || installPath === "")
|
|
196
196
|
continue;
|
|
197
|
-
|
|
197
|
+
const manifest = pluginManifest(installPath);
|
|
198
|
+
out.push(...pluginSkillDocs(plugin, installPath, manifest), ...pluginCommandDocs(plugin, installPath, manifest));
|
|
198
199
|
}
|
|
199
200
|
}
|
|
200
201
|
return out;
|
|
@@ -210,8 +211,8 @@ function disabledPlugins(configRoot) {
|
|
|
210
211
|
}
|
|
211
212
|
return out;
|
|
212
213
|
}
|
|
213
|
-
function pluginSkillDocs(plugin, installPath) {
|
|
214
|
-
const declared =
|
|
214
|
+
function pluginSkillDocs(plugin, installPath, manifest) {
|
|
215
|
+
const declared = manifest?.skills;
|
|
215
216
|
const dirs = Array.isArray(declared)
|
|
216
217
|
? declared.filter((s) => typeof s === "string").map((rel) => path.resolve(installPath, rel))
|
|
217
218
|
: skillDirNames(path.join(installPath, "skills")).map((name) => path.join(installPath, "skills", name));
|
|
@@ -226,8 +227,8 @@ function pluginSkillDocs(plugin, installPath) {
|
|
|
226
227
|
}
|
|
227
228
|
return out;
|
|
228
229
|
}
|
|
229
|
-
function pluginCommandDocs(plugin, installPath) {
|
|
230
|
-
const declared =
|
|
230
|
+
function pluginCommandDocs(plugin, installPath, manifest) {
|
|
231
|
+
const declared = manifest?.commands;
|
|
231
232
|
let files;
|
|
232
233
|
if (Array.isArray(declared)) {
|
|
233
234
|
files = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@icntswm/skillcheck",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Regression tests for Claude Code skills: check that every request still loads the right skill",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^26.6.3",
|
|
35
|
-
"tsx": "^4.23.15",
|
|
36
35
|
"typescript": "^7.0.2",
|
|
37
36
|
"vitest": "^5.0.2"
|
|
38
37
|
},
|