@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.
@@ -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 (error.code !== "ESRCH") {
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 entry = raw;
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 (entry.event === "log_overflow") {
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
- entry.event === "wake_delivery" &&
86
- typeof entry.result === "string" &&
87
- outcomes.has(entry.result)
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 requestBody(action, options, ids) {
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
- if (!options.to?.trim() || !options.message?.trim()) {
44
- throw new Error("--to and --message are required.");
45
- }
62
+ const { to, message } = requireSendOptions(options);
46
63
  return {
47
- to: options.to,
48
- message: options.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 (!path) {
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
- if (!st.isFile() || st.uid !== process.getuid() || st.mode & 0o077) {
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 ?? "0";
88
- const limit = options.limit ?? "256";
89
- const waitMs = options["wait-ms"] ?? "0";
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
- let body, action, origin, query;
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
- Object.keys(options).some((key) => !actions[action].includes(key)) ||
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
- origin = new URL(process.env.INTERCOM_BRIDGE_URL || "https://127.0.0.1:9443");
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
- const result = await new Promise((resolve, reject) => {
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
- if (result.status < 200 || result.status >= 300 || result.value?.ok !== true) {
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 ?? ""}${result.stderr ?? ""}`,
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(label, result) {
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(`[local-install-smoke] ${error instanceof Error ? error.message : String(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 });
@@ -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((key) => measured[key] > ceilings[key]);
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
+ }