@fitchmultz/pi-subagents 0.52.10 → 0.52.11

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.
@@ -18,7 +18,10 @@ import { pathToFileURL } from "node:url";
18
18
  import { createRequire } from "node:module";
19
19
  import { hostCli, hostIndex, hostRoot } from "./compat-host.mjs";
20
20
  import { SmokeProcesses } from "./real-pi-processes.mjs";
21
+ import { assertChildJournal, assertObservations } from "./native-package-smoke-assertions.mjs";
21
22
 
23
+ /** @type {(file: string, args: readonly string[], options: import("node:child_process").ExecFileOptions) => Promise<{ stdout: string, stderr: string }>} */
24
+ // oxlint-disable-next-line typescript/strict-void-return
22
25
  const execFile = promisify(execFileCallback);
23
26
 
24
27
  if (process.argv.includes("--help") || process.argv.includes("-h")) {
@@ -142,8 +145,20 @@ writeFileSync(
142
145
  retry: { enabled: false },
143
146
  }),
144
147
  );
148
+ /** @type {typeof import("@earendil-works/pi-coding-agent")} */
149
+ // oxlint-disable-next-line typescript/no-unsafe-assignment
145
150
  const sdk = await import(pathToFileURL(hostIndex).href);
151
+ /** @type {typeof import("@earendil-works/pi-ai")} */
152
+ // oxlint-disable-next-line typescript/no-unsafe-assignment
146
153
  const ai = await import(import.meta.resolve("@earendil-works/pi-ai"));
154
+ /**
155
+ * The intercom context snapshot the consumer fixture answers with.
156
+ * @typedef {object} IntercomContextSnapshot
157
+ * @property {boolean | undefined} connected
158
+ * @property {string | undefined} peerId
159
+ * @property {string | undefined} rootSessionId
160
+ * @property {number | undefined} incarnation
161
+ */
147
162
  const selected = ai.fauxProvider({ provider: "packed-probe", models: [{ id: "native" }] });
