@fitchmultz/pi-subagents 0.52.16 → 0.52.17
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/agent/host.js +1 -1
- package/dist/agent/notifications.js +10 -8
- package/dist/agent/observe-layout.js +3 -3
- package/dist/agent/parking.js +0 -1
- package/dist/agent/process.js +1 -4
- package/dist/agent/runner.js +0 -2
- package/dist/agent/shutdown.js +0 -1
- package/dist/agent/typebox.js +2 -2
- package/dist/build-info.js +1 -1
- package/dist/pi-intercom/bridge-presence.js +0 -1
- package/dist/pi-intercom/bridge-protocol.js +0 -2
- package/dist/pi-intercom/bridge-session.js +0 -1
- package/dist/pi-intercom/bridge-wake.js +0 -1
- package/dist/pi-intercom/broker/client.js +0 -2
- package/dist/pi-intercom/broker/spawn.js +0 -3
- package/dist/pi-intercom/tool-registration.js +1 -2
- package/dist/pi-intercom/validation.js +0 -2
- package/dist/shared/cooperative.js +0 -1
- package/docs/code-quality.md +96 -68
- package/package.json +9 -6
- package/scripts/build.mjs +11 -13
- package/scripts/check-compat.mjs +3 -2
- package/scripts/compat-host.mjs +5 -5
- package/scripts/intercom-bridge-credentials.mjs +4 -3
- package/scripts/intercom-remote.mjs +0 -1
- package/scripts/native-package-smoke-assertions.mjs +25 -23
- package/scripts/native-package-smoke.mjs +38 -54
- package/scripts/package-smoke.mjs +39 -30
- package/scripts/prepare.mjs +16 -10
- package/scripts/real-pi-events.mjs +92 -0
- package/scripts/real-pi-live.mjs +28 -108
- package/scripts/real-pi-processes.mjs +77 -82
- package/scripts/real-pi-smoke.mjs +20 -18
- package/src/agent/host.ts +1 -1
- package/src/agent/notifications.ts +11 -8
- package/src/agent/observe-layout.ts +3 -3
- package/src/agent/parking.ts +0 -1
- package/src/agent/process.ts +1 -4
- package/src/agent/runner.ts +0 -2
- package/src/agent/shutdown.ts +0 -1
- package/src/agent/typebox.ts +2 -2
- package/src/pi-intercom/bridge-presence.ts +0 -1
- package/src/pi-intercom/bridge-protocol.ts +0 -2
- package/src/pi-intercom/bridge-session.ts +0 -1
- package/src/pi-intercom/bridge-wake.ts +0 -1
- package/src/pi-intercom/broker/client.ts +0 -2
- package/src/pi-intercom/broker/spawn.ts +0 -3
- package/src/pi-intercom/tool-registration.ts +1 -2
- package/src/pi-intercom/validation.ts +0 -2
- package/src/shared/cooperative.ts +0 -1
|
@@ -161,8 +161,7 @@ function issueCertificate(
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
function readConfig(/** @type {string} */ path) {
|
|
164
|
-
/** @type {
|
|
165
|
-
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
164
|
+
/** @type {unknown} */
|
|
166
165
|
const config = JSON.parse(readFileSync(path, "utf8"));
|
|
167
166
|
if (!isRecord(config) || !Array.isArray(config.clients) || typeof config.ca !== "string") {
|
|
168
167
|
throw new Error("Invalid clients allowlist.");
|
|
@@ -178,7 +177,9 @@ function readConfig(/** @type {string} */ path) {
|
|
|
178
177
|
}
|
|
179
178
|
return { ...client, name: client.name, cwd: client.cwd, fingerprint256: client.fingerprint256 };
|
|
180
179
|
});
|
|
181
|
-
|
|
180
|
+
/** @type {Record<string, unknown> & { ca: string, clients: typeof clients }} */
|
|
181
|
+
const validated = { ...config, ca: config.ca, clients };
|
|
182
|
+
return validated;
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
let args;
|
|
@@ -32,7 +32,6 @@ const actions = {
|
|
|
32
32
|
send: ["to", "message", "id", "passive", "expects-reply", "reply-to"],
|
|
33
33
|
};
|
|
34
34
|
function messageId(/** @type {string | undefined} */ id) {
|
|
35
|
-
// oxlint-disable-next-line no-control-regex
|
|
36
35
|
const controls = /[\u0000-\u001f\u007f-\u009f]/;
|
|
37
36
|
if (id !== undefined && (id.trim() === "" || id.length > 512 || controls.test(id))) {
|
|
38
37
|
throw new Error("Invalid message ID.");
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
// Journal and observation assertions shared with the packed-native smoke script.
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
|
+
import { isRecord, isUnknownArray } from "../src/shared/unknown.ts";
|
|
5
|
+
|
|
6
|
+
function jsonRecords(/** @type {string} */ file) {
|
|
7
|
+
return readFileSync(file, "utf8")
|
|
8
|
+
.trimEnd()
|
|
9
|
+
.split("\n")
|
|
10
|
+
.map((line) => {
|
|
11
|
+
/** @type {unknown} */ const entry = JSON.parse(line);
|
|
12
|
+
assert.ok(isRecord(entry));
|
|
13
|
+
return entry;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function containsText(/** @type {unknown} */ content, /** @type {string} */ text) {
|
|
18
|
+
return isUnknownArray(content) && content.some((part) => isRecord(part) && part.text === text);
|
|
19
|
+
}
|
|
4
20
|
|
|
5
21
|
/**
|
|
6
22
|
* Assert the child journal records its native tool result and completion entry.
|
|
@@ -9,23 +25,15 @@ import { readFileSync } from "node:fs";
|
|
|
9
25
|
* @param {string} completionText
|
|
10
26
|
*/
|
|
11
27
|
export function assertChildJournal(sessionFile, terminalEntryId, completionText) {
|
|
12
|
-
const childEntries =
|
|
13
|
-
.trimEnd()
|
|
14
|
-
.split("\n")
|
|
15
|
-
.map((line) =>
|
|
16
|
-
/** @type {{ id?: string, type?: string, message?: { role?: string, toolName?: string, content?: { text?: string }[] } }} */ (
|
|
17
|
-
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- journal line shape, guarded below
|
|
18
|
-
JSON.parse(line)
|
|
19
|
-
),
|
|
20
|
-
);
|
|
21
|
-
// oxlint-disable typescript/no-unnecessary-condition -- journal fields are optional under tsc
|
|
28
|
+
const childEntries = jsonRecords(sessionFile);
|
|
22
29
|
assert.ok(
|
|
23
30
|
childEntries.some(
|
|
24
31
|
(entry) =>
|
|
25
32
|
entry.type === "message" &&
|
|
26
|
-
entry.message
|
|
27
|
-
entry.message
|
|
28
|
-
entry.message
|
|
33
|
+
isRecord(entry.message) &&
|
|
34
|
+
entry.message.role === "toolResult" &&
|
|
35
|
+
entry.message.toolName === "packed_probe" &&
|
|
36
|
+
containsText(entry.message.content, "PACKED_TOOL_RESULT"),
|
|
29
37
|
),
|
|
30
38
|
"actual child journal contains its native tool result",
|
|
31
39
|
);
|
|
@@ -34,12 +42,12 @@ export function assertChildJournal(sessionFile, terminalEntryId, completionText)
|
|
|
34
42
|
(entry) =>
|
|
35
43
|
entry.id === terminalEntryId &&
|
|
36
44
|
entry.type === "message" &&
|
|
37
|
-
entry.message
|
|
38
|
-
entry.message
|
|
45
|
+
isRecord(entry.message) &&
|
|
46
|
+
entry.message.role === "assistant" &&
|
|
47
|
+
containsText(entry.message.content, completionText),
|
|
39
48
|
),
|
|
40
49
|
"completion references an actually published child assistant entry",
|
|
41
50
|
);
|
|
42
|
-
// oxlint-enable typescript/no-unnecessary-condition
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
/**
|
|
@@ -47,13 +55,7 @@ export function assertChildJournal(sessionFile, terminalEntryId, completionText)
|
|
|
47
55
|
* @param {string} observations
|
|
48
56
|
*/
|
|
49
57
|
export function assertObservations(observations) {
|
|
50
|
-
|
|
51
|
-
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
52
|
-
const observed = readFileSync(observations, "utf8")
|
|
53
|
-
.trimEnd()
|
|
54
|
-
.split("\n")
|
|
55
|
-
// oxlint-disable-next-line typescript/no-unsafe-return
|
|
56
|
-
.map((line) => JSON.parse(line));
|
|
58
|
+
const observed = jsonRecords(observations);
|
|
57
59
|
const matches = (/** @type {(item: Record<string, unknown>) => boolean} */ predicate) =>
|
|
58
60
|
observed.filter(predicate).length;
|
|
59
61
|
assert.equal(
|
|
@@ -19,9 +19,10 @@ 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
21
|
import { assertChildJournal, assertObservations } from "./native-package-smoke-assertions.mjs";
|
|
22
|
+
import { isRecord, isUnknownArray } from "../src/shared/unknown.ts";
|
|
23
|
+
import { isContextTarget } from "../src/pi-intercom/types.ts";
|
|
22
24
|
|
|
23
25
|
/** @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
|
|
25
26
|
const execFile = promisify(execFileCallback);
|
|
26
27
|
|
|
27
28
|
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
@@ -145,20 +146,9 @@ writeFileSync(
|
|
|
145
146
|
retry: { enabled: false },
|
|
146
147
|
}),
|
|
147
148
|
);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
/** @type {typeof import("@earendil-works/pi-ai")} */
|
|
152
|
-
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
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
|
-
*/
|
|
149
|
+
// compat-host verifies this specifier resolves to the selected native graph.
|
|
150
|
+
const sdk = await import("@earendil-works/pi-coding-agent");
|
|
151
|
+
const ai = await import("@earendil-works/pi-ai");
|
|
162
152
|
const selected = ai.fauxProvider({ provider: "packed-probe", models: [{ id: "native" }] });
|
|
163
153
|
const processes = new SmokeProcesses({
|
|
164
154
|
cwd: root,
|
|
@@ -191,7 +181,6 @@ try {
|
|
|
191
181
|
while (!existsSync(join(runtimeDir, "broker.pid"))) {
|
|
192
182
|
assert.ok(brokerError === undefined && Date.now() < deadline, brokerReadinessMessage());
|
|
193
183
|
// Observe the broker's publication, not a sleep-based startup assumption.
|
|
194
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
195
184
|
await delay(25);
|
|
196
185
|
}
|
|
197
186
|
const settingsManager = sdk.SettingsManager.create(root, agentDir, { projectTrusted: true });
|
|
@@ -249,7 +238,6 @@ try {
|
|
|
249
238
|
},
|
|
250
239
|
});
|
|
251
240
|
assert.deepEqual(
|
|
252
|
-
// oxlint-disable-next-line typescript/no-unnecessary-condition -- assert generic inference already matches
|
|
253
241
|
[...session.getActiveToolNames()].sort(),
|
|
254
242
|
expectedTools,
|
|
255
243
|
"exactly three eager package tools",
|
|
@@ -263,9 +251,8 @@ try {
|
|
|
263
251
|
const status = await execute("intercom", { action: "status" });
|
|
264
252
|
assert.match(JSON.stringify(status.content), /Connected: Yes/);
|
|
265
253
|
const runs = await execute("agent_runs", { action: "list" });
|
|
266
|
-
/** @type {
|
|
267
|
-
|
|
268
|
-
const listDetails = runs.details;
|
|
254
|
+
/** @type {unknown} */ const listDetails = runs.details;
|
|
255
|
+
assert.ok(isRecord(listDetails));
|
|
269
256
|
assert.equal(listDetails.total, 0);
|
|
270
257
|
assert.equal(session.model?.provider, "packed-probe");
|
|
271
258
|
assert.equal(session.model.id, "native");
|
|
@@ -274,9 +261,10 @@ try {
|
|
|
274
261
|
events.push(event);
|
|
275
262
|
});
|
|
276
263
|
watchdog = setTimeout(() => {
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
264
|
+
// Interrupt a stuck turn, retaining abort failures in the smoke's error evidence.
|
|
265
|
+
session?.abort().catch((/** @type {unknown} */ error) => {
|
|
266
|
+
errors.push(error);
|
|
267
|
+
});
|
|
280
268
|
}, 30_000);
|
|
281
269
|
await session.prompt("Exercise the packed native delegate.", { expandPromptTemplates: false });
|
|
282
270
|
const relevantEvents = () =>
|
|
@@ -299,7 +287,6 @@ try {
|
|
|
299
287
|
Date.now() < completionDeadline,
|
|
300
288
|
`native completion notification resumes the parent: ${JSON.stringify(session.messages.slice(-3))}`,
|
|
301
289
|
);
|
|
302
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
303
290
|
await delay(25);
|
|
304
291
|
}
|
|
305
292
|
await session.waitForIdle();
|
|
@@ -313,20 +300,24 @@ try {
|
|
|
313
300
|
(event) => event.type === "tool_execution_start" && event.toolName === "delegate",
|
|
314
301
|
);
|
|
315
302
|
assert.equal(starts.length, 1, "one real native delegate call, no fabricated wire events");
|
|
316
|
-
const receipt =
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
assert.ok(
|
|
322
|
-
assert.equal(
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const run = (await execute("agent_runs", { action: "inspect", id:
|
|
303
|
+
const receipt = events.find(
|
|
304
|
+
(event) => event.type === "tool_execution_end" && event.toolName === "delegate",
|
|
305
|
+
);
|
|
306
|
+
assert.ok(receipt?.type === "tool_execution_end" && !receipt.isError, JSON.stringify(receipt));
|
|
307
|
+
/** @type {unknown} */ const launchResult = receipt.result;
|
|
308
|
+
assert.ok(isRecord(launchResult) && isRecord(launchResult.details));
|
|
309
|
+
assert.equal(launchResult.details.receipt, "launch");
|
|
310
|
+
assert.ok(typeof launchResult.details.id === "string");
|
|
311
|
+
/** @type {unknown} */
|
|
312
|
+
const run = (await execute("agent_runs", { action: "inspect", id: launchResult.details.id }))
|
|
326
313
|
.details;
|
|
314
|
+
assert.ok(isRecord(run) && isRecord(run.output) && typeof run.sessionFile === "string");
|
|
327
315
|
assert.equal(run.status, "completed");
|
|
328
316
|
assert.equal(run.output.text, "PACKED_NATIVE_COMPLETE");
|
|
329
|
-
|
|
317
|
+
assert.ok(run.result === undefined || isRecord(run.result));
|
|
318
|
+
const terminalEntryId = run.result?.terminalEntryId;
|
|
319
|
+
assert.ok(terminalEntryId === undefined || typeof terminalEntryId === "string");
|
|
320
|
+
assertChildJournal(run.sessionFile, terminalEntryId, "PACKED_NATIVE_COMPLETE");
|
|
330
321
|
assertObservations(observations);
|
|
331
322
|
assert.equal(networkAttempts, 0);
|
|
332
323
|
assert.deepEqual(errors, []);
|
|
@@ -334,35 +325,30 @@ try {
|
|
|
334
325
|
if (consumerScript !== undefined) {
|
|
335
326
|
assert.ok(intercomEvents !== undefined, "expected the consumer observer extension");
|
|
336
327
|
assert.ok(consumerDir !== undefined, "expected the consumer script directory argument");
|
|
337
|
-
/** @type {
|
|
328
|
+
/** @type {unknown} */ let target;
|
|
338
329
|
intercomEvents.emit("pi-intercom:context-query", {
|
|
339
330
|
version: 1,
|
|
340
|
-
respond: (/** @type {
|
|
331
|
+
respond: (/** @type {unknown} */ snapshot) => {
|
|
341
332
|
target = snapshot;
|
|
342
333
|
},
|
|
343
334
|
});
|
|
344
|
-
assert.ok(
|
|
345
|
-
target?.connected === true &&
|
|
346
|
-
target.peerId !== undefined &&
|
|
347
|
-
target.rootSessionId !== undefined &&
|
|
348
|
-
target.incarnation !== undefined,
|
|
349
|
-
);
|
|
335
|
+
assert.ok(isRecord(target) && target.connected === true && isContextTarget(target));
|
|
350
336
|
const guard = {
|
|
351
337
|
rootSessionId: target.rootSessionId,
|
|
352
338
|
peerId: target.peerId,
|
|
353
339
|
incarnation: target.incarnation,
|
|
354
340
|
applicationGuard: "package-consent-1",
|
|
355
341
|
};
|
|
356
|
-
/** @type {
|
|
342
|
+
/** @type {unknown} */ let consent;
|
|
357
343
|
intercomEvents.emit("pi-intercom:application-guard", {
|
|
358
344
|
version: 1,
|
|
359
345
|
target: guard,
|
|
360
346
|
revision: guard.applicationGuard,
|
|
361
|
-
respond: (/** @type {
|
|
347
|
+
respond: (/** @type {unknown} */ result) => {
|
|
362
348
|
consent = result;
|
|
363
349
|
},
|
|
364
350
|
});
|
|
365
|
-
assert.ok(consent
|
|
351
|
+
assert.ok(isRecord(consent), "the consumer answered the guard query");
|
|
366
352
|
assert.equal(consent.applicationGuard, guard.applicationGuard);
|
|
367
353
|
const consumerHome = join(root, "consumer-home");
|
|
368
354
|
mkdirSync(consumerHome);
|
|
@@ -376,7 +362,7 @@ try {
|
|
|
376
362
|
PI_INTERCOM_RUNTIME_DIR: runtimeDir,
|
|
377
363
|
S3_ROOT: guard.rootSessionId,
|
|
378
364
|
S3_PEER: guard.peerId,
|
|
379
|
-
S3_INCARNATION:
|
|
365
|
+
S3_INCARNATION: guard.incarnation,
|
|
380
366
|
S3_GUARD: guard.applicationGuard,
|
|
381
367
|
S3_BROKER_PID: String(brokerPid),
|
|
382
368
|
},
|
|
@@ -391,7 +377,7 @@ try {
|
|
|
391
377
|
"--permission",
|
|
392
378
|
"--allow-fs-read=*",
|
|
393
379
|
"--allow-fs-write=*",
|
|
394
|
-
"--input-type=module",
|
|
380
|
+
"--input-type=module-typescript",
|
|
395
381
|
"-e",
|
|
396
382
|
readFileSync(consumerScript, "utf8"),
|
|
397
383
|
],
|
|
@@ -399,7 +385,6 @@ try {
|
|
|
399
385
|
);
|
|
400
386
|
process.stdout.write(stdout);
|
|
401
387
|
process.stderr.write(stderr);
|
|
402
|
-
// oxlint-disable-next-line typescript/no-unnecessary-condition -- assert generic inference already matches
|
|
403
388
|
assert.equal(events.filter((event) => event.type === "turn_start").length, turns);
|
|
404
389
|
} finally {
|
|
405
390
|
// The consumer's failure/timeout must never leave this fixture's broker stopped.
|
|
@@ -472,11 +457,10 @@ export default function(pi) {
|
|
|
472
457
|
"",
|
|
473
458
|
);
|
|
474
459
|
assert.doesNotMatch(output, /Failed to load extension|ERR_INTERNAL_ASSERTION|Extension error/);
|
|
475
|
-
/** @type {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
assert.deepEqual(cli.tools.sort(), expectedTools);
|
|
460
|
+
/** @type {unknown} */ const cli = JSON.parse(readFileSync(marker, "utf8"));
|
|
461
|
+
assert.ok(isRecord(cli) && isUnknownArray(cli.tools));
|
|
462
|
+
assert.ok(cli.tools.every((tool) => typeof tool === "string"));
|
|
463
|
+
assert.deepEqual([...cli.tools].sort(), expectedTools);
|
|
480
464
|
if (!existsSync(join(packageRoot, "node_modules/@earendil-works/pi-coding-agent"))) {
|
|
481
465
|
assert.equal(cli.sdkReused, true, "production-only installs reuse Pi's loaded SDK");
|
|
482
466
|
}
|
|
@@ -16,6 +16,9 @@ import { dirname, join, resolve } from "node:path";
|
|
|
16
16
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
17
|
import { hostRoot } from "./compat-host.mjs";
|
|
18
18
|
import { compilerInvocation } from "./typescript.mjs";
|
|
19
|
+
import { isRecord, isUnknownArray } from "../src/shared/unknown.ts";
|
|
20
|
+
import { Type } from "typebox";
|
|
21
|
+
import { Assert } from "typebox/value";
|
|
19
22
|
|
|
20
23
|
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
21
24
|
console.log(
|
|
@@ -25,9 +28,11 @@ if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
|
25
28
|
}
|
|
26
29
|
assert.ok(process.argv.length <= 3, "Expected at most one package tarball; use --help");
|
|
27
30
|
|
|
28
|
-
/** @type {
|
|
29
|
-
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
31
|
+
/** @type {unknown} */
|
|
30
32
|
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
33
|
+
assert.ok(isRecord(packageJson));
|
|
34
|
+
assert.ok(typeof packageJson.name === "string" && typeof packageJson.version === "string");
|
|
35
|
+
const packageName = packageJson.name;
|
|
31
36
|
const candidate = process.argv[2] !== undefined ? resolve(process.argv[2]) : undefined;
|
|
32
37
|
const root = mkdtempSync(join(tmpdir(), "ps-package-"));
|
|
33
38
|
const packDir = join(root, "pack");
|
|
@@ -55,9 +60,8 @@ function run(
|
|
|
55
60
|
return result.stdout;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
function verifyManifest(
|
|
59
|
-
|
|
60
|
-
) {
|
|
63
|
+
function verifyManifest(/** @type {unknown} */ manifest) {
|
|
64
|
+
assert.ok(isRecord(manifest) && isRecord(manifest.pi));
|
|
61
65
|
assert.equal(manifest.name, "@fitchmultz/pi-subagents");
|
|
62
66
|
assert.notEqual(manifest.private, true);
|
|
63
67
|
assert.deepEqual(manifest.publishConfig, { access: "public" });
|
|
@@ -87,7 +91,7 @@ function installProduction(/** @type {{ filename?: string, files?: { path: strin
|
|
|
87
91
|
],
|
|
88
92
|
installDir,
|
|
89
93
|
);
|
|
90
|
-
const installed = join(installDir, "node_modules",
|
|
94
|
+
const installed = join(installDir, "node_modules", packageName);
|
|
91
95
|
const standalone = join(root, "standalone");
|
|
92
96
|
cpSync(installed, standalone, { recursive: true });
|
|
93
97
|
run("npm", ["install", "--ignore-scripts", "--omit=dev", "--legacy-peer-deps"], standalone);
|
|
@@ -143,16 +147,14 @@ function verifyDeclarations(/** @type {string} */ consumerDir) {
|
|
|
143
147
|
async function verifyRuntime(
|
|
144
148
|
/** @type {{ packageRoot: string, consumerDir: string }} */ { packageRoot, consumerDir },
|
|
145
149
|
) {
|
|
146
|
-
/** @type {
|
|
147
|
-
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
150
|
+
/** @type {unknown} */
|
|
148
151
|
const manifest = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
149
152
|
verifyManifest(manifest);
|
|
150
153
|
verifyDeclarations(consumerDir);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const sdk = await import(pathToFileURL(join(hostRoot, "dist/index.js")).href);
|
|
154
|
+
// compat-host verifies this specifier resolves to the selected native graph.
|
|
155
|
+
const sdk = await import("@earendil-works/pi-coding-agent");
|
|
154
156
|
/** @type {typeof import("../src/agent/host.ts")} */
|
|
155
|
-
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
157
|
+
// oxlint-disable-next-line typescript/no-unsafe-assignment -- QX-004: load the selected runtime artifact with its authoritative source contract.
|
|
156
158
|
const nativeHost = await import(pathToFileURL(join(packageRoot, "dist/agent/host.js")).href);
|
|
157
159
|
assert.equal(
|
|
158
160
|
await nativeHost.loadSDK(hostRoot),
|
|
@@ -182,11 +184,10 @@ assert.equal(native.Assert, hostValue.Assert, "detached validation uses the sele
|
|
|
182
184
|
},
|
|
183
185
|
);
|
|
184
186
|
/** @type {typeof import("../src/pi-intercom/broker/spawn.ts")} */
|
|
185
|
-
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
187
|
+
// oxlint-disable-next-line typescript/no-unsafe-assignment -- QX-004: load the selected runtime artifact with its authoritative source contract.
|
|
186
188
|
const broker = await import(
|
|
187
189
|
pathToFileURL(join(packageRoot, "dist/pi-intercom/broker/spawn.js")).href
|
|
188
190
|
);
|
|
189
|
-
// oxlint-disable-next-line typescript/no-unnecessary-condition -- assert generic inference already matches
|
|
190
191
|
assert.equal(realpathSync(broker.getBrokerSpawnOptions().cwd), realpathSync(packageRoot));
|
|
191
192
|
const help = run(
|
|
192
193
|
process.execPath,
|
|
@@ -207,7 +208,9 @@ assert.equal(native.Assert, hostValue.Assert, "detached validation uses the sele
|
|
|
207
208
|
fileURLToPath(new URL("./native-package-smoke.mjs", import.meta.url)),
|
|
208
209
|
packageRoot,
|
|
209
210
|
consumerDir,
|
|
210
|
-
fileURLToPath(
|
|
211
|
+
fileURLToPath(
|
|
212
|
+
new URL("../test/fixtures/intercom-client-consumer.test-d.mts", import.meta.url),
|
|
213
|
+
),
|
|
211
214
|
],
|
|
212
215
|
root,
|
|
213
216
|
{
|
|
@@ -225,25 +228,32 @@ assert.equal(native.Assert, hostValue.Assert, "detached validation uses the sele
|
|
|
225
228
|
try {
|
|
226
229
|
verifyManifest(packageJson);
|
|
227
230
|
process.env.PI_PACKAGE_DIR = hostRoot;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
231
|
+
const packSchema = Type.Array(
|
|
232
|
+
Type.Object({
|
|
233
|
+
name: Type.String(),
|
|
234
|
+
version: Type.String(),
|
|
235
|
+
filename: Type.String(),
|
|
236
|
+
files: Type.Array(Type.Object({ path: Type.String() })),
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
/** @type {unknown} */
|
|
240
|
+
const report = JSON.parse(
|
|
241
|
+
run("npm", [
|
|
242
|
+
"pack",
|
|
243
|
+
"--json",
|
|
244
|
+
"--pack-destination",
|
|
245
|
+
packDir,
|
|
246
|
+
...(candidate !== undefined ? [candidate] : []),
|
|
247
|
+
]),
|
|
240
248
|
);
|
|
249
|
+
// npm 12 reports by package name; earlier npm versions report an array.
|
|
250
|
+
assert.ok(isRecord(report) || isUnknownArray(report));
|
|
251
|
+
const packReports = Object.values(report);
|
|
252
|
+
Assert(packSchema, packReports);
|
|
241
253
|
const pack = packReports[0];
|
|
242
254
|
assert.ok(pack !== undefined && Array.isArray(pack.files), "npm pack must report files");
|
|
243
255
|
if (candidate !== undefined) {
|
|
244
|
-
// oxlint-disable-next-line typescript/no-unnecessary-condition -- assert generic inference already matches
|
|
245
256
|
assert.equal(pack.name, packageJson.name);
|
|
246
|
-
// oxlint-disable-next-line typescript/no-unnecessary-condition -- assert generic inference already matches
|
|
247
257
|
assert.equal(pack.version, packageJson.version, "Candidate version differs from source");
|
|
248
258
|
assert.ok(
|
|
249
259
|
readFileSync(join(packDir, pack.filename)).equals(readFileSync(candidate)),
|
|
@@ -287,7 +297,6 @@ try {
|
|
|
287
297
|
}
|
|
288
298
|
for (const packageRoot of installProduction(pack)) {
|
|
289
299
|
// Qualify both the npm node_modules location and standalone git-style production graph.
|
|
290
|
-
// oxlint-disable-next-line no-await-in-loop
|
|
291
300
|
await verifyRuntime(packageRoot);
|
|
292
301
|
}
|
|
293
302
|
console.log(
|
package/scripts/prepare.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { execFile as execFileCallback } from "node:child_process";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
12
13
|
import { existsSync } from "node:fs";
|
|
13
14
|
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
14
15
|
import { tmpdir } from "node:os";
|
|
@@ -17,7 +18,6 @@ import process from "node:process";
|
|
|
17
18
|
import { promisify } from "node:util";
|
|
18
19
|
|
|
19
20
|
/** @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
|
|
21
21
|
const execFile = promisify(execFileCallback);
|
|
22
22
|
const cwd = process.cwd();
|
|
23
23
|
const buildScript = join(cwd, "scripts", "build.mjs");
|
|
@@ -57,10 +57,16 @@ async function main() {
|
|
|
57
57
|
if (npmExecPath === undefined || npmExecPath === "") {
|
|
58
58
|
throw new Error(`npm_execpath is not set; run "npm install" manually in ${cwd} and retry.`);
|
|
59
59
|
}
|
|
60
|
-
/** @type {
|
|
61
|
-
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
60
|
+
/** @type {unknown} */
|
|
62
61
|
const pkg = JSON.parse(await readFile(join(cwd, "package.json"), "utf8"));
|
|
62
|
+
assert.ok(typeof pkg === "object" && pkg !== null && "devDependencies" in pkg);
|
|
63
63
|
const { devDependencies } = pkg;
|
|
64
|
+
assert.ok(
|
|
65
|
+
typeof devDependencies === "object" &&
|
|
66
|
+
devDependencies !== null &&
|
|
67
|
+
"typescript" in devDependencies &&
|
|
68
|
+
typeof devDependencies.typescript === "string",
|
|
69
|
+
);
|
|
64
70
|
const prefix = await mkdtemp(join(tmpdir(), "pi-subagents-tsc-"));
|
|
65
71
|
try {
|
|
66
72
|
await runNode([
|
|
@@ -82,13 +88,13 @@ async function main() {
|
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
main().catch((/** @type {unknown} */ error) => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
if (typeof error === "object" && error !== null) {
|
|
92
|
+
if ("stdout" in error && typeof error.stdout === "string") {
|
|
93
|
+
process.stdout.write(error.stdout);
|
|
94
|
+
}
|
|
95
|
+
if ("stderr" in error && typeof error.stderr === "string") {
|
|
96
|
+
process.stderr.write(error.stderr);
|
|
97
|
+
}
|
|
92
98
|
}
|
|
93
99
|
console.error(error instanceof Error ? error.message : JSON.stringify(error));
|
|
94
100
|
process.exitCode = 1;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Decode the RPC evidence used by the standalone live smoke without requiring local Pi peers.
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { isRecord, isUnknownArray } from "../src/shared/unknown.ts";
|
|
4
|
+
|
|
5
|
+
/** @typedef {Record<string, unknown>} RpcEvent */
|
|
6
|
+
/**
|
|
7
|
+
* The fields consumed from a native launch/continuation receipt, plus stream position.
|
|
8
|
+
* @typedef {{ [K in "id" | "name" | "sessionFile" | "context" | "predecessor"]: import("../src/agent/contracts.ts").Launch[K] | undefined } & { receipt: string, tool: string, index: number }} SessionReceipt
|
|
9
|
+
*/
|
|
10
|
+
/** @typedef {ReturnType<typeof completionNotice>} SessionNotice */
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} SessionView
|
|
13
|
+
* @property {RpcEvent[]} events
|
|
14
|
+
* @property {SessionReceipt[]} receipts
|
|
15
|
+
* @property {Map<string | undefined, SessionNotice>} notices
|
|
16
|
+
* @property {{ index: number, text: string, stopReason: string | undefined }[]} replies
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {Record<string, unknown>} details
|
|
21
|
+
* @param {unknown} tool
|
|
22
|
+
* @param {number} index
|
|
23
|
+
* @returns {SessionReceipt}
|
|
24
|
+
*/
|
|
25
|
+
function sessionReceipt(details, tool, index) {
|
|
26
|
+
const { id, name, sessionFile, context, predecessor, receipt } = details;
|
|
27
|
+
assert.ok(id === undefined || typeof id === "string");
|
|
28
|
+
assert.ok(name === undefined || typeof name === "string");
|
|
29
|
+
assert.ok(sessionFile === undefined || typeof sessionFile === "string");
|
|
30
|
+
assert.ok(context === undefined || context === "fresh" || context === "fork");
|
|
31
|
+
assert.ok(predecessor === undefined || typeof predecessor === "string");
|
|
32
|
+
assert.ok(typeof receipt === "string" && typeof tool === "string");
|
|
33
|
+
return { id, name, sessionFile, context, predecessor, receipt, tool, index };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function completionNotice(/** @type {unknown} */ details, /** @type {number} */ index) {
|
|
37
|
+
assert.ok(isRecord(details));
|
|
38
|
+
const { id, result, page } = details;
|
|
39
|
+
assert.ok(id === undefined || typeof id === "string");
|
|
40
|
+
assert.ok(result === undefined || isRecord(result));
|
|
41
|
+
assert.ok(page === undefined || isRecord(page));
|
|
42
|
+
const status = result?.status;
|
|
43
|
+
const text = page?.text;
|
|
44
|
+
assert.ok(status === undefined || typeof status === "string");
|
|
45
|
+
assert.ok(text === undefined || typeof text === "string");
|
|
46
|
+
return { id, index, result: { status }, page: { text } };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function addMessage(
|
|
50
|
+
/** @type {SessionView} */ view,
|
|
51
|
+
/** @type {unknown} */ message,
|
|
52
|
+
/** @type {number} */ index,
|
|
53
|
+
) {
|
|
54
|
+
if (!isRecord(message)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (message.customType === "agent-completion" && message.details !== undefined) {
|
|
58
|
+
const notice = completionNotice(message.details, index);
|
|
59
|
+
view.notices.set(notice.id, notice);
|
|
60
|
+
} else if (message.role === "assistant") {
|
|
61
|
+
const content = message.content ?? [];
|
|
62
|
+
assert.ok(isUnknownArray(content));
|
|
63
|
+
const text = content
|
|
64
|
+
.filter(isRecord)
|
|
65
|
+
.filter((part) => part.type === "text")
|
|
66
|
+
.map((part) => {
|
|
67
|
+
assert.ok(part.text === undefined || typeof part.text === "string");
|
|
68
|
+
return part.text;
|
|
69
|
+
});
|
|
70
|
+
assert.ok(message.stopReason === undefined || typeof message.stopReason === "string");
|
|
71
|
+
view.replies.push({ index, text: text.join("\n"), stopReason: message.stopReason });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** @param {RpcEvent[]} events @returns {SessionView} */
|
|
76
|
+
export function sessionView(events) {
|
|
77
|
+
/** @type {SessionView} */
|
|
78
|
+
const view = { events, receipts: [], notices: new Map(), replies: [] };
|
|
79
|
+
for (const [index, event] of events.entries()) {
|
|
80
|
+
const details = isRecord(event.result) ? event.result.details : undefined;
|
|
81
|
+
if (event.type === "message_end") {
|
|
82
|
+
addMessage(view, event.message, index);
|
|
83
|
+
} else if (
|
|
84
|
+
event.type === "tool_execution_end" &&
|
|
85
|
+
isRecord(details) &&
|
|
86
|
+
(details.receipt === "launch" || details.receipt === "continuation")
|
|
87
|
+
) {
|
|
88
|
+
view.receipts.push(sessionReceipt(details, event.toolName, index));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return view;
|
|
92
|
+
}
|