@fitchmultz/pi-subagents 0.52.10 → 0.52.12

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.
@@ -9,9 +9,40 @@ import {
9
9
  trySignalChildTree,
10
10
  } from "../src/shared/post-exit-stdio-guard.ts";
11
11
 
12
+ // oxlint-disable-next-line typescript/strict-void-return
12
13
  const execFileAsync = promisify(execFile);
13
14
 
14
- export function* filesIn(dir) {
15
+ /**
16
+ * @param {string} cwd
17
+ * @param {NodeJS.ProcessEnv} env
18
+ * @returns {import("node:child_process").ExecFileOptionsWithStringEncoding & { detached: boolean }}
19
+ */
20
+ function spawnOptions(cwd, env) {
21
+ return { cwd, env, encoding: "utf8", detached: true, maxBuffer: 16 * 1024 * 1024 };
22
+ }
23
+
24
+ /**
25
+ * A smoke-owned process: a direct execFile child or a discovered detached worker.
26
+ * @typedef {object} ProcessEntry
27
+ * @property {import("node:child_process").ChildProcess | { pid: number, kill(signal?: NodeJS.Signals | number): true }} child
28
+ * @property {boolean | undefined} [broker]
29
+ * @property {boolean | undefined} [exited]
30
+ * @property {number | undefined} [stoppedAt]
31
+ */
32
+ /**
33
+ * @typedef {object} SmokeProcessOptions
34
+ * @property {string} cwd
35
+ * @property {NodeJS.ProcessEnv} env
36
+ * @property {number} timeoutMs
37
+ * @property {AbortSignal} signal
38
+ * @property {string} root
39
+ */
40
+
41
+ /**
42
+ * @param {string} dir
43
+ * @returns {Generator<string, void, void>}
44
+ */
45
+ export function* filesIn(/** @type {string} */ dir) {
15
46
  if (!existsSync(dir)) {
16
47
  return;
17
48
  }
@@ -25,24 +56,29 @@ export function* filesIn(dir) {
25
56
  }
26
57
  }
27
58
 
28
- export function processRef(pid) {
59
+ export function processRef(/** @type {number} */ pid) {
29
60
  if (!Number.isSafeInteger(pid) || pid <= 1 || pid === process.pid) {
30
61
  throw new Error("Invalid smoke-owned process ID");
31
62
  }
32
- return { pid, kill: (signal) => process.kill(pid, signal) };
63
+ return {
64
+ pid,
65
+ kill: (/** @type {NodeJS.Signals | number | undefined} */ signal) => process.kill(pid, signal),
66
+ };
33
67
  }
34
68
 
35
69
  // This attempt owns only PIDs discovered from private run files or known direct children.
36
70
  // Credentials and artifacts must remain available until all owned workers and the broker exit.
37
71
  export class SmokeProcesses {
38
- #processes = new Map();
39
- #options;
72
+ // oxlint-disable-next-line typescript/prefer-readonly -- JSDoc class fields cannot express readonly
73
+ /** @type {Map<number, ProcessEntry>} */ #processes = new Map();
74
+ // oxlint-disable-next-line typescript/prefer-readonly -- JSDoc class fields cannot express readonly
75
+ /** @type {SmokeProcessOptions} */ #options;
40
76
 
41
- constructor({ cwd, env, timeoutMs, signal, root }) {
77
+ constructor(/** @type {SmokeProcessOptions} */ { cwd, env, timeoutMs, signal, root }) {
42
78
  this.#options = { cwd, env, timeoutMs, signal, root };
43
79
  }
44
80
 
45
- #remember(pid, broker = false) {
81
+ #remember(/** @type {number | undefined} */ pid, /** @type {boolean} */ broker = false) {
46
82
  if (pid === undefined) {
47
83
  return;
48
84
  }
@@ -50,17 +86,30 @@ export class SmokeProcesses {
50
86
  this.#processes.set(pid, { child: processRef(pid) });
51
87
  }
52
88
  if (broker) {
53
- this.#processes.get(pid).broker = true;
89
+ const remembered = this.#processes.get(pid);
90
+ if (remembered) {
91
+ remembered.broker = true;
92
+ }
54
93
  }
55
94
  }
56
95
 
96
+ /**
97
+ * @param {string} label
98
+ * @param {string} command
99
+ * @param {string[]} args
100
+ * @param {string | ((child: import("node:child_process").ChildProcess) => void)} [input]
101
+ * @returns {Promise<string>}
102
+ */
57
103
  run(label, command, args, input) {
58
104
  const { cwd, env, timeoutMs, signal } = this.#options;
59
105
  signal.throwIfAborted();
60
106
  return new Promise((resolve, reject) => {
61
- let discovery;
62
- let discoveryError;
63
- const finish = async (error, output) => {
107
+ /** @type {Promise<void> | undefined} */ let discovery;
108
+ /** @type {string | undefined} */ let discoveryError;
109
+ const finish = async (
110
+ /** @type {Error | undefined} */ error,
111
+ /** @type {string} */ output = "",
112
+ ) => {
64
113
  clearTimeout(timer);
65
114
  clearInterval(watcher);
66
115
  signal.removeEventListener("abort", cancel);
@@ -71,25 +120,23 @@ export class SmokeProcesses {
71
120
  resolve(output);
72
121
  }
73
122
  };
74
- const child = execFile(
75
- command,
76
- args,
77
- { cwd, env, encoding: "utf8", detached: true, maxBuffer: 16 * 1024 * 1024 },
78
- (error, stdout, stderr) => {
79
- const output = `${stdout ?? ""}${stderr ?? ""}`;
80
- void finish(
81
- error
82
- ? new Error(
83
- `${label} failed with ${error.code ?? "spawn error"}\nCommand: ${command} ${args.join(" ")}\n${output}`,
84
- )
85
- : undefined,
86
- output,
87
- );
88
- },
89
- );
123
+ const failure = (
124
+ /** @type {import("node:child_process").ExecFileException | null} */ error,
125
+ /** @type {string} */ output,
126
+ ) =>
127
+ error === null
128
+ ? undefined
129
+ : new Error(
130
+ `${label} failed with ${error.code ?? "spawn error"}\nCommand: ${command} ${args.join(" ")}\n${output}`,
131
+ );
132
+ const child = execFile(command, args, spawnOptions(cwd, env), (error, stdout, stderr) => {
133
+ const output = `${stdout}${stderr}`;
134
+ // oxlint-disable-next-line typescript/no-floating-promises -- finish settles the outer promise by design
135
+ void finish(failure(error, output), output);
136
+ });
90
137
  attachChildProcessLifecycle(child);
91
- if (child.pid) {
92
- const entry = { child };
138
+ if (child.pid !== undefined) {
139
+ /** @type {ProcessEntry} */ const entry = { child };
93
140
  this.#processes.set(child.pid, entry);
94
141
  child.once("close", () => {
95
142
  entry.exited = !isChildTreeAlive(child);
@@ -98,11 +145,12 @@ export class SmokeProcesses {
98
145
  // Discover detached startup children even if their parent's startup fails.
99
146
  const watcher = setInterval(() => {
100
147
  discovery ??= this.#collect()
101
- .catch((error) => {
102
- if (error.message !== discoveryError) {
103
- console.error(`[real-pi-smoke] process discovery: ${error.message}`);
148
+ .catch((/** @type {unknown} */ error) => {
149
+ const message = error instanceof Error ? error.message : JSON.stringify(error);
150
+ if (message !== discoveryError) {
151
+ console.error(`[real-pi-smoke] process discovery: ${message}`);
104
152
  }
105
- discoveryError = error.message;
153
+ discoveryError = message;
106
154
  })
107
155
  .finally(() => {
108
156
  discovery = undefined;
@@ -110,6 +158,7 @@ export class SmokeProcesses {
110
158
  }, 100);
111
159
  // The outer finally stops parents AND detached workers; rejecting must not kill parents first.
112
160
  const timer = setTimeout(() => {
161
+ // oxlint-disable-next-line typescript/no-floating-promises -- finish settles the outer promise by design
113
162
  void finish(
114
163
  new Error(
115
164
  `${label} timed out after ${timeoutMs}ms\nCommand: ${command} ${args.join(" ")}`,
@@ -117,36 +166,47 @@ export class SmokeProcesses {
117
166
  );
118
167
  }, timeoutMs);
119
168
  const cancel = () => {
120
- void finish(signal.reason);
169
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion, typescript/no-floating-promises -- signal.reason is any; finish settles the outer promise
170
+ void finish(/** @type {Error | undefined} */ (signal.reason));
121
171
  };
122
172
  signal.addEventListener("abort", cancel, { once: true });
123
- child.stdin.on("error", () => {
173
+ child.stdin?.on("error", () => {
124
174
  // A command may exit without reading RPC input; execFile reports its exit failure.
125
175
  });
126
176
  // A function input owns a persistent RPC stdin and closes it to request orderly shutdown.
127
177
  if (typeof input === "function") {
128
178
  input(child);
129
179
  } else {
130
- child.stdin.end(input);
180
+ child.stdin?.end(input);
131
181
  }
132
182
  });
133
183
  }
134
184
 
135
185
  #readRunPids() {
136
186
  const { env } = this.#options;
137
- const runs = join(env.PI_CODING_AGENT_DIR, "sessions", "agent-runs");
187
+ const agentDir = env.PI_CODING_AGENT_DIR;
188
+ const runtimeDir = env.PI_INTERCOM_RUNTIME_DIR;
189
+ if (agentDir === undefined || runtimeDir === undefined) {
190
+ throw new Error(
191
+ "PI_CODING_AGENT_DIR and PI_INTERCOM_RUNTIME_DIR are required for ownership discovery",
192
+ );
193
+ }
194
+ const runs = join(agentDir, "sessions", "agent-runs");
138
195
  for (const file of filesIn(runs)) {
139
196
  if (basename(file) === "process.json" && basename(dirname(file)) === "actor.lock") {
140
- this.#remember(JSON.parse(readFileSync(file, "utf8")).pid);
197
+ /** @type {{ pid: number }} */
198
+ // oxlint-disable-next-line typescript/no-unsafe-assignment
199
+ const record = JSON.parse(readFileSync(file, "utf8"));
200
+ this.#remember(record.pid);
141
201
  }
142
202
  }
143
- const brokerPid = join(env.PI_INTERCOM_RUNTIME_DIR, "broker.pid");
203
+ const brokerPid = join(runtimeDir, "broker.pid");
144
204
  if (existsSync(brokerPid)) {
145
205
  this.#remember(Number(readFileSync(brokerPid, "utf8").split("\n")[0]), true);
146
206
  }
147
207
  }
148
208
 
149
- async #discoverChildren(child) {
209
+ async #discoverChildren(/** @type {{ pid?: number | undefined }} */ child) {
150
210
  try {
151
211
  const { stdout } = await execFileAsync("pgrep", ["-P", String(child.pid)], {
152
212
  env: this.#options.env,
@@ -157,7 +217,7 @@ export class SmokeProcesses {
157
217
  this.#remember(Number(pid));
158
218
  }
159
219
  } catch (error) {
160
- if (error.code !== 1) {
220
+ if (typeof error !== "object" || error === null || !("code" in error) || error.code !== 1) {
161
221
  throw error;
162
222
  }
163
223
  // pgrep exit 1 means there are no matching children.
@@ -168,10 +228,11 @@ export class SmokeProcesses {
168
228
  this.#readRunPids();
169
229
  // Visit newly remembered grandchildren too, before signalling parents. Never scan argv globally.
170
230
  for (const entry of this.#processes.values()) {
171
- if (!entry.exited) {
231
+ if (entry.exited !== true) {
172
232
  entry.exited = !isChildTreeAlive(entry.child);
173
233
  }
174
- if (!entry.exited) {
234
+ // oxlint-disable-next-line typescript/no-unnecessary-boolean-literal-compare -- exited stays optional under tsc
235
+ if (entry.exited !== true) {
175
236
  // The Map expands during discovery; sequential traversal covers newly found grandchildren.
176
237
  // oxlint-disable-next-line no-await-in-loop
177
238
  await this.#discoverChildren(entry.child);
@@ -180,23 +241,24 @@ export class SmokeProcesses {
180
241
  }
181
242
 
182
243
  #alive() {
183
- const alive = [];
244
+ /** @type {ProcessEntry[]} */ const alive = [];
184
245
  for (const entry of this.#processes.values()) {
185
- if (!entry.exited) {
246
+ if (entry.exited !== true) {
186
247
  entry.exited = !isChildTreeAlive(entry.child);
187
248
  }
188
- if (!entry.exited) {
249
+ // oxlint-disable-next-line typescript/no-unnecessary-boolean-literal-compare -- exited stays optional under tsc
250
+ if (entry.exited !== true) {
189
251
  alive.push(entry);
190
252
  }
191
253
  }
192
254
  return alive;
193
255
  }
194
256
 
195
- #signal(alive) {
196
- const workers = alive.filter((entry) => !entry.broker);
257
+ #signal(/** @type {ProcessEntry[]} */ alive) {
258
+ const workers = alive.filter((entry) => entry.broker !== true);
197
259
  // Stop the broker last so exiting workers cannot restart it during cleanup.
198
- for (const entry of workers.length ? workers : alive) {
199
- if (!entry.stoppedAt) {
260
+ for (const entry of workers.length > 0 ? workers : alive) {
261
+ if (entry.stoppedAt === undefined) {
200
262
  entry.stoppedAt = Date.now();
201
263
  trySignalChildTree(entry.child, "SIGTERM");
202
264
  } else if (Date.now() - entry.stoppedAt >= 3000) {
@@ -206,7 +268,7 @@ export class SmokeProcesses {
206
268
  }
207
269
 
208
270
  async stop() {
209
- let lastError;
271
+ /** @type {string | undefined} */ let lastError;
210
272
  while (true) {
211
273
  let collected = false;
212
274
  try {
@@ -216,10 +278,11 @@ export class SmokeProcesses {
216
278
  collected = true;
217
279
  } catch (error) {
218
280
  // Retain the controller and credentials if ownership cannot yet be read.
219
- if (error.message !== lastError) {
220
- console.error(`[real-pi-smoke] waiting for owned-process cleanup: ${error.message}`);
281
+ const message = error instanceof Error ? error.message : JSON.stringify(error);
282
+ if (message !== lastError) {
283
+ console.error(`[real-pi-smoke] waiting for owned-process cleanup: ${message}`);
221
284
  }
222
- lastError = error.message;
285
+ lastError = message;
223
286
  }
224
287
  const alive = this.#alive();
225
288
  if (collected && alive.length === 0) {
@@ -13,7 +13,7 @@ function usage() {
13
13
  );
14
14
  }
15
15
 
16
- function parsePositiveInteger(value, source) {
16
+ function parsePositiveInteger(/** @type {string} */ value, /** @type {string} */ source) {
17
17
  if (!/^\d+$/.test(value)) {
18
18
  throw new Error(`${source} must be a positive integer, got ${value}`);
19
19
  }
@@ -24,10 +24,23 @@ function parsePositiveInteger(value, source) {
24
24
  return parsed;
25
25
  }
26
26
 
27
- function parseArgs(argv) {
27
+ /**
28
+ * @param {string[]} argv
29
+ * @param {number} index
30
+ * @returns {string}
31
+ */
32
+ function requiredValue(argv, index) {
33
+ const value = argv[index];
34
+ if (value === undefined || value === "") {
35
+ throw new Error("--timeout-ms requires a value");
36
+ }
37
+ return value;
38
+ }
39
+
40
+ function parseArgs(/** @type {string[]} */ argv) {
28
41
  const options = { llm: false, llmFull: false, keepTemp: false, timeoutMs: DEFAULT_TIMEOUT_MS };
29
42
  for (let index = 0; index < argv.length; index += 1) {
30
- const arg = argv[index];
43
+ const arg = argv[index] ?? "";
31
44
  if (arg === "--help" || arg === "-h") {
32
45
  usage();
33
46
  process.exit(0);
@@ -46,11 +59,7 @@ function parseArgs(argv) {
46
59
  continue;
47
60
  }
48
61
  if (arg === "--timeout-ms") {
49
- const value = argv[index + 1];
50
- if (!value) {
51
- throw new Error("--timeout-ms requires a value");
52
- }
53
- options.timeoutMs = parsePositiveInteger(value, "--timeout-ms");
62
+ options.timeoutMs = parsePositiveInteger(requiredValue(argv, index + 1), "--timeout-ms");
54
63
  index += 1;
55
64
  continue;
56
65
  }
@@ -63,7 +72,7 @@ function parseArgs(argv) {
63
72
  return options;
64
73
  }
65
74
 
66
- function isolatedEnv(root, agentDir) {
75
+ function isolatedEnv(/** @type {string} */ root, /** @type {string} */ agentDir) {
67
76
  const home = join(root, "home");
68
77
  const env = { ...process.env };
69
78
  for (const key of Object.keys(env)) {
@@ -85,11 +94,18 @@ function isolatedEnv(root, agentDir) {
85
94
  };
86
95
  }
87
96
 
97
+ /**
98
+ * @param {string} label
99
+ * @param {string[]} args
100
+ * @param {{ processes: import("./real-pi-processes.mjs").SmokeProcesses, input?: string | ((child: import("node:child_process").ChildProcess) => void) }} options
101
+ */
88
102
  function runPi(label, args, options) {
89
103
  return options.processes.run(label, "pi", args, options.input);
90
104
  }
91
105
 
92
- async function verifyBundledResources(options) {
106
+ async function verifyBundledResources(
107
+ /** @type {{ processes: import("./real-pi-processes.mjs").SmokeProcesses, input?: string | ((child: import("node:child_process").ChildProcess) => void), root: string }} */ options,
108
+ ) {
93
109
  const marker = join(options.root, "resources.json");
94
110
  const observer = join(options.root, "observe-resources.mjs");
95
111
  writeFileSync(
@@ -107,6 +123,8 @@ async function verifyBundledResources(options) {
107
123
  throw new Error(`bundled Pi resources did not register ${name}:\n${output}`);
108
124
  }
109
125
  }
126
+ /** @type {string[]} */
127
+ // oxlint-disable-next-line typescript/no-unsafe-assignment
110
128
  const tools = JSON.parse(readFileSync(marker, "utf8"));
111
129
  for (const name of ["delegate", "agent_runs", "intercom"]) {
112
130
  if (!tools.includes(name)) {
@@ -126,12 +144,19 @@ async function verifyBundledResources(options) {
126
144
  }
127
145
  }
128
146
 
147
+ /**
148
+ * @param {string} output
149
+ * @returns {{ type?: string, id?: string, success?: boolean, data?: { commands?: { name?: string }[] } } | undefined}
150
+ */
129
151
  function commandResponse(output) {
152
+ /** @type {{ type?: string, id?: string, success?: boolean, data?: { commands?: { name?: string }[] } } | undefined} */
130
153
  let response;
131
154
  for (const line of output.split(/\r?\n/)) {
132
155
  try {
156
+ /** @type {{ type?: string, id?: string, success?: boolean, data?: { commands?: { name?: string }[] } }} */
157
+ // oxlint-disable-next-line typescript/no-unsafe-assignment
133
158
  const parsed = JSON.parse(line);
134
- if (parsed?.type === "response" && parsed?.id === "commands") {
159
+ if (parsed.type === "response" && parsed.id === "commands") {
135
160
  response = parsed;
136
161
  }
137
162
  } catch {
@@ -141,13 +166,18 @@ function commandResponse(output) {
141
166
  return response;
142
167
  }
143
168
 
144
- function registeredCommands(response) {
145
- return response?.success === true && Array.isArray(response?.data?.commands)
146
- ? response.data.commands.map((command) => command?.name)
169
+ function registeredCommands(
170
+ /** @type {{ success?: boolean, data?: { commands?: { name?: string }[] } } | undefined} */ response,
171
+ ) {
172
+ return response !== undefined &&
173
+ response.success === true &&
174
+ response.data !== undefined &&
175
+ Array.isArray(response.data.commands)
176
+ ? response.data.commands.map((command) => command.name)
147
177
  : [];
148
178
  }
149
179
 
150
- function createProject(root) {
180
+ function createProject(/** @type {string} */ root) {
151
181
  const projectRoot = join(root, "project");
152
182
  mkdirSync(projectRoot, { recursive: true });
153
183
  return projectRoot;
@@ -158,7 +188,9 @@ async function main() {
158
188
  try {
159
189
  options = parseArgs(process.argv.slice(2));
160
190
  } catch (error) {
161
- console.error(`[real-pi-smoke] ${error instanceof Error ? error.message : String(error)}`);
191
+ console.error(
192
+ `[real-pi-smoke] ${error instanceof Error ? error.message : JSON.stringify(error)}`,
193
+ );
162
194
  usage();
163
195
  process.exit(2);
164
196
  }
@@ -169,19 +201,19 @@ async function main() {
169
201
  const projectRoot = createProject(root);
170
202
  const env = isolatedEnv(root, agentDir);
171
203
  const cancellation = new AbortController();
172
- const cancel = (signal) => cancellation.abort(new Error(`Cancelled by ${signal}`));
204
+ const cancel = (/** @type {NodeJS.Signals} */ signal) =>
205
+ cancellation.abort(new Error(`Cancelled by ${signal}`));
173
206
  for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
174
207
  process.on(signal, cancel);
175
208
  }
176
- const runOptions = {
209
+ const smokeOptions = {
177
210
  cwd: projectRoot,
178
211
  env,
179
212
  timeoutMs: options.timeoutMs,
180
213
  root,
181
214
  signal: cancellation.signal,
182
215
  };
183
-
184
- runOptions.processes = new SmokeProcesses(runOptions);
216
+ const runOptions = { ...smokeOptions, processes: new SmokeProcesses(smokeOptions) };
185
217
 
186
218
  try {
187
219
  await runPi("pi install pi-subagents", ["install", repoRoot, "--approve"], runOptions);
@@ -223,7 +255,9 @@ async function main() {
223
255
  }
224
256
  }
225
257
 
226
- main().catch((error) => {
227
- console.error(`[real-pi-smoke] ${error instanceof Error ? error.message : String(error)}`);
258
+ main().catch((/** @type {unknown} */ error) => {
259
+ console.error(
260
+ `[real-pi-smoke] ${error instanceof Error ? error.message : JSON.stringify(error)}`,
261
+ );
228
262
  process.exitCode = 1;
229
263
  });
@@ -14,7 +14,7 @@ function usage() {
14
14
  );
15
15
  }
16
16
 
17
- function parsePositiveInteger(value, source) {
17
+ function parsePositiveInteger(/** @type {string} */ value, /** @type {string} */ source) {
18
18
  if (!/^\d+$/.test(value)) {
19
19
  console.error(`${source} must be a positive integer number of milliseconds, got: ${value}`);
20
20
  process.exit(2);
@@ -29,25 +29,39 @@ function parsePositiveInteger(value, source) {
29
29
  return parsed;
30
30
  }
31
31
 
32
- function parseArgs(argv) {
33
- let mode = "unit";
34
- let timeoutMs = process.env.PI_TEST_TIMEOUT_MS
35
- ? parsePositiveInteger(process.env.PI_TEST_TIMEOUT_MS, "PI_TEST_TIMEOUT_MS")
32
+ /**
33
+ * @param {string[]} argv
34
+ * @param {number} index
35
+ * @returns {string}
36
+ */
37
+ function requiredValue(argv, index) {
38
+ const value = argv[index];
39
+ if (value === undefined || value === "") {
40
+ console.error("--timeout-ms requires a value");
41
+ process.exit(2);
42
+ }
43
+ return value;
44
+ }
45
+
46
+ function initialTimeout() {
47
+ const env = process.env.PI_TEST_TIMEOUT_MS;
48
+ return env !== undefined && env !== ""
49
+ ? parsePositiveInteger(env, "PI_TEST_TIMEOUT_MS")
36
50
  : undefined;
51
+ }
52
+
53
+ function parseArgs(/** @type {string[]} */ argv) {
54
+ let mode = "unit";
55
+ let timeoutMs = initialTimeout();
37
56
 
38
57
  for (let index = 0; index < argv.length; index += 1) {
39
- const arg = argv[index];
58
+ const arg = argv[index] ?? "";
40
59
  if (arg === "--help" || arg === "-h") {
41
60
  usage();
42
61
  process.exit(0);
43
62
  }
44
63
  if (arg === "--timeout-ms") {
45
- const value = argv[index + 1];
46
- if (!value) {
47
- console.error("--timeout-ms requires a value");
48
- process.exit(2);
49
- }
50
- timeoutMs = parsePositiveInteger(value, "--timeout-ms");
64
+ timeoutMs = parsePositiveInteger(requiredValue(argv, index + 1), "--timeout-ms");
51
65
  index += 1;
52
66
  continue;
53
67
  }
@@ -71,7 +85,7 @@ function parseArgs(argv) {
71
85
  return { mode, timeoutMs };
72
86
  }
73
87
 
74
- function testFiles(dir) {
88
+ function testFiles(/** @type {string} */ dir) {
75
89
  return readdirSync(dir)
76
90
  .filter((name) => name.endsWith(".test.ts"))
77
91
  .sort()
@@ -88,8 +102,20 @@ function sanitizedEnv() {
88
102
  return env;
89
103
  }
90
104
 
105
+ /**
106
+ * @param {string} label
107
+ * @param {string[]} files
108
+ * @param {number} timeoutMs
109
+ * @param {number} [concurrency]
110
+ */
91
111
  function runNodeTest(label, files, timeoutMs, concurrency) {
92
- const args = [...(concurrency ? [`--test-concurrency=${concurrency}`] : []), "--test", ...files];
112
+ const args = [
113
+ ...(concurrency !== undefined && concurrency !== 0
114
+ ? [`--test-concurrency=${concurrency}`]
115
+ : []),
116
+ "--test",
117
+ ...files,
118
+ ];
93
119
  const startedAt = Date.now();
94
120
  const tempRoot = mkdtempSync(
95
121
  join(process.platform === "android" ? tmpdir() : "/tmp", "pi-subagents-"),
@@ -112,12 +138,14 @@ function runNodeTest(label, files, timeoutMs, concurrency) {
112
138
  try {
113
139
  rmSync(tempRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
114
140
  } catch (error) {
115
- console.error(`Could not remove ${tempRoot}: ${error.message}`);
141
+ console.error(
142
+ `Could not remove ${tempRoot}: ${error instanceof Error ? error.message : JSON.stringify(error)}`,
143
+ );
116
144
  }
117
145
  }
118
146
  const elapsedMs = Date.now() - startedAt;
119
147
  if (result.error) {
120
- if (result.error.code === "ETIMEDOUT") {
148
+ if (/** @type {NodeJS.ErrnoException} */ (result.error).code === "ETIMEDOUT") {
121
149
  console.error(`${label} timed out after ${timeoutMs}ms (elapsed ${elapsedMs}ms).`);
122
150
  console.error(`Command: ${process.execPath} ${args.join(" ")}`);
123
151
  console.error(
@@ -128,7 +156,7 @@ function runNodeTest(label, files, timeoutMs, concurrency) {
128
156
  console.error(`Failed to start ${label}: ${result.error.message}`);
129
157
  return 1;
130
158
  }
131
- if (result.signal) {
159
+ if (result.signal !== null) {
132
160
  console.error(`${label} exited due to signal ${result.signal} after ${elapsedMs}ms.`);
133
161
  console.error(`Command: ${process.execPath} ${args.join(" ")}`);
134
162
  return 1;
package/tsconfig.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "strict": true,
15
15
  "noImplicitReturns": true,
16
16
  "allowJs": true,
17
- "checkJs": false,
17
+ "checkJs": true,
18
18
  "types": ["node"],
19
19
  "noUnusedLocals": true,
20
20
  "noFallthroughCasesInSwitch": true,