@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.
- package/CHANGELOG.md +13 -0
- package/dist/build-info.js +1 -1
- package/package.json +1 -1
- package/scripts/archive-retired-history.mjs +19 -12
- package/scripts/build.mjs +54 -27
- package/scripts/check-compat.mjs +18 -9
- package/scripts/compat-host.mjs +22 -10
- package/scripts/intercom-bridge-credentials.mjs +69 -25
- package/scripts/intercom-bridge-wake-preflight.mjs +9 -8
- package/scripts/intercom-remote.mjs +68 -31
- package/scripts/local-install-smoke.mjs +10 -5
- package/scripts/measure-size.mjs +5 -1
- package/scripts/native-package-smoke-assertions.mjs +84 -0
- package/scripts/native-package-smoke.mjs +122 -120
- package/scripts/package-smoke.mjs +44 -14
- package/scripts/prepare.mjs +20 -12
- package/scripts/real-pi-live.mjs +259 -69
- package/scripts/real-pi-processes.mjs +117 -54
- package/scripts/real-pi-smoke.mjs +56 -22
- package/scripts/run-tests.mjs +45 -17
- package/tsconfig.json +1 -1
|
@@ -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(
|
|
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:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
|
236
|
-
const events = [];
|
|
237
|
-
session.subscribe((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
|
-
|
|
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 =
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
)
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
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(
|
|
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 });
|
package/scripts/prepare.mjs
CHANGED
|
@@ -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 = (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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 (
|
|
83
|
-
process.stderr.write(
|
|
90
|
+
if (output.stderr !== undefined && output.stderr !== "") {
|
|
91
|
+
process.stderr.write(output.stderr);
|
|
84
92
|
}
|
|
85
|
-
console.error(error instanceof Error ? error.message :
|
|
93
|
+
console.error(error instanceof Error ? error.message : JSON.stringify(error));
|
|
86
94
|
process.exitCode = 1;
|
|
87
95
|
});
|