@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.
- package/CHANGELOG.md +26 -0
- package/dist/build-info.js +1 -1
- package/docs/code-quality.md +44 -0
- 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
|
@@ -46,7 +46,9 @@ try {
|
|
|
46
46
|
process.kill(pid, 0);
|
|
47
47
|
throw new Error("Legacy helper is still running; stop it before preflight.");
|
|
48
48
|
} catch (error) {
|
|
49
|
-
if (
|
|
49
|
+
if (
|
|
50
|
+
!(typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH")
|
|
51
|
+
) {
|
|
50
52
|
throw error;
|
|
51
53
|
}
|
|
52
54
|
}
|
|
@@ -68,23 +70,22 @@ try {
|
|
|
68
70
|
if (!isRecord(raw) || typeof raw.timestamp !== "string") {
|
|
69
71
|
throw new Error("Invalid audit record.");
|
|
70
72
|
}
|
|
71
|
-
const
|
|
72
|
-
const timestamp = Date.parse(entry.timestamp);
|
|
73
|
+
const timestamp = Date.parse(raw.timestamp);
|
|
73
74
|
if (!Number.isSafeInteger(timestamp)) {
|
|
74
75
|
throw new Error("Audit record has no valid timestamp.");
|
|
75
76
|
}
|
|
76
77
|
if (timestamp < coveredSince || timestamp > stoppedAt) {
|
|
77
78
|
continue;
|
|
78
79
|
}
|
|
79
|
-
if (
|
|
80
|
+
if (raw.event === "log_overflow") {
|
|
80
81
|
throw new Error(
|
|
81
82
|
"Audit overflow prevents complete-history migration; use conservative startup.",
|
|
82
83
|
);
|
|
83
84
|
}
|
|
84
85
|
if (
|
|
85
|
-
|
|
86
|
-
typeof
|
|
87
|
-
outcomes.has(
|
|
86
|
+
raw.event === "wake_delivery" &&
|
|
87
|
+
typeof raw.result === "string" &&
|
|
88
|
+
outcomes.has(raw.result)
|
|
88
89
|
) {
|
|
89
90
|
attempts.push(timestamp);
|
|
90
91
|
}
|
|
@@ -107,6 +108,6 @@ try {
|
|
|
107
108
|
}),
|
|
108
109
|
);
|
|
109
110
|
} catch (error) {
|
|
110
|
-
console.error(error.message);
|
|
111
|
+
console.error(error instanceof Error ? error.message : JSON.stringify(error));
|
|
111
112
|
process.exitCode = 1;
|
|
112
113
|
}
|
|
@@ -23,6 +23,7 @@ Acceptance is not native journal persistence, model start, or task completion.
|
|
|
23
23
|
No automatic retries: a lost mutation response means UNKNOWN, not unsent.`);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** @type {Record<string, string[]>} */
|
|
26
27
|
const actions = {
|
|
27
28
|
register: [],
|
|
28
29
|
list: [],
|
|
@@ -30,7 +31,7 @@ const actions = {
|
|
|
30
31
|
inbox: ["after", "limit", "wait-ms"],
|
|
31
32
|
send: ["to", "message", "id", "passive", "expects-reply", "reply-to"],
|
|
32
33
|
};
|
|
33
|
-
function messageId(id) {
|
|
34
|
+
function messageId(/** @type {string | undefined} */ id) {
|
|
34
35
|
// oxlint-disable-next-line no-control-regex
|
|
35
36
|
const controls = /[\u0000-\u001f\u007f-\u009f]/;
|
|
36
37
|
if (id !== undefined && (id.trim() === "" || id.length > 512 || controls.test(id))) {
|
|
@@ -38,18 +39,34 @@ function messageId(id) {
|
|
|
38
39
|
}
|
|
39
40
|
return id;
|
|
40
41
|
}
|
|
41
|
-
function
|
|
42
|
+
function requireSendOptions(/** @type {Record<string, string | boolean | undefined>} */ options) {
|
|
43
|
+
const to = options.to;
|
|
44
|
+
const message = options.message;
|
|
45
|
+
if (
|
|
46
|
+
typeof to !== "string" ||
|
|
47
|
+
typeof message !== "string" ||
|
|
48
|
+
to.trim() === "" ||
|
|
49
|
+
message.trim() === ""
|
|
50
|
+
) {
|
|
51
|
+
throw new Error("--to and --message are required.");
|
|
52
|
+
}
|
|
53
|
+
return { to, message };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function requestBody(
|
|
57
|
+
/** @type {string} */ action,
|
|
58
|
+
/** @type {Record<string, string | boolean | undefined>} */ options,
|
|
59
|
+
/** @type {string[]} */ ids,
|
|
60
|
+
) {
|
|
42
61
|
if (action === "send") {
|
|
43
|
-
|
|
44
|
-
throw new Error("--to and --message are required.");
|
|
45
|
-
}
|
|
62
|
+
const { to, message } = requireSendOptions(options);
|
|
46
63
|
return {
|
|
47
|
-
to
|
|
48
|
-
message
|
|
49
|
-
messageId: messageId(options.id),
|
|
64
|
+
to,
|
|
65
|
+
message,
|
|
66
|
+
messageId: messageId(typeof options.id === "string" ? options.id : undefined),
|
|
50
67
|
passive: options.passive,
|
|
51
68
|
expectsReply: options["expects-reply"],
|
|
52
|
-
replyTo: messageId(options["reply-to"]),
|
|
69
|
+
replyTo: messageId(typeof options["reply-to"] === "string" ? options["reply-to"] : undefined),
|
|
53
70
|
};
|
|
54
71
|
}
|
|
55
72
|
if (action === "ack") {
|
|
@@ -62,10 +79,10 @@ function requestBody(action, options, ids) {
|
|
|
62
79
|
}
|
|
63
80
|
|
|
64
81
|
function tlsFiles() {
|
|
65
|
-
const files = {};
|
|
82
|
+
/** @type {Record<string, Buffer>} */ const files = {};
|
|
66
83
|
for (const field of ["CA", "CERT", "KEY"]) {
|
|
67
84
|
const path = process.env[`INTERCOM_BRIDGE_${field}`];
|
|
68
|
-
if (
|
|
85
|
+
if (path === undefined || path === "") {
|
|
69
86
|
throw new Error(`INTERCOM_BRIDGE_${field} is required.`);
|
|
70
87
|
}
|
|
71
88
|
if (field === "KEY") {
|
|
@@ -76,17 +93,18 @@ function tlsFiles() {
|
|
|
76
93
|
return files;
|
|
77
94
|
}
|
|
78
95
|
|
|
79
|
-
function privateKey(path) {
|
|
96
|
+
function privateKey(/** @type {string} */ path) {
|
|
80
97
|
const st = lstatSync(path);
|
|
81
|
-
|
|
98
|
+
const uid = process.getuid?.();
|
|
99
|
+
if (uid === undefined || !st.isFile() || st.uid !== uid || (st.mode & 0o077) !== 0) {
|
|
82
100
|
throw new Error("Client key must be an owned private regular file (0600).");
|
|
83
101
|
}
|
|
84
102
|
}
|
|
85
103
|
|
|
86
|
-
function inboxQuery(options) {
|
|
87
|
-
const after = options.after
|
|
88
|
-
const limit = options.limit
|
|
89
|
-
const waitMs = options["wait-ms"]
|
|
104
|
+
function inboxQuery(/** @type {Record<string, string | boolean | undefined>} */ options) {
|
|
105
|
+
const after = typeof options.after === "string" ? options.after : "0";
|
|
106
|
+
const limit = typeof options.limit === "string" ? options.limit : "256";
|
|
107
|
+
const waitMs = typeof options["wait-ms"] === "string" ? options["wait-ms"] : "0";
|
|
90
108
|
if (
|
|
91
109
|
!/^(0|[1-9]\d{0,15})$/.test(after) ||
|
|
92
110
|
!Number.isSafeInteger(Number(after)) ||
|
|
@@ -105,7 +123,10 @@ function inboxQuery(options) {
|
|
|
105
123
|
return query.toString();
|
|
106
124
|
}
|
|
107
125
|
|
|
108
|
-
|
|
126
|
+
/** @type {unknown} */ let body;
|
|
127
|
+
/** @type {string} */ let action = "";
|
|
128
|
+
/** @type {URL | undefined} */ let origin;
|
|
129
|
+
/** @type {string} */ let query = "";
|
|
109
130
|
try {
|
|
110
131
|
const parsed = parseArgs({
|
|
111
132
|
allowPositionals: true,
|
|
@@ -123,7 +144,7 @@ try {
|
|
|
123
144
|
limit: { type: "string" },
|
|
124
145
|
},
|
|
125
146
|
});
|
|
126
|
-
if (parsed.values.help) {
|
|
147
|
+
if (parsed.values.help === true) {
|
|
127
148
|
help();
|
|
128
149
|
process.exit(0);
|
|
129
150
|
}
|
|
@@ -138,26 +159,31 @@ try {
|
|
|
138
159
|
}
|
|
139
160
|
names.add(token.name);
|
|
140
161
|
}
|
|
141
|
-
[action] = parsed.positionals;
|
|
162
|
+
[action = ""] = parsed.positionals;
|
|
142
163
|
if (!Object.hasOwn(actions, action)) {
|
|
143
164
|
throw new Error("Choose an action; use --help.");
|
|
144
165
|
}
|
|
166
|
+
const allowedKeys = actions[action];
|
|
145
167
|
if (
|
|
146
|
-
|
|
168
|
+
allowedKeys === undefined ||
|
|
169
|
+
Object.keys(options).some((key) => !allowedKeys.includes(key)) ||
|
|
147
170
|
(action !== "ack" && parsed.positionals.length !== 1)
|
|
148
171
|
) {
|
|
149
172
|
throw new Error("Unexpected arguments for this action.");
|
|
150
173
|
}
|
|
151
174
|
body = requestBody(action, options, parsed.positionals.slice(1));
|
|
152
175
|
query = action === "inbox" ? inboxQuery(options) : "";
|
|
153
|
-
|
|
176
|
+
const bridgeUrl = process.env.INTERCOM_BRIDGE_URL;
|
|
177
|
+
origin = new URL(
|
|
178
|
+
bridgeUrl !== undefined && bridgeUrl !== "" ? bridgeUrl : "https://127.0.0.1:9443",
|
|
179
|
+
);
|
|
154
180
|
if (
|
|
155
181
|
origin.protocol !== "https:" ||
|
|
156
|
-
origin.username ||
|
|
157
|
-
origin.password ||
|
|
182
|
+
origin.username !== "" ||
|
|
183
|
+
origin.password !== "" ||
|
|
158
184
|
origin.pathname !== "/" ||
|
|
159
|
-
origin.search ||
|
|
160
|
-
origin.hash
|
|
185
|
+
origin.search !== "" ||
|
|
186
|
+
origin.hash !== ""
|
|
161
187
|
) {
|
|
162
188
|
throw new Error("INTERCOM_BRIDGE_URL must be an HTTPS origin, without credentials or a path.");
|
|
163
189
|
}
|
|
@@ -180,7 +206,12 @@ try {
|
|
|
180
206
|
origin.search = query;
|
|
181
207
|
const payload = JSON.stringify(body);
|
|
182
208
|
const get = ["list", "inbox"].includes(action);
|
|
183
|
-
|
|
209
|
+
/**
|
|
210
|
+
* @typedef {{ status: number | undefined, value: unknown }} BridgeReply
|
|
211
|
+
*/
|
|
212
|
+
/** @type {{ promise: Promise<BridgeReply>, resolve: (value: BridgeReply) => void, reject: (reason?: unknown) => void }} */
|
|
213
|
+
const { promise: reply, resolve, reject } = Promise.withResolvers();
|
|
214
|
+
{
|
|
184
215
|
const req = https.request(
|
|
185
216
|
origin,
|
|
186
217
|
{
|
|
@@ -196,9 +227,9 @@ try {
|
|
|
196
227
|
: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) },
|
|
197
228
|
},
|
|
198
229
|
(res) => {
|
|
199
|
-
const chunks = [];
|
|
230
|
+
/** @type {Buffer[]} */ const chunks = [];
|
|
200
231
|
let bytes = 0;
|
|
201
|
-
res.on("data", (chunk) => {
|
|
232
|
+
res.on("data", (/** @type {Buffer} */ chunk) => {
|
|
202
233
|
bytes += chunk.length;
|
|
203
234
|
if (bytes > 16 * 1024 * 1024) {
|
|
204
235
|
req.destroy(new Error("Bridge response exceeds 16 MiB."));
|
|
@@ -225,9 +256,15 @@ try {
|
|
|
225
256
|
req.once("close", () => clearTimeout(timer));
|
|
226
257
|
req.once("error", reject);
|
|
227
258
|
req.end(get ? undefined : payload);
|
|
228
|
-
}
|
|
259
|
+
}
|
|
260
|
+
const result = await reply;
|
|
229
261
|
console.log(JSON.stringify(result.value, null, 2));
|
|
230
|
-
|
|
262
|
+
const ok =
|
|
263
|
+
typeof result.value === "object" &&
|
|
264
|
+
result.value !== null &&
|
|
265
|
+
"ok" in result.value &&
|
|
266
|
+
result.value.ok === true;
|
|
267
|
+
if ((result.status !== undefined && (result.status < 200 || result.status >= 300)) || !ok) {
|
|
231
268
|
process.exitCode = 1;
|
|
232
269
|
}
|
|
233
270
|
} catch (error) {
|
|
@@ -13,7 +13,7 @@ if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
|
13
13
|
process.exit(0);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function isolatedEnv(home) {
|
|
16
|
+
function isolatedEnv(/** @type {string} */ home) {
|
|
17
17
|
const env = { ...process.env };
|
|
18
18
|
for (const key of Object.keys(env)) {
|
|
19
19
|
if (key.startsWith("PI_SUBAGENT_") || key.startsWith("PI_INTERCOM_")) {
|
|
@@ -33,7 +33,7 @@ function isolatedEnv(home) {
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function runPi(args, env) {
|
|
36
|
+
function runPi(/** @type {string[]} */ args, /** @type {NodeJS.ProcessEnv} */ env) {
|
|
37
37
|
const result = spawnSync(process.execPath, [hostCli, ...args], {
|
|
38
38
|
cwd: process.cwd(),
|
|
39
39
|
env,
|
|
@@ -42,12 +42,15 @@ function runPi(args, env) {
|
|
|
42
42
|
});
|
|
43
43
|
return {
|
|
44
44
|
ok: !result.error && result.status === 0,
|
|
45
|
-
output: result.error ? result.error.message : `${result.stdout
|
|
45
|
+
output: result.error ? result.error.message : `${result.stdout}${result.stderr}`,
|
|
46
46
|
status: result.status,
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function requireSuccess(
|
|
50
|
+
function requireSuccess(
|
|
51
|
+
/** @type {string} */ label,
|
|
52
|
+
/** @type {{ ok: boolean, output: string, status: number | null }} */ result,
|
|
53
|
+
) {
|
|
51
54
|
if (result.ok) {
|
|
52
55
|
return;
|
|
53
56
|
}
|
|
@@ -71,7 +74,9 @@ try {
|
|
|
71
74
|
"[local-install-smoke] installed and listed the single pi-subagents package with bundled intercom",
|
|
72
75
|
);
|
|
73
76
|
} catch (error) {
|
|
74
|
-
console.error(
|
|
77
|
+
console.error(
|
|
78
|
+
`[local-install-smoke] ${error instanceof Error ? error.message : JSON.stringify(error)}`,
|
|
79
|
+
);
|
|
75
80
|
process.exitCode = 1;
|
|
76
81
|
} finally {
|
|
77
82
|
rmSync(home, { recursive: true, force: true });
|
package/scripts/measure-size.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { extname, join } from "node:path";
|
|
|
4
4
|
|
|
5
5
|
const roots = ["src", "scripts", "generated"];
|
|
6
6
|
const extensions = new Set([".ts", ".js", ".mjs", ".cjs", ".json", ".sh"]);
|
|
7
|
+
/** @type {Record<string, number>} */
|
|
7
8
|
const ceilings = { productionBytes: 1048576 };
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -47,6 +48,7 @@ const rows = await Promise.all(
|
|
|
47
48
|
return { path, lines: lineCount(content.toString("utf8")), bytes: content.byteLength };
|
|
48
49
|
}),
|
|
49
50
|
);
|
|
51
|
+
/** @type {Record<string, number>} */
|
|
50
52
|
const measured = {
|
|
51
53
|
sourceLines: rows
|
|
52
54
|
.filter((row) => row.path.startsWith("src/") && row.path.endsWith(".ts"))
|
|
@@ -56,7 +58,9 @@ const measured = {
|
|
|
56
58
|
};
|
|
57
59
|
console.log(JSON.stringify({ files: rows.length, measured, ceilings }, null, 2));
|
|
58
60
|
if (!process.argv.includes("--report")) {
|
|
59
|
-
const exceeded = Object.keys(ceilings).filter(
|
|
61
|
+
const exceeded = Object.keys(ceilings).filter(
|
|
62
|
+
(key) => (measured[key] ?? 0) > (ceilings[key] ?? Number.POSITIVE_INFINITY),
|
|
63
|
+
);
|
|
60
64
|
if (exceeded.length > 0) {
|
|
61
65
|
console.error(`Shrink budget exceeded: ${exceeded.join(", ")}`);
|
|
62
66
|
process.exitCode = 1;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Journal and observation assertions shared with the packed-native smoke script.
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Assert the child journal records its native tool result and completion entry.
|
|
7
|
+
* @param {string} sessionFile
|
|
8
|
+
* @param {string | undefined} terminalEntryId
|
|
9
|
+
* @param {string} completionText
|
|
10
|
+
*/
|
|
11
|
+
export function assertChildJournal(sessionFile, terminalEntryId, completionText) {
|
|
12
|
+
const childEntries = readFileSync(sessionFile, "utf8")
|
|
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
|
|
22
|
+
assert.ok(
|
|
23
|
+
childEntries.some(
|
|
24
|
+
(entry) =>
|
|
25
|
+
entry.type === "message" &&
|
|
26
|
+
entry.message?.role === "toolResult" &&
|
|
27
|
+
entry.message?.toolName === "packed_probe" &&
|
|
28
|
+
entry.message?.content?.some((part) => part.text === "PACKED_TOOL_RESULT") === true,
|
|
29
|
+
),
|
|
30
|
+
"actual child journal contains its native tool result",
|
|
31
|
+
);
|
|
32
|
+
assert.ok(
|
|
33
|
+
childEntries.some(
|
|
34
|
+
(entry) =>
|
|
35
|
+
entry.id === terminalEntryId &&
|
|
36
|
+
entry.type === "message" &&
|
|
37
|
+
entry.message?.role === "assistant" &&
|
|
38
|
+
entry.message?.content?.some((part) => part.text === completionText) === true,
|
|
39
|
+
),
|
|
40
|
+
"completion references an actually published child assistant entry",
|
|
41
|
+
);
|
|
42
|
+
// oxlint-enable typescript/no-unnecessary-condition
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Assert the recorded observations show the expected broker conversation shape.
|
|
47
|
+
* @param {string} observations
|
|
48
|
+
*/
|
|
49
|
+
export function assertObservations(observations) {
|
|
50
|
+
/** @type {Record<string, unknown>[]} */
|
|
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));
|
|
57
|
+
const matches = (/** @type {(item: Record<string, unknown>) => boolean} */ predicate) =>
|
|
58
|
+
observed.filter(predicate).length;
|
|
59
|
+
assert.equal(
|
|
60
|
+
matches((item) => item.child === true && item.modelBound === true),
|
|
61
|
+
1,
|
|
62
|
+
);
|
|
63
|
+
assert.equal(
|
|
64
|
+
matches((item) => item.child === true && item.toolExecuted === true),
|
|
65
|
+
1,
|
|
66
|
+
);
|
|
67
|
+
assert.equal(
|
|
68
|
+
matches((item) => item.child === true && item.consumedResult === true && item.calls === 2),
|
|
69
|
+
1,
|
|
70
|
+
);
|
|
71
|
+
assert.equal(
|
|
72
|
+
matches((item) => item.child !== true && item.consumedResult === true && item.calls === 2),
|
|
73
|
+
1,
|
|
74
|
+
);
|
|
75
|
+
assert.equal(
|
|
76
|
+
matches((item) => item.child !== true && item.consumedCompletion === true && item.calls === 3),
|
|
77
|
+
1,
|
|
78
|
+
"the parent is resumed exactly once by the native completion notification",
|
|
79
|
+
);
|
|
80
|
+
assert.equal(
|
|
81
|
+
observed.some((item) => item.networkAttempt === true),
|
|
82
|
+
false,
|
|
83
|
+
);
|
|
84
|
+
}
|