148
163
  const processes = new SmokeProcesses({
149
164
  cwd: root,
@@ -152,22 +167,29 @@ const processes = new SmokeProcesses({
152
167
  timeoutMs: 60_000,
153
168
  signal: new AbortController().signal,
154
169
  });
155
- let session;
170
+ /** @type {import("@earendil-works/pi-coding-agent").AgentSession | undefined} */ let session;
156
171
  let watchdog;
157
- let brokerError;
158
- let intercomEvents;
172
+ /** @type {unknown} */ let brokerError;
173
+ /** @type {import("@earendil-works/pi-coding-agent").EventBus | undefined} */ let intercomEvents;
159
174
  const brokerRun = processes
160
175
  .run("private broker", process.execPath, [join(packageRoot, "dist/pi-intercom/broker/broker.js")])
161
- .catch((error) => {
176
+ .catch((/** @type {unknown} */ error) => {
162
177
  brokerError = error;
163
178
  });
179
+ const brokerReadinessMessage = () => {
180
+ if (brokerError === undefined) {
181
+ return "Private broker did not publish readiness";
182
+ }
183
+ return brokerError instanceof Error ? brokerError.message : JSON.stringify(brokerError);
184
+ };
185
+ const runtimeDir = process.env.PI_INTERCOM_RUNTIME_DIR;
186
+ if (runtimeDir === undefined) {
187
+ throw new Error("PI_INTERCOM_RUNTIME_DIR is required");
188
+ }
164
189
  try {
165
190
  const deadline = Date.now() + 10_000;
166
- while (!existsSync(join(process.env.PI_INTERCOM_RUNTIME_DIR, "broker.pid"))) {
167
- assert.ok(
168
- !brokerError && Date.now() < deadline,
169
- String(brokerError ?? "Private broker did not publish readiness"),
170
- );
191
+ while (!existsSync(join(runtimeDir, "broker.pid"))) {
192
+ assert.ok(brokerError === undefined && Date.now() < deadline, brokerReadinessMessage());
171
193
  // Observe the broker's publication, not a sleep-based startup assumption.
172
194
  // oxlint-disable-next-line no-await-in-loop
173
195
  await delay(25);
@@ -181,20 +203,21 @@ try {
181
203
  noContextFiles: true,
182
204
  noPromptTemplates: true,
183
205
  noThemes: true,
184
- extensionFactories: consumerScript
185
- ? [
186
- (pi) => {
187
- intercomEvents = pi.events;
188
- },
189
- ]
190
- : [],
206
+ extensionFactories:
207
+ consumerScript !== undefined
208
+ ? [
209
+ (/** @type {import("@earendil-works/pi-coding-agent").ExtensionAPI} */ pi) => {
210
+ intercomEvents = pi.events;
211
+ },
212
+ ]
213
+ : [],
191
214
  });
192
215
  await loader.reload();
193
216
  assert.deepEqual(loader.getExtensions().errors, []);
194
217
  assert.deepEqual(loader.getExtensions().warnings ?? [], []);
195
218
  assert.equal(
196
219
  loader.getExtensions().extensions.length,
197
- consumerScript ? 3 : 2,
220
+ consumerScript !== undefined ? 3 : 2,
198
221
  "one package entry plus credential-free provider and optional client observer fixtures",
199
222
  );
200
223
  const modelRuntime = await sdk.ModelRuntime.create({
@@ -205,52 +228,69 @@ try {
205
228
  allowModelNetwork: false,
206
229
  });
207
230
  modelRuntime.registerNativeProvider(selected.provider);
208
- ({ session } = await sdk.createAgentSession({
209
- cwd: root,
210
- agentDir,
211
- settingsManager,
212
- resourceLoader: loader,
213
- modelRuntime,
214
- model: selected.getModel(),
215
- thinkingLevel: "off",
216
- sessionManager: sdk.SessionManager.create(root, join(root, "sessions")),
217
- noTools: "builtin",
218
- }));
219
- const errors = [];
220
- await session.bindExtensions({ mode: "print", onError: (error) => errors.push(error) });
231
+ session = (
232
+ await sdk.createAgentSession({
233
+ cwd: root,
234
+ agentDir,
235
+ settingsManager,
236
+ resourceLoader: loader,
237
+ modelRuntime,
238
+ model: selected.getModel(),
239
+ thinkingLevel: "off",
240
+ sessionManager: sdk.SessionManager.create(root, join(root, "sessions")),
241
+ noTools: "builtin",
242
+ })
243
+ ).session;
244
+ /** @type {unknown[]} */ const errors = [];
245
+ await session.bindExtensions({
246
+ mode: "print",
247
+ onError: (error) => {
248
+ errors.push(error);
249
+ },
250
+ });
221
251
  assert.deepEqual(
252
+ // oxlint-disable-next-line typescript/no-unnecessary-condition -- assert generic inference already matches
222
253
  [...session.getActiveToolNames()].sort(),
223
254
  expectedTools,
224
255
  "exactly three eager package tools",
225
256
  );
226
- const execute = (name, input) =>
227
- session.agent.state.tools
228
- .find((tool) => tool.name === name)
229
- .execute(`probe-${name}`, input, new AbortController().signal);
257
+ const execute = (/** @type {string} */ name, /** @type {Record<string, unknown>} */ input) => {
258
+ assert.ok(session !== undefined, "expected a created agent session");
259
+ const tool = session.agent.state.tools.find((candidate) => candidate.name === name);
260
+ assert.ok(tool !== undefined, `expected the ${name} package tool`);
261
+ return tool.execute(`probe-${name}`, input, new AbortController().signal);
262
+ };
230
263
  const status = await execute("intercom", { action: "status" });
231
264
  assert.match(JSON.stringify(status.content), /Connected: Yes/);
232
265
  const runs = await execute("agent_runs", { action: "list" });
233
- assert.equal(runs.details.total, 0);
266
+ /** @type {{ total: number, runs: unknown[] }} */
267
+ // oxlint-disable-next-line typescript/no-unsafe-assignment
268
+ const listDetails = runs.details;
269
+ assert.equal(listDetails.total, 0);
234
270
  assert.equal(session.model?.provider, "packed-probe");
235
- assert.equal(session.model?.id, "native");
236
- const events = [];
237
- session.subscribe((event) => events.push(event));
271
+ assert.equal(session.model.id, "native");
272
+ /** @type {import("@earendil-works/pi-coding-agent").AgentSessionEvent[]} */ const events = [];
273
+ session.subscribe((event) => {
274
+ events.push(event);
275
+ });
238
276
  watchdog = setTimeout(() => {
239
- void session.abort();
277
+ // Fire-and-forget by design: the watchdog only interrupts a stuck turn.
278
+ // oxlint-disable-next-line typescript/no-floating-promises
279
+ void session?.abort();
240
280
  }, 30_000);
241
281
  await session.prompt("Exercise the packed native delegate.", { expandPromptTemplates: false });
282
+ const relevantEvents = () =>
283
+ events.filter(
284
+ (event) =>
285
+ (event.type === "message_end" && event.message.role === "assistant") ||
286
+ event.type === "tool_execution_end",
287
+ );
288
+
242
289
  await session.waitForIdle();
243
290
  assert.equal(
244
291
  session.getLastAssistantText(),
245
292
  "PACKED_LAUNCH_RECEIPT_SEEN",
246
- JSON.stringify({
247
- errors,
248
- messages: events.filter(
249
- (event) =>
250
- (event.type === "message_end" && event.message?.role === "assistant") ||
251
- event.type === "tool_execution_end",
252
- ),
253
- }),
293
+ JSON.stringify({ errors, messages: relevantEvents() }),
254
294
  );
255
295
  const completionDeadline = Date.now() + 30_000;
256
296
  while (session.getLastAssistantText() !== "PACKED_PARENT_COMPLETE") {
@@ -267,118 +307,76 @@ try {
267
307
  assert.equal(
268
308
  session.getLastAssistantText(),
269
309
  "PACKED_PARENT_COMPLETE",
270
- JSON.stringify({
271
- errors,
272
- messages: events.filter(
273
- (event) =>
274
- (event.type === "message_end" && event.message?.role === "assistant") ||
275
- event.type === "tool_execution_end",
276
- ),
277
- }),
310
+ JSON.stringify({ errors, messages: relevantEvents() }),
278
311
  );
279
312
  const starts = events.filter(
280
313
  (event) => event.type === "tool_execution_start" && event.toolName === "delegate",
281
314
  );
282
315
  assert.equal(starts.length, 1, "one real native delegate call, no fabricated wire events");
283
- const receipt = events.find(
284
- (event) => event.type === "tool_execution_end" && event.toolName === "delegate",
285
- );
286
- assert.ok(receipt && !receipt.isError, JSON.stringify(receipt));
287
- assert.equal(receipt.result.details.receipt, "launch");
288
- const run = (await execute("agent_runs", { action: "inspect", id: receipt.result.details.id }))
316
+ const receipt =
317
+ /** @type {{ isError?: boolean, result?: { details?: { receipt?: string, id?: string } } } | undefined} */ (
318
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the packed event shape is not exported by the SDK
319
+ events.find((event) => event.type === "tool_execution_end" && event.toolName === "delegate")
320
+ );
321
+ assert.ok(receipt !== undefined && receipt.isError !== true, JSON.stringify(receipt));
322
+ assert.equal(receipt.result?.details?.receipt, "launch");
323
+ /** @type {{ status: string, output: { text: string }, sessionFile: string, result?: { terminalEntryId?: string } }} */
324
+ // oxlint-disable-next-line typescript/no-unnecessary-condition, typescript/no-unsafe-assignment -- cast-type optionality is not modelled by the checker
325
+ const run = (await execute("agent_runs", { action: "inspect", id: receipt.result?.details?.id }))
289
326
  .details;
290
327
  assert.equal(run.status, "completed");
291
328
  assert.equal(run.output.text, "PACKED_NATIVE_COMPLETE");
292
- const childEntries = readFileSync(run.sessionFile, "utf8")
293
- .trimEnd()
294
- .split("\n")
295
- .map((line) => JSON.parse(line));
296
- assert.ok(
297
- childEntries.some(
298
- (entry) =>
299
- entry.type === "message" &&
300
- entry.message.role === "toolResult" &&
301
- entry.message.toolName === "packed_probe" &&
302
- entry.message.content.some((part) => part.text === "PACKED_TOOL_RESULT"),
303
- ),
304
- "actual child journal contains its native tool result",
305
- );
306
- assert.ok(
307
- childEntries.some(
308
- (entry) =>
309
- entry.id === run.result.terminalEntryId &&
310
- entry.type === "message" &&
311
- entry.message.role === "assistant" &&
312
- entry.message.content.some((part) => part.text === "PACKED_NATIVE_COMPLETE"),
313
- ),
314
- "completion references an actually published child assistant entry",
315
- );
316
- const observed = readFileSync(observations, "utf8")
317
- .trimEnd()
318
- .split("\n")
319
- .map((line) => JSON.parse(line));
320
- assert.equal(observed.filter((item) => item.child && item.modelBound).length, 1);
321
- assert.equal(observed.filter((item) => item.child && item.toolExecuted).length, 1);
322
- assert.equal(
323
- observed.filter((item) => item.child && item.consumedResult && item.calls === 2).length,
324
- 1,
325
- );
326
- assert.equal(
327
- observed.filter((item) => !item.child && item.consumedResult && item.calls === 2).length,
328
- 1,
329
- );
330
- assert.equal(
331
- observed.filter((item) => !item.child && item.consumedCompletion && item.calls === 3).length,
332
- 1,
333
- "the parent is resumed exactly once by the native completion notification",
334
- );
329
+ assertChildJournal(run.sessionFile, run.result?.terminalEntryId, "PACKED_NATIVE_COMPLETE");
330
+ assertObservations(observations);
335
331
  assert.equal(networkAttempts, 0);
336
- assert.equal(
337
- observed.some((item) => item.networkAttempt),
338
- false,
339
- );
340
332
  assert.deepEqual(errors, []);
341
333
 
342
- if (consumerScript) {
343
- let target;
334
+ if (consumerScript !== undefined) {
335
+ assert.ok(intercomEvents !== undefined, "expected the consumer observer extension");
336
+ assert.ok(consumerDir !== undefined, "expected the consumer script directory argument");
337
+ /** @type {IntercomContextSnapshot | undefined} */ let target;
344
338
  intercomEvents.emit("pi-intercom:context-query", {
345
339
  version: 1,
346
- respond: (snapshot) => {
340
+ respond: (/** @type {IntercomContextSnapshot} */ snapshot) => {
347
341
  target = snapshot;
348
342
  },
349
343
  });
350
- assert.ok(target?.connected && target.peerId && target.rootSessionId && target.incarnation);
344
+ assert.ok(
345
+ target?.connected === true &&
346
+ target.peerId !== undefined &&
347
+ target.rootSessionId !== undefined &&
348
+ target.incarnation !== undefined,
349
+ );
351
350
  const guard = {
352
351
  rootSessionId: target.rootSessionId,
353
352
  peerId: target.peerId,
354
353
  incarnation: target.incarnation,
355
354
  applicationGuard: "package-consent-1",
356
355
  };
357
- let consent;
356
+ /** @type {{ applicationGuard?: string } | undefined} */ let consent;
358
357
  intercomEvents.emit("pi-intercom:application-guard", {
359
358
  version: 1,
360
359
  target: guard,
361
360
  revision: guard.applicationGuard,
362
- respond: (result) => {
361
+ respond: (/** @type {{ applicationGuard?: string }} */ result) => {
363
362
  consent = result;
364
363
  },
365
364
  });
365
+ assert.ok(consent !== undefined, "the consumer answered the guard query");
366
366
  assert.equal(consent.applicationGuard, guard.applicationGuard);
367
367
  const consumerHome = join(root, "consumer-home");
368
368
  mkdirSync(consumerHome);
369
- const brokerPid = Number(
370
- readFileSync(join(process.env.PI_INTERCOM_RUNTIME_DIR, "broker.pid"), "utf8").split("\n")[0],
371
- );
369
+ const brokerPid = Number(readFileSync(join(runtimeDir, "broker.pid"), "utf8").split("\n")[0]);
372
370
  const consumerOptions = {
373
371
  cwd: consumerDir,
374
372
  env: {
375
373
  HOME: consumerHome,
376
374
  PATH: process.env.PATH,
377
375
  PI_CODING_AGENT_DIR: join(consumerHome, "agent"),
378
- PI_INTERCOM_RUNTIME_DIR: process.env.PI_INTERCOM_RUNTIME_DIR,
376
+ PI_INTERCOM_RUNTIME_DIR: runtimeDir,
379
377
  S3_ROOT: guard.rootSessionId,
380
378
  S3_PEER: guard.peerId,
381
- S3_INCARNATION: guard.incarnation,
379
+ S3_INCARNATION: String(guard.incarnation),
382
380
  S3_GUARD: guard.applicationGuard,
383
381
  S3_BROKER_PID: String(brokerPid),
384
382
  },
@@ -401,6 +399,7 @@ try {
401
399
  );
402
400
  process.stdout.write(stdout);
403
401
  process.stderr.write(stderr);
402
+ // oxlint-disable-next-line typescript/no-unnecessary-condition -- assert generic inference already matches
404
403
  assert.equal(events.filter((event) => event.type === "turn_start").length, turns);
405
404
  } finally {
406
405
  // The consumer's failure/timeout must never leave this fixture's broker stopped.
@@ -435,7 +434,7 @@ export default function(pi) {
435
434
  });
436
435
  }`,
437
436
  );
438
- const cliEnv = {
437
+ /** @type {typeof process.env} */ const cliEnv = {
439
438
  ...process.env,
440
439
  PATH:
441
440
  process.platform === "android" ? `${dirname(process.execPath)}:/system/bin` : "/usr/bin:/bin",
@@ -473,7 +472,10 @@ export default function(pi) {
473
472
  "",
474
473
  );
475
474
  assert.doesNotMatch(output, /Failed to load extension|ERR_INTERNAL_ASSERTION|Extension error/);
475
+ /** @type {{ tools: string[], sdkReused: boolean }} */
476
+ // oxlint-disable-next-line typescript/no-unsafe-assignment
476
477
  const cli = JSON.parse(readFileSync(marker, "utf8"));
478
+ // oxlint-disable-next-line typescript/no-unnecessary-condition -- assert generic inference already matches
477
479
  assert.deepEqual(cli.tools.sort(), expectedTools);
478
480
  if (!existsSync(join(packageRoot, "node_modules/@earendil-works/pi-coding-agent"))) {
479
481
  assert.equal(cli.sdkReused, true, "production-only installs reuse Pi's loaded SDK");
@@ -25,13 +25,20 @@ if (process.argv.includes("--help") || process.argv.includes("-h")) {
25
25
  }
26
26
  assert.ok(process.argv.length <= 3, "Expected at most one package tarball; use --help");
27
27
 
28
+ /** @type {{ name: string, version: string, pi: { extensions?: string[], skills?: string[], agents?: unknown, prompts?: unknown } }} */
29
+ // oxlint-disable-next-line typescript/no-unsafe-assignment
28
30
  const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
29
- const candidate = process.argv[2] ? resolve(process.argv[2]) : undefined;
31
+ const candidate = process.argv[2] !== undefined ? resolve(process.argv[2]) : undefined;
30
32
  const root = mkdtempSync(join(tmpdir(), "ps-package-"));
31
33
  const packDir = join(root, "pack");
32
34
  mkdirSync(packDir);
33
35
 
34
- function run(command, args, cwd = process.cwd(), env = process.env) {
36
+ function run(
37
+ /** @type {string} */ command,
38
+ /** @type {string[]} */ args,
39
+ /** @type {string} */ cwd = process.cwd(),
40
+ /** @type {NodeJS.ProcessEnv} */ env = process.env,
41
+ ) {
35
42
  const result = spawnSync(command, args, {
36
43
  cwd,
37
44
  env,
@@ -48,7 +55,9 @@ function run(command, args, cwd = process.cwd(), env = process.env) {
48
55
  return result.stdout;
49
56
  }
50
57
 
51
- function verifyManifest(manifest) {
58
+ function verifyManifest(
59
+ /** @type {{ name?: string, private?: boolean, publishConfig?: unknown, bin?: unknown, pi: { extensions?: string[], skills?: string[], agents?: unknown, prompts?: unknown } }} */ manifest,
60
+ ) {
52
61
  assert.equal(manifest.name, "@fitchmultz/pi-subagents");
53
62
  assert.notEqual(manifest.private, true);
54
63
  assert.deepEqual(manifest.publishConfig, { access: "public" });
@@ -59,7 +68,8 @@ function verifyManifest(manifest) {
59
68
  assert.equal(manifest.pi.prompts, undefined);
60
69
  }
61
70
 
62
- function installProduction(pack) {
71
+ function installProduction(/** @type {{ filename?: string, files?: { path: string }[] }} */ pack) {
72
+ assert.ok(pack.filename !== undefined, "npm pack must report a filename");
63
73
  const installDir = join(root, "npm");
64
74
  mkdirSync(installDir);
65
75
  writeFileSync(
@@ -103,7 +113,7 @@ function installProduction(pack) {
103
113
  ];
104
114
  }
105
115
 
106
- function verifyDeclarations(consumerDir) {
116
+ function verifyDeclarations(/** @type {string} */ consumerDir) {
107
117
  const contract = join(consumerDir, "public-client.test-d.ts");
108
118
  cpSync(new URL("../test/fixtures/intercom-client-contract.test-d.ts", import.meta.url), contract);
109
119
  const compiler = compilerInvocation();
@@ -130,10 +140,19 @@ function verifyDeclarations(consumerDir) {
130
140
  );
131
141
  }
132
142
 
133
- async function verifyRuntime({ packageRoot, consumerDir }) {
134
- verifyManifest(JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")));
143
+ async function verifyRuntime(
144
+ /** @type {{ packageRoot: string, consumerDir: string }} */ { packageRoot, consumerDir },
145
+ ) {
146
+ /** @type {{ name?: string, private?: boolean, publishConfig?: unknown, bin?: unknown, pi: { extensions?: string[], skills?: string[], agents?: unknown, prompts?: unknown } }} */
147
+ // oxlint-disable-next-line typescript/no-unsafe-assignment
148
+ const manifest = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
149
+ verifyManifest(manifest);
135
150
  verifyDeclarations(consumerDir);
151
+ /** @type {typeof import("@earendil-works/pi-coding-agent")} */
152
+ // oxlint-disable-next-line typescript/no-unsafe-assignment
136
153
  const sdk = await import(pathToFileURL(join(hostRoot, "dist/index.js")).href);
154
+ /** @type {typeof import("../src/agent/host.ts")} */
155
+ // oxlint-disable-next-line typescript/no-unsafe-assignment
137
156
  const nativeHost = await import(pathToFileURL(join(packageRoot, "dist/agent/host.js")).href);
138
157
  assert.equal(
139
158
  await nativeHost.loadSDK(hostRoot),
@@ -162,9 +181,12 @@ assert.equal(native.Assert, hostValue.Assert, "detached validation uses the sele
162
181
  PI_INTERCOM_PARENT_SESSION_ID: "package-smoke",
163
182
  },
164
183
  );
184
+ /** @type {typeof import("../src/pi-intercom/broker/spawn.ts")} */
185
+ // oxlint-disable-next-line typescript/no-unsafe-assignment
165
186
  const broker = await import(
166
187
  pathToFileURL(join(packageRoot, "dist/pi-intercom/broker/spawn.js")).href
167
188
  );
189
+ // oxlint-disable-next-line typescript/no-unnecessary-condition -- assert generic inference already matches
168
190
  assert.equal(realpathSync(broker.getBrokerSpawnOptions().cwd), realpathSync(packageRoot));
169
191
  const help = run(
170
192
  process.execPath,
@@ -203,27 +225,33 @@ assert.equal(native.Assert, hostValue.Assert, "detached validation uses the sele
203
225
  try {
204
226
  verifyManifest(packageJson);
205
227
  process.env.PI_PACKAGE_DIR = hostRoot;
206
- const pack = Object.values(
228
+ /** @type {{ name: string, version: string, filename: string, files: { path: string }[] }[]} */
229
+ const packReports = Object.values(
230
+ // oxlint-disable-next-line typescript/no-unsafe-argument
207
231
  JSON.parse(
208
232
  run("npm", [
209
233
  "pack",
210
234
  "--json",
211
235
  "--pack-destination",
212
236
  packDir,
213
- ...(candidate ? [candidate] : []),
237
+ ...(candidate !== undefined ? [candidate] : []),
214
238
  ]),
215
239
  ),
216
- )[0];
217
- assert.ok(pack && Array.isArray(pack.files), "npm pack must report files");
218
- if (candidate) {
240
+ );
241
+ const pack = packReports[0];
242
+ assert.ok(pack !== undefined && Array.isArray(pack.files), "npm pack must report files");
243
+ if (candidate !== undefined) {
244
+ // oxlint-disable-next-line typescript/no-unnecessary-condition -- assert generic inference already matches
219
245
  assert.equal(pack.name, packageJson.name);
246
+ // oxlint-disable-next-line typescript/no-unnecessary-condition -- assert generic inference already matches
220
247
  assert.equal(pack.version, packageJson.version, "Candidate version differs from source");
221
248
  assert.ok(
222
249
  readFileSync(join(packDir, pack.filename)).equals(readFileSync(candidate)),
223
250
  "npm changed the candidate tarball",
224
251
  );
225
252
  }
226
- const paths = new Set(pack.files.map((file) => file.path));
253
+ const files = pack.files;
254
+ const paths = new Set(files.map((file) => file.path));
227
255
  for (const path of [
228
256
  "package.json",
229
257
  "LICENSE",
@@ -266,7 +294,9 @@ try {
266
294
  `[package-smoke] ${pack.name}@${pack.version}: ${pack.files.length} files; one entry, zero profiles/prompts; runtime-only npm and standalone native actors passed`,
267
295
  );
268
296
  } catch (error) {
269
- console.error(`[package-smoke] ${error instanceof Error ? error.stack : String(error)}`);
297
+ console.error(
298
+ `[package-smoke] ${error instanceof Error ? (error.stack ?? "no stack") : JSON.stringify(error)}`,
299
+ );
270
300
  process.exitCode = 1;
271
301
  } finally {
272
302
  rmSync(root, { recursive: true, force: true });
@@ -16,10 +16,13 @@ import { join } from "node:path";
16
16
  import process from "node:process";
17
17
  import { promisify } from "node:util";
18
18
 
19
+ /** @type {(file: string, args: readonly string[], options: import("node:child_process").ExecFileOptions) => Promise<{ stdout: string, stderr: string }>} */
20
+ // oxlint-disable-next-line typescript/strict-void-return
19
21
  const execFile = promisify(execFileCallback);
20
22
  const cwd = process.cwd();
21
23
  const buildScript = join(cwd, "scripts", "build.mjs");
22
- const tscBin = (root) => join(root, "node_modules", "typescript", "bin", "tsc");
24
+ const tscBin = (/** @type {string} */ root) =>
25
+ join(root, "node_modules", "typescript", "bin", "tsc");
23
26
 
24
27
  if (process.argv.includes("--help") || process.argv.includes("-h")) {
25
28
  console.log(
@@ -30,17 +33,17 @@ if (process.argv.includes("--help") || process.argv.includes("-h")) {
30
33
 
31
34
  // No shell: process.execPath runs scripts and npm's cli.js directly, which is safe for
32
35
  // paths containing spaces on every platform (shell:true concatenates args unescaped).
33
- async function runNode(args) {
36
+ async function runNode(/** @type {string[]} */ args) {
34
37
  const { stderr, stdout } = await execFile(process.execPath, args, {
35
38
  cwd,
36
39
  maxBuffer: 20 * 1024 * 1024,
37
40
  });
38
41
  // Forward output on success too: install-time diagnostics such as the
39
42
  // concurrent-swap race-loss warning are otherwise swallowed.
40
- if (stdout) {
43
+ if (stdout !== "") {
41
44
  process.stdout.write(stdout);
42
45
  }
43
- if (stderr) {
46
+ if (stderr !== "") {
44
47
  process.stderr.write(stderr);
45
48
  }
46
49
  }
@@ -51,10 +54,13 @@ async function main() {
51
54
  return;
52
55
  }
53
56
  const npmExecPath = process.env.npm_execpath;
54
- if (!npmExecPath) {
57
+ if (npmExecPath === undefined || npmExecPath === "") {
55
58
  throw new Error(`npm_execpath is not set; run "npm install" manually in ${cwd} and retry.`);
56
59
  }
57
- const { devDependencies } = JSON.parse(await readFile(join(cwd, "package.json"), "utf8"));
60
+ /** @type {{ devDependencies: { typescript: string } }} */
61
+ // oxlint-disable-next-line typescript/no-unsafe-assignment
62
+ const pkg = JSON.parse(await readFile(join(cwd, "package.json"), "utf8"));
63
+ const { devDependencies } = pkg;
58
64
  const prefix = await mkdtemp(join(tmpdir(), "pi-subagents-tsc-"));
59
65
  try {
60
66
  await runNode([
@@ -75,13 +81,15 @@ async function main() {
75
81
  }
76
82
  }
77
83
 
78
- main().catch((error) => {
79
- if (error?.stdout) {
80
- process.stdout.write(error.stdout);
84
+ main().catch((/** @type {unknown} */ error) => {
85
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- exec rejections carry partial output
86
+ const output = /** @type {{ stdout?: string, stderr?: string }} */ (error);
87
+ if (output.stdout !== undefined && output.stdout !== "") {
88
+ process.stdout.write(output.stdout);
81
89
  }
82
- if (error?.stderr) {
83
- process.stderr.write(error.stderr);
90
+ if (output.stderr !== undefined && output.stderr !== "") {
91
+ process.stderr.write(output.stderr);
84
92
  }
85
- console.error(error instanceof Error ? error.message : String(error));
93
+ console.error(error instanceof Error ? error.message : JSON.stringify(error));
86
94
  process.exitCode = 1;
87
95
  });