@uinaf/skillcheck 0.5.1 → 0.5.3

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.
@@ -0,0 +1,71 @@
1
+ import { spawn } from "node:child_process";
2
+ //#region src/cursor-process.ts
3
+ let child;
4
+ let terminal = false;
5
+ let cleaning = false;
6
+ let watchdog = setTimeout(cleanup, 5e3);
7
+ function cleanup() {
8
+ if (cleaning) return;
9
+ cleaning = true;
10
+ clearTimeout(watchdog);
11
+ if (process.platform !== "win32") process.kill(-process.pid, "SIGKILL");
12
+ child?.kill("SIGKILL");
13
+ process.exit(1);
14
+ }
15
+ function fail(error) {
16
+ if (terminal) return;
17
+ terminal = true;
18
+ process.send?.({
19
+ type: "failure",
20
+ error
21
+ }, cleanup);
22
+ clearTimeout(watchdog);
23
+ watchdog = setTimeout(cleanup, 1e3);
24
+ }
25
+ if (!process.send) process.exit(1);
26
+ process.on("disconnect", cleanup);
27
+ process.on("message", (message) => {
28
+ if (message === "stop") return cleanup();
29
+ if (message === "cleanup" && terminal) {
30
+ process.send?.({ type: "cleanup" }, cleanup);
31
+ return;
32
+ }
33
+ if (child !== void 0 || terminal) return;
34
+ if (typeof message !== "object" || message === null || !("command" in message) || typeof message.command !== "string" || !("cwd" in message) || typeof message.cwd !== "string" || !("args" in message) || !Array.isArray(message.args) || !message.args.every((arg) => typeof arg === "string") || !("timeoutMs" in message) || typeof message.timeoutMs !== "number" || !Number.isFinite(message.timeoutMs) || message.timeoutMs <= 0) return fail("cursor-agent supervisor received invalid configuration");
35
+ clearTimeout(watchdog);
36
+ watchdog = setTimeout(() => fail("cursor-agent supervisor watchdog timed out"), message.timeoutMs + 1e3);
37
+ const command = message.command;
38
+ try {
39
+ child = spawn(command, message.args, {
40
+ cwd: message.cwd,
41
+ stdio: [
42
+ "pipe",
43
+ "inherit",
44
+ "inherit"
45
+ ]
46
+ });
47
+ } catch (error) {
48
+ return fail(`failed to spawn ${command}: ${error instanceof Error ? error.message : String(error)}`);
49
+ }
50
+ child.on("error", (error) => fail(`failed to spawn ${command}: ${error.message}`));
51
+ child.stdin?.on("error", (error) => fail(`cursor-agent stdin failed: ${error.message}`));
52
+ process.stdin.on("error", (error) => fail(`cursor-agent prompt stream failed: ${error.message}`));
53
+ if (child.stdin) process.stdin.pipe(child.stdin);
54
+ child.on("exit", (code, signal) => {
55
+ if (terminal) return;
56
+ if (!child?.stdin?.writableFinished) return fail("cursor-agent stdin closed before prompt delivery");
57
+ terminal = true;
58
+ process.stdin.unpipe();
59
+ clearTimeout(watchdog);
60
+ watchdog = setTimeout(cleanup, 1e3);
61
+ process.send?.({
62
+ type: "terminal",
63
+ code,
64
+ signal
65
+ }, (error) => {
66
+ if (error) cleanup();
67
+ });
68
+ });
69
+ });
70
+ //#endregion
71
+ export {};
@@ -1,4 +1,5 @@
1
- import { spawn } from "node:child_process";
1
+ import { fork } from "node:child_process";
2
+ import path from "node:path";
2
3
  //#region src/cursor-provider.ts
3
4
  function newStreamState() {
4
5
  return {
@@ -65,53 +66,114 @@ var CursorAgentProvider = class {
65
66
  const stderr = [];
66
67
  let stdoutBuf = "";
67
68
  return new Promise((resolve) => {
68
- const child = spawn(command, args, {
69
- cwd: this.config.working_dir,
70
- env: process.env,
69
+ const timeoutMs = this.config.timeout_ms ?? 9e5;
70
+ const supervisor = fork(new URL(`./cursor-process${path.extname(import.meta.url)}`, import.meta.url), {
71
+ execArgv: [],
72
+ detached: process.platform !== "win32",
71
73
  stdio: [
72
74
  "pipe",
73
75
  "pipe",
74
- "pipe"
76
+ "pipe",
77
+ "ipc"
75
78
  ]
76
79
  });
77
- const timeoutMs = this.config.timeout_ms ?? 9e5;
78
- const timer = setTimeout(() => {
79
- child.kill("SIGKILL");
80
- resolve({ error: `cursor-agent timed out after ${timeoutMs}ms` });
81
- }, timeoutMs);
82
- child.on("error", (err) => {
83
- clearTimeout(timer);
84
- resolve({ error: `failed to spawn ${command}: ${err.message}` });
85
- });
86
- child.stdout.on("data", (chunk) => {
87
- stdoutBuf += chunk.toString("utf8");
88
- const lines = stdoutBuf.split("\n");
89
- stdoutBuf = lines.pop() ?? "";
90
- for (const line of lines) foldLine(state, line);
91
- });
92
- child.stderr.on("data", (chunk) => {
93
- stderr.push(chunk.toString("utf8"));
94
- });
95
- child.on("close", (code) => {
80
+ let failure;
81
+ let terminal;
82
+ let cleanupConfirmed = false;
83
+ let settled = false;
84
+ let cleanupTimer;
85
+ const finish = (closed, signal = null) => {
86
+ if (settled) return;
87
+ settled = true;
96
88
  clearTimeout(timer);
89
+ clearTimeout(cleanupTimer);
90
+ supervisor.stdin?.destroy();
91
+ supervisor.stdout?.destroy();
92
+ supervisor.stderr?.destroy();
93
+ if (!closed) {
94
+ supervisor.kill("SIGKILL");
95
+ supervisor.unref();
96
+ if (supervisor.connected) supervisor.disconnect();
97
+ }
97
98
  if (stdoutBuf !== "") foldLine(state, stdoutBuf);
98
- if (state.result === void 0) {
99
+ const metadata = { skillCalls: state.skillCalls };
100
+ const fail = (error) => resolve({
101
+ error,
102
+ tokenUsage: state.tokenUsage,
103
+ metadata
104
+ });
105
+ if (failure !== void 0) {
99
106
  const tail = stderr.join("").trim().slice(-2e3);
100
- resolve({ error: `cursor-agent exited ${code ?? "by signal"} without a result event${tail === "" ? "" : `: ${tail}`}` });
101
- return;
107
+ const missingResult = terminal !== void 0 && state.result === void 0 ? " without a result event" : "";
108
+ return fail(`${failure}${missingResult}${tail === "" ? "" : `: ${tail}`}`);
102
109
  }
103
- if (state.isError) {
104
- resolve({ error: state.result || "cursor-agent reported an error result" });
105
- return;
110
+ if (!closed || !cleanupConfirmed || terminal === void 0 || process.platform !== "win32" && signal !== "SIGKILL") return fail("cursor-agent supervisor ended without confirmed cleanup");
111
+ if (terminal.code !== 0 || terminal.signal !== null || state.result === void 0) {
112
+ const tail = stderr.join("").trim().slice(-2e3);
113
+ return fail(`cursor-agent exited ${terminal.signal ?? terminal.code ?? "without status"}${state.result === void 0 ? " without a result event" : ""}${tail === "" ? "" : `: ${tail}`}`);
106
114
  }
115
+ if (state.isError) return fail(state.result || "cursor-agent reported an error result");
107
116
  resolve({
108
117
  output: state.result,
109
118
  tokenUsage: state.tokenUsage,
110
- metadata: { skillCalls: state.skillCalls }
119
+ metadata
111
120
  });
121
+ };
122
+ const boundCleanup = () => {
123
+ cleanupTimer ??= setTimeout(() => {
124
+ failure = failure === void 0 ? "cursor-agent cleanup or pipe draining timed out" : `${failure}; cleanup or pipe draining timed out`;
125
+ finish(false);
126
+ }, 2e3);
127
+ };
128
+ const stop = (error) => {
129
+ if (settled) return;
130
+ failure ??= error;
131
+ boundCleanup();
132
+ if (supervisor.connected) supervisor.send("stop", () => {});
133
+ };
134
+ const timer = setTimeout(() => stop(`cursor-agent timed out after ${timeoutMs}ms`), timeoutMs);
135
+ supervisor.on("error", (err) => stop(`cursor-agent supervisor failed: ${err.message}`));
136
+ supervisor.on("disconnect", () => {
137
+ if (!cleanupConfirmed && !settled) stop("cursor-agent supervisor disconnected before cleanup");
138
+ });
139
+ supervisor.on("message", (message) => {
140
+ if (settled) return;
141
+ if (typeof message !== "object" || message === null) return;
142
+ if (!("type" in message)) return;
143
+ if (message.type === "terminal" && "code" in message && "signal" in message && (message.code === null || typeof message.code === "number") && (message.signal === null || typeof message.signal === "string")) {
144
+ terminal = {
145
+ code: message.code,
146
+ signal: message.signal
147
+ };
148
+ clearTimeout(timer);
149
+ if (terminal.code !== 0 || terminal.signal !== null) failure ??= `cursor-agent exited ${terminal.signal ?? terminal.code ?? "without status"}`;
150
+ boundCleanup();
151
+ supervisor.send("cleanup", (err) => {
152
+ if (err) stop(`cursor-agent cleanup request failed: ${err.message}`);
153
+ });
154
+ } else if (message.type === "failure" && "error" in message && typeof message.error === "string") stop(message.error);
155
+ else if (message.type === "cleanup" && terminal !== void 0) cleanupConfirmed = true;
156
+ });
157
+ supervisor.stdout?.on("data", (chunk) => {
158
+ stdoutBuf += chunk.toString("utf8");
159
+ const lines = stdoutBuf.split("\n");
160
+ stdoutBuf = lines.pop() ?? "";
161
+ for (const line of lines) foldLine(state, line);
162
+ });
163
+ supervisor.stderr?.on("data", (chunk) => stderr.push(chunk.toString("utf8")));
164
+ supervisor.stdout?.on("error", (err) => stop(`cursor-agent stdout failed: ${err.message}`));
165
+ supervisor.stderr?.on("error", (err) => stop(`cursor-agent stderr failed: ${err.message}`));
166
+ supervisor.stdin?.on("error", (err) => stop(`cursor-agent stdin failed: ${err.message}`));
167
+ supervisor.on("close", (_code, signal) => finish(true, signal));
168
+ supervisor.send({
169
+ command,
170
+ args,
171
+ cwd: this.config.working_dir,
172
+ timeoutMs
173
+ }, (err) => {
174
+ if (err) stop(`cursor-agent supervisor initialization failed: ${err.message}`);
112
175
  });
113
- child.stdin.write(prompt);
114
- child.stdin.end();
176
+ supervisor.stdin?.end(prompt);
115
177
  });
116
178
  }
117
179
  };
package/docs/releasing.md CHANGED
@@ -48,11 +48,20 @@ history continues from `v0.1.3`; `v0.1.0`–`v0.1.3` are the legacy git-install
48
48
  tags and are never deleted or moved.
49
49
 
50
50
  During preparation, `@semantic-release/npm` stages the released `package.json`
51
- version and `@jno21/semantic-release-github-commit` commits it to `main` through
52
- GitHub's API as the authenticated App. GitHub signs that commit, and the release
53
- tag points to it. The `[skip ci]` marker on that commit is what stops a release
51
+ version and `scripts/release-commit.ts` commits it through GitHub's signed
52
+ `createCommitOnBranch` API as the authenticated App. The checkout and expected
53
+ branch head are the verified workflow event SHA. GitHub atomically rejects the
54
+ write if `main` has advanced; a successful response is fetched by its immutable
55
+ commit SHA. The plugin checks its parent, unchanged source, and exact prepared
56
+ manifest before semantic-release tags it. Only the package version may change. The `[skip ci]` marker on that commit is what stops a release
54
57
  from releasing itself.
55
58
 
59
+ If GitHub accepts the commit but fetching or validating it fails, preparation
60
+ stops before tagging or publishing. Inspect that commit's parent, tree, version
61
+ and verified signature, plus existing tags, npm versions and GitHub Releases,
62
+ before recovery. Reconcile only missing publication steps from the validated
63
+ commit; do not create another version or move an existing tag to hide failure.
64
+
56
65
  Check what the next version would be without publishing anything:
57
66
 
58
67
  ```sh
@@ -61,7 +70,7 @@ pnpm dlx semantic-release --dry-run --no-ci
61
70
 
62
71
  ## The artifact
63
72
 
64
- `dist/` is generated and untracked. `prepublishOnly` runs `pnpm run verify`,
73
+ `dist/` is generated and untracked. `prepublishOnly` runs `pnpm run verify:full`,
65
74
  which builds it, so the tarball is always packed from a tree that just passed
66
75
  the gate. `files` is `dist`, `docs`, `README.md`, `LICENSE`.
67
76
 
package/docs/usage.md CHANGED
@@ -56,7 +56,13 @@ the model to that CLI's own default.
56
56
  cursor provider, so the run uses this package's own provider module, which
57
57
  replays the CLI's `stream-json` output: the `result` event becomes the graded
58
58
  output and `SKILL.md` reads under `.cursor/skills/` become the `skill-used`
59
- evidence. The judge leg is unchanged.
59
+ evidence. Grading requires both a successful result event and harness exit code
60
+ zero. On macOS and Linux, a supervisor owns the process group and kills remaining
61
+ helpers when the harness exits, times out, or the provider disconnects. Output
62
+ pipes have a separate two-second cleanup/drain deadline; incomplete cleanup is
63
+ an error. Helpers that detach into another process group are outside this cleanup
64
+ boundary; retained output pipes still cause a bounded error. Windows retains
65
+ only direct-child cleanup. The judge leg is unchanged.
60
66
 
61
67
  `--judge` takes either a bare Claude model (graded through the Anthropic
62
68
  selection in [auth](#auth)) or a provider-qualified promptfoo id, passed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uinaf/skillcheck",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Lint and eval harness for agent skills",
5
5
  "homepage": "https://github.com/uinaf/skillcheck#readme",
6
6
  "bugs": {