@narumitw/pi-subagents 3.0.2 → 3.1.1

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/dist/index.ts CHANGED
@@ -1,16 +1,34 @@
1
1
  // @generated by scripts/build-runtime.mjs; do not edit.
2
2
  // @ts-nocheck -- generated entries use .ts extensions for Pi's Jiti loader.
3
3
  import {
4
- BROKER_CREDENTIAL_FD,
5
4
  CHILD_COMMUNICATION_TOOL_NAMES,
5
+ prepareTimeoutArguments,
6
+ resolveTimeoutMs
7
+ } from "./chunks/chunk-ZOAFWLDJ.ts";
8
+ import {
9
+ addAttachmentIgnoreRules,
10
+ isAbortError,
11
+ isRecord,
12
+ isWithin,
13
+ realpath,
14
+ throwIfAttachmentAborted,
15
+ toPosixPath,
16
+ toolSourceId
17
+ } from "./chunks/chunk-3FCHXUKZ.ts";
18
+ import {
19
+ BROKER_CREDENTIAL_FD,
20
+ CHILD_READINESS_FD,
21
+ CHILD_READINESS_FD_ENV,
6
22
  MAX_IDENTIFIER_LENGTH,
7
23
  MAX_MESSAGE_BYTES,
24
+ MAX_READINESS_FRAME_BYTES,
8
25
  MessageBroker,
9
- brokerCredentialEnvironment,
26
+ assertChildBootstrapCapacity,
27
+ childBootstrapEnvironment,
10
28
  sanitizeTerminalText,
11
- serializeBrokerCredentials,
29
+ serializeChildBootstrap,
12
30
  validateMessage
13
- } from "./chunks/chunk-7JQOB375.ts";
31
+ } from "./chunks/chunk-YMVSJ5GE.ts";
14
32
 
15
33
  // src/completion-renderer.ts
16
34
  import { getMarkdownTheme, keyText } from "@earendil-works/pi-coding-agent";
@@ -102,34 +120,26 @@ import { StringDecoder } from "node:string_decoder";
102
120
  import { fileURLToPath } from "node:url";
103
121
  import { getPackageDir } from "@earendil-works/pi-coding-agent";
104
122
  var CORE_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
105
- var MAX_TIMEOUT_MS = 2147483647;
106
- var MAX_TIMEOUT_SECONDS = MAX_TIMEOUT_MS / 1e3;
107
123
  var MAX_OUTPUT_BYTES = 32 * 1024;
108
124
  var MAX_ERROR_BYTES = 8 * 1024;
109
125
  var MAX_EVENT_LINE_BYTES = 256 * 1024;
110
126
  var RPC_RESPONSE_TIMEOUT_MS = 3e4;
111
127
  var KILL_GRACE_MS = 1e3;
112
- function resolveTimeoutMs(timeout) {
113
- if (timeout === void 0) return void 0;
114
- if (!Number.isFinite(timeout) || timeout <= 0) {
115
- throw new Error("Invalid timeout: must be a finite number of seconds");
116
- }
117
- const timeoutMs = timeout * 1e3;
118
- if (timeoutMs > MAX_TIMEOUT_MS) {
119
- throw new Error(`Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`);
120
- }
121
- return timeoutMs;
122
- }
128
+ var MAX_WINDOWS_COMMAND_LINE_CODE_UNITS = 32767;
123
129
  async function runChild(request) {
124
130
  if (request.signal.aborted) return cancelledResult();
125
131
  try {
132
+ assertChildCommandCapacity(request);
126
133
  const invocation = resolvePiInvocation(buildPiArgs(request));
127
134
  return await executeProcess(invocation, request);
128
135
  } catch (error) {
129
136
  if (request.signal.aborted) return cancelledResult();
130
137
  return {
131
138
  state: "failed",
132
- error: truncateText(error instanceof Error ? error.message : String(error), MAX_ERROR_BYTES).text,
139
+ error: truncateText(
140
+ redactAttachmentPaths(error instanceof Error ? error.message : String(error), request),
141
+ MAX_ERROR_BYTES
142
+ ).text,
133
143
  limitations: [],
134
144
  truncated: false
135
145
  };
@@ -144,31 +154,106 @@ function buildPiArgs(request) {
144
154
  "--no-skills",
145
155
  "--no-prompt-templates",
146
156
  "-e",
147
- childCommunicationBridgePath(),
157
+ childCommunicationBridgePath()
158
+ ];
159
+ for (const extension of request.extensions) args.push("-e", extension.path);
160
+ if (requiresReadinessAttestation(request)) args.push("-e", childReadinessProbePath());
161
+ for (const skill of request.skills) args.push("--skill", skill);
162
+ args.push(
148
163
  "--model",
149
164
  request.model,
150
165
  "--thinking",
151
166
  request.thinkingLevel,
152
167
  request.projectTrusted ? "--approve" : "--no-approve"
153
- ];
154
- const tools = [.../* @__PURE__ */ new Set([...request.tools, ...CHILD_COMMUNICATION_TOOL_NAMES])];
155
- args.push("--tools", tools.join(","));
168
+ );
169
+ args.push("--tools", selectedChildTools(request).join(","));
156
170
  return args;
157
171
  }
172
+ function assertChildCommandCapacity(request, platform = process.platform) {
173
+ if (platform !== "win32") return;
174
+ const invocation = resolvePiInvocation(buildPiArgs(request));
175
+ const commandLineLength = [invocation.command, ...invocation.args].map(quoteWindowsArgument).join(" ").length;
176
+ if (commandLineLength + 1 > MAX_WINDOWS_COMMAND_LINE_CODE_UNITS) {
177
+ throw new Error("Subagent child command line exceeds the Windows process limit.");
178
+ }
179
+ }
180
+ function quoteWindowsArgument(value) {
181
+ if (value.length > 0 && !/[\s"]/u.test(value)) return value;
182
+ let quoted = '"';
183
+ let backslashes = 0;
184
+ for (const character of value) {
185
+ if (character === "\\") {
186
+ backslashes++;
187
+ continue;
188
+ }
189
+ if (character === '"') {
190
+ quoted += `${"\\".repeat(backslashes * 2 + 1)}"`;
191
+ } else {
192
+ quoted += `${"\\".repeat(backslashes)}${character}`;
193
+ }
194
+ backslashes = 0;
195
+ }
196
+ return `${quoted}${"\\".repeat(backslashes * 2)}"`;
197
+ }
158
198
  function childCommunicationBridgePath() {
159
199
  return fileURLToPath(new URL("./child-communication-bridge.ts", import.meta.url));
160
200
  }
201
+ function childReadinessProbePath() {
202
+ return fileURLToPath(new URL("./child-readiness-probe.ts", import.meta.url));
203
+ }
204
+ function requiresReadinessAttestation(request) {
205
+ return request.extensions.length > 0;
206
+ }
207
+ function selectedChildTools(request) {
208
+ return [
209
+ .../* @__PURE__ */ new Set([
210
+ ...request.tools,
211
+ ...CHILD_COMMUNICATION_TOOL_NAMES,
212
+ ...request.extensions.flatMap((extension) => extension.tools)
213
+ ])
214
+ ];
215
+ }
161
216
  async function executeProcess(invocation, request) {
162
217
  const timeoutMs = resolveTimeoutMs(request.timeout);
218
+ const expectedTools = selectedChildTools(request);
219
+ const expectReadiness = requiresReadinessAttestation(request);
220
+ if (expectReadiness) assertChildBootstrapCapacity(expectedTools);
163
221
  let latestOutput = "";
164
222
  let terminalOutput;
165
223
  let terminalStopReason;
166
224
  let errorMessage = "";
167
225
  let assistantFailed = false;
168
226
  let stderr = "";
227
+ let stderrRedactionCarry = "";
228
+ const stderrDecoder = new StringDecoder("utf8");
229
+ const diagnosticAttachmentPaths = attachmentPathVariants(request);
169
230
  let truncated = false;
231
+ const appendStderr = (value) => {
232
+ if (!value) return;
233
+ const limited = truncateTail(`${stderr}${value}`, MAX_ERROR_BYTES);
234
+ stderr = limited.text;
235
+ truncated ||= limited.truncated;
236
+ };
237
+ const pushStderr = (chunk) => {
238
+ const combined = `${stderrRedactionCarry}${typeof chunk === "string" ? chunk : stderrDecoder.write(chunk)}`;
239
+ const carryLength = attachmentPathCarryLength(combined, diagnosticAttachmentPaths);
240
+ const boundary = combined.length - carryLength;
241
+ appendStderr(redactAttachmentPathVariants(combined.slice(0, boundary), diagnosticAttachmentPaths));
242
+ stderrRedactionCarry = combined.slice(boundary);
243
+ };
244
+ const flushStderr = () => {
245
+ const combined = `${stderrRedactionCarry}${stderrDecoder.end()}`;
246
+ const carryLength = attachmentPathCarryLength(combined, diagnosticAttachmentPaths);
247
+ const boundary = combined.length - carryLength;
248
+ appendStderr(
249
+ `${redactAttachmentPathVariants(combined.slice(0, boundary), diagnosticAttachmentPaths)}${carryLength > 0 ? "[attachment path]" : ""}`
250
+ );
251
+ stderrRedactionCarry = "";
252
+ };
170
253
  let malformedEvents = 0;
171
254
  let rpcCounter = 0;
255
+ let attachmentStartupPending = expectReadiness;
256
+ let attachmentStartupError;
172
257
  const pendingCommands = /* @__PURE__ */ new Map();
173
258
  let rpcInputError;
174
259
  let sendCommand = () => Promise.reject(new Error("Subagent RPC process is unavailable."));
@@ -189,11 +274,11 @@ async function executeProcess(invocation, request) {
189
274
  const rejectPendingCommands = (error2) => {
190
275
  for (const id of [...pendingCommands.keys()]) rejectPendingCommand(id, error2);
191
276
  };
192
- const resolvePendingCommand = (id) => {
277
+ const resolvePendingCommand = (id, response) => {
193
278
  const pending = takePendingCommand(id);
194
279
  if (!pending) return;
195
280
  try {
196
- pending.onAccepted?.();
281
+ pending.onAccepted?.(response);
197
282
  pending.resolve();
198
283
  } catch (error2) {
199
284
  pending.reject(error2 instanceof Error ? error2 : new Error(String(error2)));
@@ -206,16 +291,21 @@ async function executeProcess(invocation, request) {
206
291
  const decoder = new JsonLineDecoder(
207
292
  (value) => {
208
293
  const event = value;
294
+ if (expectReadiness && event.type === "extension_error" && (event.event === "session_start" || event.event === "resources_discover") && !attachmentStartupError) {
295
+ const detail = typeof event.error === "string" && event.error ? redactAttachmentPaths(sanitizeTerminalText(event.error), request) : "Unknown error.";
296
+ attachmentStartupError = `Subagent attachment startup failed during ${event.event}: ${detail}`;
297
+ return;
298
+ }
209
299
  if (event.type === "response" && typeof event.id === "string") {
210
300
  const pending = pendingCommands.get(event.id);
211
301
  if (!pending) return;
212
302
  if (event.success === true) {
213
- resolvePendingCommand(event.id);
303
+ resolvePendingCommand(event.id, event);
214
304
  } else {
215
305
  rejectPendingCommand(
216
306
  event.id,
217
307
  new Error(
218
- typeof event.error === "string" ? event.error : `Subagent RPC ${pending.command} command failed.`
308
+ pending.command === "get_commands" ? "Subagent child resource attestation failed." : typeof event.error === "string" ? event.error : `Subagent RPC ${pending.command} command failed.`
219
309
  )
220
310
  );
221
311
  }
@@ -240,7 +330,7 @@ async function executeProcess(invocation, request) {
240
330
  assistantFailed = true;
241
331
  }
242
332
  if (event.message.errorMessage) {
243
- const limited = truncateText(event.message.errorMessage, MAX_ERROR_BYTES);
333
+ const limited = truncateText(redactAttachmentPaths(event.message.errorMessage, request), MAX_ERROR_BYTES);
244
334
  errorMessage = limited.text;
245
335
  truncated ||= limited.truncated;
246
336
  }
@@ -248,9 +338,12 @@ async function executeProcess(invocation, request) {
248
338
  },
249
339
  () => {
250
340
  malformedEvents++;
341
+ if (attachmentStartupPending && !attachmentStartupError) {
342
+ attachmentStartupError = "Subagent attachment startup emitted malformed or oversized RPC output.";
343
+ }
251
344
  }
252
345
  );
253
- const settlement = await new Promise((resolve2) => {
346
+ const settlement = await new Promise((resolve4) => {
254
347
  let process2;
255
348
  let settled = false;
256
349
  let finishRequested = false;
@@ -260,6 +353,9 @@ async function executeProcess(invocation, request) {
260
353
  let timedOut = false;
261
354
  let completed = false;
262
355
  let ready = false;
356
+ let attachmentReady = !expectReadiness;
357
+ let promptStarted = false;
358
+ let readinessPipe;
263
359
  let deadline;
264
360
  let forceClose;
265
361
  let escalation;
@@ -274,8 +370,10 @@ async function executeProcess(invocation, request) {
274
370
  if (forceClose) clearTimeout(forceClose);
275
371
  if (escalation) clearTimeout(escalation);
276
372
  request.signal.removeEventListener("abort", onAbort);
373
+ readinessPipe?.removeAllListeners();
374
+ readinessPipe?.destroy();
277
375
  rejectPendingCommands(new Error("Subagent RPC process closed."));
278
- resolve2({ code, cancelled, timedOut, completed, launchError });
376
+ resolve4({ code, cancelled, timedOut, completed, launchError });
279
377
  };
280
378
  if (termination) void termination.then(complete, complete);
281
379
  else complete();
@@ -299,6 +397,7 @@ async function executeProcess(invocation, request) {
299
397
  process2.stdin?.destroy();
300
398
  process2.stdout?.destroy();
301
399
  process2.stderr?.destroy();
400
+ readinessPipe?.destroy();
302
401
  finish(code);
303
402
  }, KILL_GRACE_MS * 2);
304
403
  forceClose.unref();
@@ -315,16 +414,18 @@ async function executeProcess(invocation, request) {
315
414
  };
316
415
  onAgentSettled = completeNormally;
317
416
  try {
417
+ const environment = {
418
+ ...globalThis.process.env,
419
+ ...childBootstrapEnvironment(expectReadiness),
420
+ PI_SUBAGENT_DEPTH: String((Number.parseInt(globalThis.process.env.PI_SUBAGENT_DEPTH ?? "0", 10) || 0) + 1)
421
+ };
422
+ if (!expectReadiness) delete environment[CHILD_READINESS_FD_ENV];
318
423
  process2 = spawn(invocation.command, invocation.args, {
319
424
  cwd: request.cwd,
320
425
  detached: globalThis.process.platform !== "win32",
321
426
  shell: false,
322
- stdio: ["pipe", "pipe", "pipe", "pipe"],
323
- env: {
324
- ...globalThis.process.env,
325
- ...brokerCredentialEnvironment(),
326
- PI_SUBAGENT_DEPTH: String((Number.parseInt(globalThis.process.env.PI_SUBAGENT_DEPTH ?? "0", 10) || 0) + 1)
327
- }
427
+ stdio: expectReadiness ? ["pipe", "pipe", "pipe", "pipe", "pipe"] : ["pipe", "pipe", "pipe", "pipe"],
428
+ env: environment
328
429
  });
329
430
  } catch (error2) {
330
431
  finish(1, error2 instanceof Error ? error2.message : String(error2));
@@ -379,11 +480,9 @@ async function executeProcess(invocation, request) {
379
480
  }
380
481
  });
381
482
  };
382
- request.signal.addEventListener("abort", onAbort, { once: true });
383
- if (request.signal.aborted) onAbort();
384
- process2.once("spawn", () => {
385
- spawned = true;
386
- if (settled || cancelled) return;
483
+ const startPrompt = () => {
484
+ if (!spawned || !attachmentReady || promptStarted || settled || cancelled || terminating) return;
485
+ promptStarted = true;
387
486
  void sendCommand(
388
487
  { type: "prompt", message: `Task: ${request.task}` },
389
488
  () => {
@@ -411,22 +510,105 @@ async function executeProcess(invocation, request) {
411
510
  request.signal
412
511
  ).catch((error2) => {
413
512
  if (settled || terminating) return;
414
- errorMessage = truncateText(error2 instanceof Error ? error2.message : String(error2), MAX_ERROR_BYTES).text;
513
+ errorMessage = truncateText(
514
+ redactAttachmentPaths(error2 instanceof Error ? error2.message : String(error2), request),
515
+ MAX_ERROR_BYTES
516
+ ).text;
415
517
  terminate(1);
416
518
  });
519
+ };
520
+ const failReadiness = (message) => {
521
+ if (settled || terminating || attachmentReady) return;
522
+ errorMessage = truncateText(redactAttachmentPaths(message, request), MAX_ERROR_BYTES).text;
523
+ terminate(1);
524
+ };
525
+ const confirmAttachmentStartup = () => {
526
+ void sendCommand({ type: "get_state" }, void 0, request.signal).then(async () => {
527
+ if (attachmentStartupError) throw new Error(attachmentStartupError);
528
+ if (!request.projectTrusted) {
529
+ await sendCommand(
530
+ { type: "get_commands" },
531
+ (response) => assertTrustedChildResources(response.data, request.cwd),
532
+ request.signal
533
+ );
534
+ }
535
+ if (attachmentStartupError) throw new Error(attachmentStartupError);
536
+ attachmentStartupPending = false;
537
+ attachmentReady = true;
538
+ startPrompt();
539
+ }).catch((error2) => {
540
+ failReadiness(error2 instanceof Error ? error2.message : String(error2));
541
+ });
542
+ };
543
+ request.signal.addEventListener("abort", onAbort, { once: true });
544
+ if (request.signal.aborted) onAbort();
545
+ process2.once("spawn", () => {
546
+ spawned = true;
547
+ startPrompt();
417
548
  });
549
+ if (expectReadiness) {
550
+ const candidate = process2.stdio[CHILD_READINESS_FD];
551
+ if (!candidate || !("on" in candidate)) {
552
+ errorMessage = "Subagent readiness pipe is unavailable.";
553
+ terminate(1);
554
+ } else {
555
+ readinessPipe = candidate;
556
+ let buffer = Buffer.alloc(0);
557
+ let readinessSettled = false;
558
+ readinessPipe.on("data", (chunk) => {
559
+ if (readinessSettled || terminating) return;
560
+ buffer = Buffer.concat([buffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
561
+ if (buffer.byteLength > MAX_READINESS_FRAME_BYTES) {
562
+ readinessSettled = true;
563
+ failReadiness("Subagent readiness frame exceeded its size limit.");
564
+ }
565
+ });
566
+ readinessPipe.once("end", () => {
567
+ if (readinessSettled || terminating) return;
568
+ readinessSettled = true;
569
+ let frame;
570
+ try {
571
+ frame = parseReadinessFrame(buffer, expectedTools.length);
572
+ } catch (error2) {
573
+ failReadiness(error2 instanceof Error ? error2.message : String(error2));
574
+ return;
575
+ }
576
+ if (!frame.ok) {
577
+ failReadiness(frame.error);
578
+ return;
579
+ }
580
+ for (const extension of request.extensions) {
581
+ for (const tool of extension.tools) {
582
+ const source = frame.sources[expectedTools.indexOf(tool)];
583
+ if (!source || !Object.hasOwn(request.toolSources, tool) || !request.toolSources[tool]?.includes(source)) {
584
+ failReadiness(`Subagent extension tool ${tool} was not provided by its requested attachment.`);
585
+ return;
586
+ }
587
+ }
588
+ }
589
+ confirmAttachmentStartup();
590
+ });
591
+ readinessPipe.once("error", () => {
592
+ if (readinessSettled || terminating) return;
593
+ readinessSettled = true;
594
+ failReadiness("Subagent readiness transfer failed.");
595
+ });
596
+ readinessPipe.once("close", () => {
597
+ if (readinessSettled || terminating) return;
598
+ readinessSettled = true;
599
+ failReadiness("Subagent readiness pipe closed without a result.");
600
+ });
601
+ }
602
+ }
418
603
  process2.stdout?.on("data", (chunk) => decoder.push(chunk));
419
- process2.stderr?.on("data", (chunk) => {
420
- const limited = truncateTail(`${stderr}${chunk.toString()}`, MAX_ERROR_BYTES);
421
- stderr = limited.text;
422
- truncated ||= limited.truncated;
423
- });
604
+ process2.stderr?.on("data", (chunk) => pushStderr(chunk));
424
605
  process2.once("close", (code) => {
606
+ flushStderr();
425
607
  decoder.finish();
426
608
  finish(cancelled ? 130 : timedOut ? 124 : completed ? 0 : code ?? 1);
427
609
  });
428
610
  process2.once("error", (error2) => {
429
- const limited = truncateText(error2.message, MAX_ERROR_BYTES);
611
+ const limited = truncateText(redactAttachmentPaths(error2.message, request), MAX_ERROR_BYTES);
430
612
  errorMessage = limited.text;
431
613
  truncated ||= limited.truncated;
432
614
  if (spawned) terminate(1);
@@ -449,7 +631,12 @@ async function executeProcess(invocation, request) {
449
631
  credentialPipe.on("error", onCredentialError);
450
632
  credentialPipe.once("close", removeCredentialListeners);
451
633
  try {
452
- credentialPipe.end(serializeBrokerCredentials(request.communication));
634
+ credentialPipe.end(
635
+ serializeChildBootstrap({
636
+ communication: request.communication,
637
+ expectedTools: expectReadiness ? expectedTools : []
638
+ })
639
+ );
453
640
  } catch {
454
641
  onCredentialError();
455
642
  }
@@ -471,7 +658,17 @@ async function executeProcess(invocation, request) {
471
658
  truncated
472
659
  };
473
660
  }
474
- const error = settlement.launchError || errorMessage || stderr.trim();
661
+ const redactedStderr = truncateTail(redactAttachmentPaths(stderr.trim(), request), MAX_ERROR_BYTES);
662
+ const combinedError = combineErrors(
663
+ redactAttachmentPaths(settlement.launchError ?? "", request),
664
+ redactAttachmentPaths(errorMessage, request),
665
+ redactedStderr.text
666
+ );
667
+ const error = combinedError.text;
668
+ if ((combinedError.truncated || redactedStderr.truncated) && !truncated) {
669
+ limitations.push("Child output was truncated to runtime limits.");
670
+ }
671
+ truncated ||= combinedError.truncated || redactedStderr.truncated;
475
672
  if (settlement.completed && terminalStopReason === "stop" && !assistantFailed && !errorMessage) {
476
673
  return {
477
674
  state: "completed",
@@ -510,8 +707,8 @@ function resolvePiInvocation(args) {
510
707
  return { command: globalThis.process.execPath, args };
511
708
  }
512
709
  const cliPath = fs.realpathSync(path.resolve(packageDirectory, declared));
513
- const relative2 = path.relative(packageDirectory, cliPath);
514
- if (relative2 === ".." || relative2.startsWith(`..${path.sep}`) || path.isAbsolute(relative2)) {
710
+ const relative4 = path.relative(packageDirectory, cliPath);
711
+ if (relative4 === ".." || relative4.startsWith(`..${path.sep}`) || path.isAbsolute(relative4)) {
515
712
  throw new Error("Pi core bin.pi escapes its package directory.");
516
713
  }
517
714
  if (!fs.statSync(cliPath).isFile()) throw new Error("Pi core bin.pi is not a file.");
@@ -535,7 +732,7 @@ function terminateWindowsProcessTree(process2, spawnProcess = spawn, taskkillPat
535
732
  killImmediateChild(process2);
536
733
  return Promise.resolve();
537
734
  }
538
- return new Promise((resolve2) => {
735
+ return new Promise((resolve4) => {
539
736
  let settled = false;
540
737
  let treeKiller;
541
738
  let deadline;
@@ -549,7 +746,7 @@ function terminateWindowsProcessTree(process2, spawnProcess = spawn, taskkillPat
549
746
  treeKiller.removeListener("close", onClose);
550
747
  if (terminateHelper) killImmediateChild(treeKiller);
551
748
  if (fallback) killImmediateChild(process2);
552
- resolve2();
749
+ resolve4();
553
750
  };
554
751
  try {
555
752
  treeKiller = spawnProcess(taskkillPath, ["/PID", String(process2.pid), "/T", "/F"], {
@@ -558,7 +755,7 @@ function terminateWindowsProcessTree(process2, spawnProcess = spawn, taskkillPat
558
755
  });
559
756
  } catch {
560
757
  killImmediateChild(process2);
561
- resolve2();
758
+ resolve4();
562
759
  return;
563
760
  }
564
761
  treeKiller.once("error", onError);
@@ -578,6 +775,97 @@ function killImmediateChild(process2) {
578
775
  } catch {
579
776
  }
580
777
  }
778
+ function assertTrustedChildResources(value, cwd) {
779
+ const invalid = () => new Error("Subagent child resource attestation returned invalid commands.");
780
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw invalid();
781
+ const commands = value.commands;
782
+ if (!Array.isArray(commands)) throw invalid();
783
+ let canonicalCwd;
784
+ try {
785
+ canonicalCwd = fs.realpathSync(cwd);
786
+ } catch {
787
+ throw invalid();
788
+ }
789
+ for (const command of commands) {
790
+ if (!command || typeof command !== "object" || Array.isArray(command)) throw invalid();
791
+ const entry = command;
792
+ if (entry.source !== "skill" && entry.source !== "prompt") continue;
793
+ const resourcePath = entry.sourceInfo?.path;
794
+ if (typeof resourcePath !== "string" || !resourcePath) throw invalid();
795
+ const lexicalPath = path.resolve(cwd, resourcePath);
796
+ let canonicalPath;
797
+ try {
798
+ canonicalPath = fs.realpathSync(lexicalPath);
799
+ } catch {
800
+ throw invalid();
801
+ }
802
+ if (isWithin(cwd, lexicalPath) || isWithin(canonicalCwd, canonicalPath)) {
803
+ throw new Error("Subagent child cannot load project resources because the project is not trusted.");
804
+ }
805
+ }
806
+ }
807
+ function parseReadinessFrame(buffer, expectedToolCount) {
808
+ if (buffer.byteLength === 0) {
809
+ throw new Error("Subagent readiness pipe closed without a result.");
810
+ }
811
+ const text = buffer.toString("utf8");
812
+ const newline = text.indexOf("\n");
813
+ if (newline < 0 || newline !== text.length - 1) {
814
+ throw new Error("Subagent readiness pipe returned an invalid frame.");
815
+ }
816
+ let value;
817
+ try {
818
+ value = JSON.parse(text.slice(0, newline));
819
+ } catch {
820
+ throw new Error("Subagent readiness pipe returned malformed JSON.");
821
+ }
822
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
823
+ throw new Error("Subagent readiness pipe returned an invalid result.");
824
+ }
825
+ const record = value;
826
+ if (record.ok === true && Object.keys(record).length === 2 && Array.isArray(record.sources) && record.sources.length === expectedToolCount && record.sources.every((source) => typeof source === "string" && /^[a-f0-9]{64}$/u.test(source))) {
827
+ return { ok: true, sources: record.sources };
828
+ }
829
+ if (record.ok === false && Object.keys(record).length === 2 && typeof record.error === "string" && record.error.length > 0 && Buffer.byteLength(record.error, "utf8") <= MAX_ERROR_BYTES) {
830
+ return { ok: false, error: sanitizeTerminalText(record.error) };
831
+ }
832
+ throw new Error("Subagent readiness pipe returned an invalid result.");
833
+ }
834
+ function redactAttachmentPaths(value, request) {
835
+ return redactAttachmentPathVariants(value, attachmentPathVariants(request));
836
+ }
837
+ function redactAttachmentPathVariants(value, candidates) {
838
+ let redacted = value;
839
+ for (const candidate of candidates) redacted = redacted.replaceAll(candidate, "[attachment path]");
840
+ return redacted;
841
+ }
842
+ function attachmentPathCarryLength(value, candidates) {
843
+ const maximum = Math.min(
844
+ value.length,
845
+ candidates.reduce((length, candidate) => Math.max(length, candidate.length - 1), 0)
846
+ );
847
+ for (let length = maximum; length > 0; length--) {
848
+ const suffix = value.slice(-length);
849
+ if (candidates.some((candidate) => candidate.length > length && candidate.startsWith(suffix))) return length;
850
+ }
851
+ return 0;
852
+ }
853
+ function attachmentPathVariants(request) {
854
+ const variants = /* @__PURE__ */ new Set();
855
+ const paths = [...request.skills, ...request.extensions.map((extension) => extension.path)];
856
+ for (const resourcePath of paths) {
857
+ for (const spelling of [resourcePath, resourcePath.replaceAll("\\", "/"), resourcePath.replaceAll("/", "\\")]) {
858
+ if (!spelling) continue;
859
+ variants.add(spelling);
860
+ variants.add(JSON.stringify(spelling).slice(1, -1));
861
+ }
862
+ }
863
+ return [...variants].sort((left, right) => right.length - left.length);
864
+ }
865
+ function combineErrors(...messages) {
866
+ const unique = [...new Set(messages.filter((message) => Boolean(message)))];
867
+ return truncateText(unique.join("\n"), MAX_ERROR_BYTES);
868
+ }
581
869
  function abortError(message) {
582
870
  const error = new Error(message);
583
871
  error.name = "AbortError";
@@ -663,6 +951,247 @@ var JsonLineDecoder = class {
663
951
  }
664
952
  };
665
953
 
954
+ // src/resource-attachments.ts
955
+ import { existsSync, statSync as statSync3 } from "node:fs";
956
+ import {
957
+ opendir as opendirAsync2,
958
+ readFile as readFileAsync,
959
+ realpath as realpathAsync2,
960
+ stat as statAsync2
961
+ } from "node:fs/promises";
962
+ import * as path3 from "node:path";
963
+ import { DefaultPackageManager, SettingsManager } from "@earendil-works/pi-coding-agent";
964
+ import ignore2 from "ignore";
965
+
966
+ // src/skill-attachments.ts
967
+ import { statSync as statSync2 } from "node:fs";
968
+ import { opendir as opendirAsync, realpath as realpathAsync, stat as statAsync } from "node:fs/promises";
969
+ import * as path2 from "node:path";
970
+ import { loadSkills } from "@earendil-works/pi-coding-agent";
971
+ import ignore from "ignore";
972
+ var MAX_SKILL_SCAN_DEPTH = 32;
973
+ var MAX_SKILL_SCAN_ENTRIES = 4096;
974
+ var MAX_SKILL_SCAN_BYTES = 4 * 1024 * 1024;
975
+ var MAX_SKILL_IGNORE_BYTES = 1024 * 1024;
976
+ async function assertLoadableSkills(skillPaths, cwd, canonicalCwd, projectTrusted, signal) {
977
+ const scanState = {
978
+ entries: 0,
979
+ skillBytes: 0,
980
+ ignoreBytes: 0,
981
+ ancestors: /* @__PURE__ */ new Set(),
982
+ signal
983
+ };
984
+ for (const skillPath of skillPaths) {
985
+ throwIfAttachmentAborted(signal);
986
+ const candidates = await collectBoundedSkillCandidates(skillPath, scanState);
987
+ throwIfAttachmentAborted(signal);
988
+ const result = loadExplicitSkills([skillPath], cwd);
989
+ if (result.skills.length === 0) {
990
+ throw new Error("Subagent skill path must contain at least one loadable Pi skill.");
991
+ }
992
+ assertNoUntrustedProjectSkills(result, cwd, canonicalCwd, projectTrusted);
993
+ assertNoOmittedSkillDiagnostics(result);
994
+ assertNoSkillNameCollisions(result.diagnostics);
995
+ await assertNoSilentlyOmittedSkills(skillPath, cwd, result, canonicalCwd, projectTrusted, candidates, signal);
996
+ }
997
+ if (skillPaths.length > 1) {
998
+ throwIfAttachmentAborted(signal);
999
+ const result = loadExplicitSkills(skillPaths, cwd);
1000
+ assertNoUntrustedProjectSkills(result, cwd, canonicalCwd, projectTrusted);
1001
+ assertNoSkillNameCollisions(result.diagnostics);
1002
+ }
1003
+ return scanState;
1004
+ }
1005
+ function loadExplicitSkills(skillPaths, cwd) {
1006
+ return loadSkills({ cwd, agentDir: cwd, skillPaths, includeDefaults: false });
1007
+ }
1008
+ function assertNoOmittedSkillDiagnostics(result) {
1009
+ const loadedPaths = new Set(result.skills.map((skill) => skill.filePath));
1010
+ const omitted = result.diagnostics.some(
1011
+ (diagnostic) => diagnostic.type === "error" || diagnostic.type === "warning" && (!diagnostic.path || !loadedPaths.has(diagnostic.path))
1012
+ );
1013
+ if (omitted) {
1014
+ throw new Error("Subagent skill attachment must not contain an invalid or unreadable declared skill.");
1015
+ }
1016
+ }
1017
+ function assertNoSkillNameCollisions(diagnostics) {
1018
+ if (diagnostics.some((diagnostic) => diagnostic.type === "collision" && diagnostic.collision?.resourceType === "skill")) {
1019
+ throw new Error("Subagent skill attachments must not contain duplicate skill names.");
1020
+ }
1021
+ }
1022
+ function assertNoUntrustedProjectSkills(result, cwd, canonicalCwd, projectTrusted) {
1023
+ if (projectTrusted) return;
1024
+ for (const skill of result.skills) {
1025
+ const lexicalPath = path2.resolve(skill.filePath);
1026
+ const canonicalPath = realpath(skill.filePath, "Loaded subagent skill");
1027
+ if (isWithin(cwd, lexicalPath) || isWithin(canonicalCwd, canonicalPath)) {
1028
+ throw new Error("Subagent skill cannot load a project path because the project is not trusted.");
1029
+ }
1030
+ }
1031
+ }
1032
+ async function assertNoSilentlyOmittedSkills(skillPath, cwd, result, canonicalCwd, projectTrusted, candidates, signal) {
1033
+ if (!statSync2(skillPath).isDirectory()) return;
1034
+ const loadedPaths = new Set(result.skills.map((skill) => realpath(skill.filePath, "Loaded subagent skill")));
1035
+ for (const [index, candidate] of candidates.entries()) {
1036
+ if (index > 0 && index % 64 === 0) await new Promise((resolve4) => setImmediate(resolve4));
1037
+ throwIfAttachmentAborted(signal);
1038
+ const candidateResult = loadExplicitSkills([candidate], cwd);
1039
+ assertNoUntrustedProjectSkills(candidateResult, cwd, canonicalCwd, projectTrusted);
1040
+ if (candidateResult.skills.length === 0) {
1041
+ if (path2.basename(candidate) === "SKILL.md") throwInvalidDeclaredSkill();
1042
+ continue;
1043
+ }
1044
+ const loaded = candidateResult.skills.some(
1045
+ (skill) => loadedPaths.has(realpath(skill.filePath, "Loaded subagent skill"))
1046
+ );
1047
+ if (!loaded) throwInvalidDeclaredSkill();
1048
+ }
1049
+ }
1050
+ async function collectBoundedSkillCandidates(skillPath, state) {
1051
+ throwIfAttachmentAborted(state.signal);
1052
+ const stats = await statAsync(skillPath);
1053
+ throwIfAttachmentAborted(state.signal);
1054
+ if (!stats.isDirectory()) {
1055
+ if (stats.isFile() && skillPath.endsWith(".md")) addSkillBytes(state, stats.size);
1056
+ return [];
1057
+ }
1058
+ return collectSkillCandidates(skillPath, true, ignore(), skillPath, 0, state);
1059
+ }
1060
+ async function collectSkillCandidates(directory, includeRootMarkdown, ignoreMatcher, rootDirectory, depth, state) {
1061
+ throwIfAttachmentAborted(state.signal);
1062
+ if (depth > MAX_SKILL_SCAN_DEPTH) throwSkillScanLimit();
1063
+ let canonicalDirectory;
1064
+ try {
1065
+ canonicalDirectory = await realpathAsync(directory);
1066
+ } catch {
1067
+ throwInvalidDeclaredSkill();
1068
+ }
1069
+ throwIfAttachmentAborted(state.signal);
1070
+ if (state.ancestors.has(canonicalDirectory)) {
1071
+ throw new Error("Subagent skill attachment must not contain a recursive directory link.");
1072
+ }
1073
+ state.ancestors.add(canonicalDirectory);
1074
+ try {
1075
+ await addAttachmentIgnoreRules(ignoreMatcher, directory, rootDirectory, state.signal, (_ignorePath, bytes) => {
1076
+ state.ignoreBytes += bytes;
1077
+ if (state.ignoreBytes > MAX_SKILL_IGNORE_BYTES) throwSkillScanLimit();
1078
+ });
1079
+ let directoryHandle;
1080
+ try {
1081
+ directoryHandle = await opendirAsync(directory);
1082
+ } catch {
1083
+ throwInvalidDeclaredSkill();
1084
+ }
1085
+ const entries = [];
1086
+ try {
1087
+ for await (const entry of directoryHandle) {
1088
+ throwIfAttachmentAborted(state.signal);
1089
+ state.entries++;
1090
+ if (state.entries > MAX_SKILL_SCAN_ENTRIES) throwSkillScanLimit();
1091
+ entries.push(entry);
1092
+ }
1093
+ } catch (error) {
1094
+ if (isAbortError(error) || isSkillScanLimitError(error)) throw error;
1095
+ throwInvalidDeclaredSkill();
1096
+ }
1097
+ const rootSkill = entries.find((entry) => entry.name === "SKILL.md");
1098
+ if (rootSkill) {
1099
+ const rootPath = path2.join(directory, rootSkill.name);
1100
+ const relativePath = toPosixPath(path2.relative(rootDirectory, rootPath));
1101
+ if (!ignoreMatcher.ignores(relativePath)) {
1102
+ let rootStats;
1103
+ try {
1104
+ rootStats = await statAsync(rootPath);
1105
+ } catch (error) {
1106
+ if (isAbortError(error)) throw error;
1107
+ throwInvalidDeclaredSkill();
1108
+ }
1109
+ throwIfAttachmentAborted(state.signal);
1110
+ if (rootStats.isFile()) {
1111
+ addSkillBytes(state, rootStats.size);
1112
+ return [rootPath];
1113
+ }
1114
+ }
1115
+ }
1116
+ const candidates = [];
1117
+ for (const entry of entries) {
1118
+ throwIfAttachmentAborted(state.signal);
1119
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1120
+ const candidate = path2.join(directory, entry.name);
1121
+ const relativePath = toPosixPath(path2.relative(rootDirectory, candidate));
1122
+ let candidateStats;
1123
+ try {
1124
+ candidateStats = await statAsync(candidate);
1125
+ } catch (error) {
1126
+ if (isAbortError(error)) throw error;
1127
+ throwIfAttachmentAborted(state.signal);
1128
+ if (!ignoreMatcher.ignores(relativePath) && (entry.name === "SKILL.md" || includeRootMarkdown && entry.name.endsWith(".md"))) {
1129
+ throwInvalidDeclaredSkill();
1130
+ }
1131
+ continue;
1132
+ }
1133
+ throwIfAttachmentAborted(state.signal);
1134
+ if (ignoreMatcher.ignores(candidateStats.isDirectory() ? `${relativePath}/` : relativePath)) continue;
1135
+ if (candidateStats.isDirectory()) {
1136
+ candidates.push(
1137
+ ...await collectSkillCandidates(candidate, false, ignoreMatcher, rootDirectory, depth + 1, state)
1138
+ );
1139
+ } else if (candidateStats.isFile() && (entry.name === "SKILL.md" || includeRootMarkdown && entry.name.endsWith(".md"))) {
1140
+ addSkillBytes(state, candidateStats.size);
1141
+ candidates.push(candidate);
1142
+ }
1143
+ }
1144
+ return candidates;
1145
+ } finally {
1146
+ state.ancestors.delete(canonicalDirectory);
1147
+ }
1148
+ }
1149
+ function addSkillBytes(state, bytes) {
1150
+ state.skillBytes += bytes;
1151
+ if (state.skillBytes > MAX_SKILL_SCAN_BYTES) throwSkillScanLimit();
1152
+ }
1153
+ function isSkillScanLimitError(error) {
1154
+ return error instanceof Error && error.name === "SkillScanLimitError";
1155
+ }
1156
+ function throwSkillScanLimit() {
1157
+ const error = new Error(
1158
+ `Subagent skill attachment exceeds traversal limits (${MAX_SKILL_SCAN_ENTRIES} entries, depth ${MAX_SKILL_SCAN_DEPTH}, ${MAX_SKILL_SCAN_BYTES} skill bytes, or ${MAX_SKILL_IGNORE_BYTES} ignore bytes).`
1159
+ );
1160
+ error.name = "SkillScanLimitError";
1161
+ throw error;
1162
+ }
1163
+ function throwInvalidDeclaredSkill() {
1164
+ throw new Error("Subagent skill attachment must not contain an invalid or unreadable declared skill.");
1165
+ }
1166
+ async function assertLoadablePackageSkills(packageSkillPaths, explicitSkillPaths, cwd, scanState) {
1167
+ const uniquePackageSkillPaths = deduplicatePackageSkillPaths(packageSkillPaths, explicitSkillPaths);
1168
+ for (const [index, skillPath] of uniquePackageSkillPaths.entries()) {
1169
+ if (index > 0 && index % 64 === 0) await new Promise((resolve4) => setImmediate(resolve4));
1170
+ throwIfAttachmentAborted(scanState.signal);
1171
+ await collectBoundedSkillCandidates(skillPath, scanState);
1172
+ }
1173
+ for (const [index, skillPath] of uniquePackageSkillPaths.entries()) {
1174
+ if (index > 0 && index % 64 === 0) await new Promise((resolve4) => setImmediate(resolve4));
1175
+ throwIfAttachmentAborted(scanState.signal);
1176
+ const result = loadExplicitSkills([skillPath], cwd);
1177
+ if (result.skills.length === 0) throwInvalidDeclaredSkill();
1178
+ assertNoOmittedSkillDiagnostics(result);
1179
+ }
1180
+ throwIfAttachmentAborted(scanState.signal);
1181
+ assertNoSkillNameCollisions(loadExplicitSkills([...uniquePackageSkillPaths, ...explicitSkillPaths], cwd).diagnostics);
1182
+ }
1183
+ function deduplicatePackageSkillPaths(packageSkillPaths, explicitSkillPaths) {
1184
+ const seenPaths = new Set(explicitSkillPaths.map((skillPath) => realpath(skillPath, "Loaded subagent skill")));
1185
+ const uniquePaths = [];
1186
+ for (const skillPath of packageSkillPaths) {
1187
+ const canonicalPath = realpath(skillPath, "Loaded subagent package skill");
1188
+ if (seenPaths.has(canonicalPath)) continue;
1189
+ seenPaths.add(canonicalPath);
1190
+ uniquePaths.push(skillPath);
1191
+ }
1192
+ return uniquePaths;
1193
+ }
1194
+
666
1195
  // src/types.ts
667
1196
  var TERMINAL_JOB_STATES = /* @__PURE__ */ new Set([
668
1197
  "completed",
@@ -683,6 +1212,514 @@ var SUBAGENT_THINKING_LEVELS = [
683
1212
  "max"
684
1213
  ];
685
1214
 
1215
+ // src/resource-attachments.ts
1216
+ var MAX_ATTACHED_SKILLS = 16;
1217
+ var MAX_ATTACHED_EXTENSIONS = 16;
1218
+ var MAX_SELECTED_TOOLS = 64;
1219
+ var MAX_RESOURCE_PATH_BYTES = 4 * 1024;
1220
+ var MAX_EXTENSION_TOOL_NAME_LENGTH = 128;
1221
+ var MAX_EXTENSION_SCAN_DEPTH = 32;
1222
+ var MAX_EXTENSION_SCAN_ENTRIES = 4096;
1223
+ var MAX_EXTENSION_METADATA_BYTES = 1024 * 1024;
1224
+ var RESERVED_CHILD_TOOL_NAMES = /* @__PURE__ */ new Set([...CHILD_CORE_TOOL_NAMES, ...CHILD_COMMUNICATION_TOOL_NAMES]);
1225
+ async function resolveResourceAttachments(input, options) {
1226
+ const cwd = path3.resolve(options.cwd);
1227
+ const canonicalCwd = realpath(cwd, "Subagent working directory");
1228
+ const skillInputs = optionalArray(input.skills, "skills", MAX_ATTACHED_SKILLS);
1229
+ const extensionInputs = optionalArray(input.extensions, "extensions", MAX_ATTACHED_EXTENSIONS);
1230
+ const skills = [];
1231
+ const seenSkills = /* @__PURE__ */ new Set();
1232
+ for (const candidate of skillInputs) {
1233
+ const resolved = resolveResourcePath(candidate, "skill", cwd, canonicalCwd, options.projectTrusted);
1234
+ if (!seenSkills.has(resolved)) {
1235
+ seenSkills.add(resolved);
1236
+ skills.push(resolved);
1237
+ }
1238
+ }
1239
+ const skillScanState = await assertLoadableSkills(skills, cwd, canonicalCwd, options.projectTrusted, options.signal);
1240
+ const extensions = [];
1241
+ const extensionsByPath = /* @__PURE__ */ new Map();
1242
+ const entrypointsByPath = /* @__PURE__ */ new Map();
1243
+ const packageSkillPaths = [];
1244
+ for (const candidate of extensionInputs) {
1245
+ if (!isRecord(candidate) || Object.keys(candidate).some((key) => key !== "path" && key !== "tools")) {
1246
+ throw new Error("Each subagent extension must contain only path and tools.");
1247
+ }
1248
+ const resolved = resolveResourcePath(candidate.path, "extension", cwd, canonicalCwd, options.projectTrusted);
1249
+ const toolInputs = optionalArray(candidate.tools, "extension tools", MAX_SELECTED_TOOLS, false);
1250
+ const tools = toolInputs.map(resolveExtensionToolName);
1251
+ for (const tool of tools) {
1252
+ if (RESERVED_CHILD_TOOL_NAMES.has(tool)) {
1253
+ throw new Error(`Subagent extension tool name conflicts with the built-in ${tool} tool.`);
1254
+ }
1255
+ }
1256
+ let attachment = extensionsByPath.get(resolved);
1257
+ if (!attachment) {
1258
+ const resources = await assertResolvableExtensionAttachment(
1259
+ resolved,
1260
+ cwd,
1261
+ canonicalCwd,
1262
+ options.projectTrusted,
1263
+ options.signal
1264
+ );
1265
+ packageSkillPaths.push(...resources.skills);
1266
+ entrypointsByPath.set(resolved, resources.entrypoints);
1267
+ attachment = { path: resolved, tools: [] };
1268
+ extensionsByPath.set(resolved, attachment);
1269
+ extensions.push(attachment);
1270
+ }
1271
+ for (const tool of tools) {
1272
+ if (!attachment.tools.includes(tool)) attachment.tools.push(tool);
1273
+ }
1274
+ }
1275
+ if (packageSkillPaths.length > 0) {
1276
+ await assertLoadablePackageSkills(packageSkillPaths, skills, cwd, skillScanState);
1277
+ }
1278
+ const effectiveTools = [.../* @__PURE__ */ new Set([...options.coreTools, ...extensions.flatMap((extension) => extension.tools)])];
1279
+ if (effectiveTools.length > MAX_SELECTED_TOOLS) {
1280
+ throw new Error(`Subagent jobs may select at most ${MAX_SELECTED_TOOLS} total tools.`);
1281
+ }
1282
+ const toolSources = /* @__PURE__ */ new Map();
1283
+ const firstSourceIdByCanonicalPath = /* @__PURE__ */ new Map();
1284
+ for (const extension of extensions) {
1285
+ const sourceIds = (entrypointsByPath.get(extension.path) ?? []).map((entrypoint) => {
1286
+ const canonicalPath = realpath(entrypoint, "Subagent extension entrypoint");
1287
+ const sourceId = firstSourceIdByCanonicalPath.get(canonicalPath) ?? toolSourceId(entrypoint);
1288
+ firstSourceIdByCanonicalPath.set(canonicalPath, sourceId);
1289
+ return sourceId;
1290
+ });
1291
+ for (const tool of extension.tools) {
1292
+ if (toolSources.has(tool)) {
1293
+ throw new Error(`Subagent extension tool ${tool} is requested by multiple attachments.`);
1294
+ }
1295
+ toolSources.set(tool, sourceIds);
1296
+ }
1297
+ }
1298
+ return { skills, extensions, effectiveTools, toolSources: Object.fromEntries(toolSources) };
1299
+ }
1300
+ function optionalArray(value, field, maxItems, optional = true) {
1301
+ if (value === void 0 && optional) return [];
1302
+ if (!Array.isArray(value)) throw new Error(`Subagent ${field} must be an array.`);
1303
+ if (value.length > maxItems) throw new Error(`Subagent ${field} may contain at most ${maxItems} entries.`);
1304
+ return value;
1305
+ }
1306
+ function resolveResourcePath(value, kind, cwd, canonicalCwd, projectTrusted) {
1307
+ if (typeof value !== "string" || !value.trim()) throw new Error(`Subagent ${kind} path is required.`);
1308
+ const resourcePath = value.trim();
1309
+ if (hasControlCharacter(resourcePath)) throw new Error(`Subagent ${kind} path must not contain control characters.`);
1310
+ if (Buffer.byteLength(resourcePath, "utf8") > MAX_RESOURCE_PATH_BYTES) {
1311
+ throw new Error(`Subagent ${kind} path must be at most ${MAX_RESOURCE_PATH_BYTES} UTF-8 bytes.`);
1312
+ }
1313
+ if (!isLocalPath(resourcePath)) throw new Error(`Subagent ${kind} must use a local path.`);
1314
+ const lexicalPath = path3.resolve(cwd, resourcePath);
1315
+ const canonicalPath = realpath(lexicalPath, `Subagent ${kind}`);
1316
+ const stats = statSync3(canonicalPath);
1317
+ if (!stats.isFile() && !stats.isDirectory()) {
1318
+ throw new Error(`Subagent ${kind} path must reference a file or directory.`);
1319
+ }
1320
+ if (!projectTrusted && (isWithin(cwd, lexicalPath) || isWithin(canonicalCwd, canonicalPath))) {
1321
+ throw new Error(`Subagent ${kind} cannot load a project path because the project is not trusted.`);
1322
+ }
1323
+ return canonicalPath;
1324
+ }
1325
+ async function assertResolvableExtensionAttachment(extensionPath, cwd, canonicalCwd, projectTrusted, signal) {
1326
+ await inspectExtensionAttachment(extensionPath, cwd, canonicalCwd, projectTrusted, signal);
1327
+ throwIfAttachmentAborted(signal);
1328
+ const resolved = await resolveAttachedExtensionResources(extensionPath, cwd);
1329
+ throwIfAttachmentAborted(signal);
1330
+ const entrypoints = enabledResourcePaths(resolved.extensions);
1331
+ if (entrypoints.length === 0) {
1332
+ throw new Error("Subagent extension directory must contain at least one loadable Pi extension entrypoint.");
1333
+ }
1334
+ const loadPaths = entrypoints.flatMap(resolveDirectExtensionLoadPaths);
1335
+ for (const entrypoint of loadPaths) {
1336
+ assertTrustedResolvedPath(entrypoint, "extension entrypoint", cwd, canonicalCwd, projectTrusted);
1337
+ }
1338
+ const packageSkills = enabledResourcePaths(resolved.skills);
1339
+ const packageResources = [
1340
+ packageSkills,
1341
+ enabledResourcePaths(resolved.prompts),
1342
+ enabledResourcePaths(resolved.themes)
1343
+ ].flat();
1344
+ for (const resource of packageResources) {
1345
+ assertTrustedResolvedPath(resource, "extension package resource", cwd, canonicalCwd, projectTrusted);
1346
+ }
1347
+ return { skills: packageSkills, entrypoints: loadPaths };
1348
+ }
1349
+ function resolveDirectExtensionLoadPaths(entrypoint) {
1350
+ if (!statSync3(entrypoint).isDirectory()) return [entrypoint];
1351
+ for (const filename of ["index.ts", "index.js"]) {
1352
+ const indexPath = path3.join(entrypoint, filename);
1353
+ if (existsSync(indexPath) && statSync3(indexPath).isFile()) return [entrypoint, indexPath];
1354
+ }
1355
+ throwUnresolvableExtensionEntrypoint();
1356
+ }
1357
+ async function resolveAttachedExtensionResources(extensionPath, cwd) {
1358
+ const packageManager = new DefaultPackageManager({
1359
+ cwd,
1360
+ agentDir: cwd,
1361
+ settingsManager: SettingsManager.inMemory()
1362
+ });
1363
+ return packageManager.resolveExtensionSources([extensionPath], { temporary: true });
1364
+ }
1365
+ function enabledResourcePaths(resources) {
1366
+ return resources.filter((resource) => resource.enabled).map((resource) => resource.path);
1367
+ }
1368
+ function assertTrustedResolvedPath(resourcePath, label, cwd, canonicalCwd, projectTrusted) {
1369
+ const canonicalPath = realpath(resourcePath, `Subagent ${label}`);
1370
+ const stats = statSync3(canonicalPath);
1371
+ if (!stats.isFile() && !stats.isDirectory()) {
1372
+ throw new Error(`Subagent ${label} must reference a file or directory.`);
1373
+ }
1374
+ assertProjectPathTrusted(resourcePath, canonicalPath, label, cwd, canonicalCwd, projectTrusted);
1375
+ }
1376
+ function assertProjectPathTrusted(lexicalPath, canonicalPath, label, cwd, canonicalCwd, projectTrusted) {
1377
+ if (!projectTrusted && (isWithin(cwd, path3.resolve(lexicalPath)) || isWithin(canonicalCwd, canonicalPath))) {
1378
+ throw new Error(`Subagent ${label} cannot load a project path because the project is not trusted.`);
1379
+ }
1380
+ }
1381
+ async function inspectExtensionAttachment(extensionPath, cwd, canonicalCwd, projectTrusted, signal) {
1382
+ if (!statSync3(extensionPath).isDirectory()) return;
1383
+ const state = {
1384
+ entries: 0,
1385
+ metadataBytes: 0,
1386
+ ancestors: /* @__PURE__ */ new Set(),
1387
+ cwd,
1388
+ canonicalCwd,
1389
+ projectTrusted,
1390
+ signal
1391
+ };
1392
+ const inspected = await inspectPiManifest(extensionPath, state);
1393
+ if (inspected.hasPiManifest) {
1394
+ await inspectDeclaredExtensionEntries(extensionPath, inspected.manifest?.extensions, true, state);
1395
+ for (const resourceType of ["skills", "prompts", "themes"]) {
1396
+ await inspectDeclaredPackageResourceEntries(
1397
+ extensionPath,
1398
+ inspected.manifest?.[resourceType],
1399
+ resourceType,
1400
+ state
1401
+ );
1402
+ }
1403
+ return;
1404
+ }
1405
+ const resourceDirectories = ["extensions", "skills", "prompts", "themes"].map(
1406
+ (name) => path3.join(extensionPath, name)
1407
+ );
1408
+ const resourceStats = await Promise.all(resourceDirectories.map((directory) => statIfPresent(directory)));
1409
+ if (resourceStats.some((stats) => stats?.isDirectory())) {
1410
+ if (resourceStats[0]?.isDirectory()) await inspectAutoExtensionDirectory(resourceDirectories[0], true, state);
1411
+ for (const [index, resourceType] of ["skills", "prompts", "themes"].entries()) {
1412
+ if (resourceStats[index + 1]?.isDirectory()) {
1413
+ await inspectPackageResourceDirectory(resourceDirectories[index + 1], resourceType, state);
1414
+ }
1415
+ }
1416
+ return;
1417
+ }
1418
+ await inspectAutoExtensionDirectory(extensionPath, true, state);
1419
+ }
1420
+ async function inspectAutoExtensionDirectory(directory, discoverContents, state, enforceTrust = true) {
1421
+ throwIfAttachmentAborted(state.signal);
1422
+ if (enforceTrust) assertExtensionPackagePathTrusted(directory, "extension entrypoint", state);
1423
+ const inspected = await inspectPiManifest(directory, state, enforceTrust);
1424
+ const sourceEntries = inspected.manifest?.extensions?.filter((entrypoint) => !isExtensionOverridePattern(entrypoint));
1425
+ if (sourceEntries && sourceEntries.length > 0) {
1426
+ await inspectDeclaredExtensionEntries(directory, inspected.manifest?.extensions, false, state);
1427
+ return true;
1428
+ }
1429
+ if (await hasRegularExtensionIndex(directory, state, enforceTrust)) return true;
1430
+ if (!discoverContents) return false;
1431
+ const ignoreMatcher = ignore2();
1432
+ await addPackageIgnoreRules(ignoreMatcher, directory, directory, state, enforceTrust);
1433
+ const entries = await readBoundedPackageEntries(directory, state, "reject");
1434
+ let found = false;
1435
+ for (const entry of entries) {
1436
+ throwIfAttachmentAborted(state.signal);
1437
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1438
+ const entryPath = path3.join(directory, entry.name);
1439
+ const entryStats = await statIfPresent(entryPath);
1440
+ if (!entryStats) continue;
1441
+ const relativePath = toPosixPath(path3.relative(directory, entryPath));
1442
+ if (ignoreMatcher.ignores(entryStats.isDirectory() ? `${relativePath}/` : relativePath)) continue;
1443
+ if (entryStats.isFile()) {
1444
+ if (!entry.name.endsWith(".ts") && !entry.name.endsWith(".js")) continue;
1445
+ if (enforceTrust) assertExtensionPackagePathTrusted(entryPath, "extension entrypoint", state);
1446
+ found = true;
1447
+ } else if (entryStats.isDirectory()) {
1448
+ if (enforceTrust) assertExtensionPackagePathTrusted(entryPath, "extension entrypoint", state);
1449
+ if (await inspectAutoExtensionDirectory(entryPath, false, state, enforceTrust)) found = true;
1450
+ }
1451
+ }
1452
+ return found;
1453
+ }
1454
+ async function inspectDeclaredExtensionEntries(directory, entries, expandDirectories, state) {
1455
+ if (!entries) return;
1456
+ for (const entrypoint of entries) {
1457
+ throwIfAttachmentAborted(state.signal);
1458
+ state.entries++;
1459
+ if (state.entries > MAX_EXTENSION_SCAN_ENTRIES) throwExtensionScanLimit();
1460
+ if (isExtensionOverridePattern(entrypoint)) continue;
1461
+ if (hasExtensionGlob(entrypoint)) {
1462
+ throw new Error("Subagent extension package must not contain glob entrypoint declarations.");
1463
+ }
1464
+ const resolved = path3.resolve(directory, entrypoint);
1465
+ const resolvedStats = await statIfPresent(resolved);
1466
+ if (!resolvedStats) throwUnresolvableExtensionEntrypoint();
1467
+ if (resolvedStats.isFile()) continue;
1468
+ if (!resolvedStats.isDirectory()) throwUnresolvableExtensionEntrypoint();
1469
+ if (expandDirectories) {
1470
+ if (!await inspectAutoExtensionDirectory(resolved, true, state, false)) throwUnresolvableExtensionEntrypoint();
1471
+ } else if (!await hasRegularExtensionIndex(resolved, state, false)) {
1472
+ throwUnresolvableExtensionEntrypoint();
1473
+ }
1474
+ }
1475
+ }
1476
+ async function inspectDeclaredPackageResourceEntries(directory, entries, resourceType, state) {
1477
+ if (!entries) return;
1478
+ for (const entrypoint of entries) {
1479
+ throwIfAttachmentAborted(state.signal);
1480
+ state.entries++;
1481
+ if (state.entries > MAX_EXTENSION_SCAN_ENTRIES) throwExtensionScanLimit();
1482
+ if (isExtensionOverridePattern(entrypoint)) continue;
1483
+ if (hasExtensionGlob(entrypoint)) {
1484
+ throw new Error("Subagent extension package must not contain glob resource declarations.");
1485
+ }
1486
+ const resolved = path3.resolve(directory, entrypoint);
1487
+ const resolvedStats = await statIfPresent(resolved);
1488
+ throwIfAttachmentAborted(state.signal);
1489
+ if (resourceType === "skills" && (!resolvedStats || !resolvedStats.isFile() && !resolvedStats.isDirectory())) {
1490
+ throw new Error("Subagent extension package must not contain a missing or unreadable declared skill.");
1491
+ }
1492
+ if (resolvedStats?.isDirectory()) await inspectPackageResourceDirectory(resolved, resourceType, state, false);
1493
+ }
1494
+ }
1495
+ async function inspectPackageResourceDirectory(directory, resourceType, state, enforceTrust = true) {
1496
+ await inspectPackageResourceTree(directory, resourceType, ignore2(), directory, 0, state, enforceTrust);
1497
+ }
1498
+ async function inspectPackageResourceTree(directory, resourceType, ignoreMatcher, rootDirectory, depth, state, enforceTrust) {
1499
+ const canonicalDirectory = await enterPackageResourceDirectory(directory, resourceType, depth, state, enforceTrust);
1500
+ if (!canonicalDirectory) return;
1501
+ try {
1502
+ await addPackageIgnoreRules(ignoreMatcher, directory, rootDirectory, state, enforceTrust);
1503
+ const entries = await readBoundedPackageEntries(
1504
+ directory,
1505
+ state,
1506
+ resourceType === "skills" ? "reject-skill" : "skip"
1507
+ );
1508
+ if (resourceType === "skills") {
1509
+ const rootSkill = entries.find((entry) => entry.name === "SKILL.md");
1510
+ if (rootSkill) {
1511
+ const skillPath = path3.join(directory, rootSkill.name);
1512
+ const skillStats = await statIfPresent(skillPath);
1513
+ const relativePath = toPosixPath(path3.relative(rootDirectory, skillPath));
1514
+ if (skillStats?.isFile() && !ignoreMatcher.ignores(relativePath)) {
1515
+ if (enforceTrust) assertExtensionPackagePathTrusted(skillPath, "extension package resource", state);
1516
+ return;
1517
+ }
1518
+ }
1519
+ }
1520
+ for (const entry of entries) {
1521
+ throwIfAttachmentAborted(state.signal);
1522
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1523
+ const entryPath = path3.join(directory, entry.name);
1524
+ const relativePath = toPosixPath(path3.relative(rootDirectory, entryPath));
1525
+ const entryStats = await statIfPresent(entryPath);
1526
+ throwIfAttachmentAborted(state.signal);
1527
+ const isSkillCandidate = resourceType === "skills" && (entry.name === "SKILL.md" || directory === rootDirectory && entry.name.endsWith(".md"));
1528
+ if (isSkillCandidate && (!entryStats || !entryStats.isFile() && !entryStats.isDirectory()) && !ignoreMatcher.ignores(relativePath) && !ignoreMatcher.ignores(`${relativePath}/`)) {
1529
+ throw new Error("Subagent extension package must not contain an invalid or unreadable declared skill.");
1530
+ }
1531
+ if (!entryStats) continue;
1532
+ if (ignoreMatcher.ignores(entryStats.isDirectory() ? `${relativePath}/` : relativePath)) continue;
1533
+ if (entryStats.isFile()) {
1534
+ const isResourceFile = entry.name.endsWith(resourceType === "themes" ? ".json" : ".md") && (resourceType !== "skills" || directory === rootDirectory);
1535
+ if (enforceTrust && isResourceFile) {
1536
+ assertExtensionPackagePathTrusted(entryPath, "extension package resource", state);
1537
+ }
1538
+ continue;
1539
+ }
1540
+ if (!entryStats.isDirectory()) continue;
1541
+ if (enforceTrust) assertExtensionPackagePathTrusted(entryPath, "extension package resource", state);
1542
+ await inspectPackageResourceTree(
1543
+ entryPath,
1544
+ resourceType,
1545
+ ignoreMatcher,
1546
+ rootDirectory,
1547
+ depth + 1,
1548
+ state,
1549
+ enforceTrust
1550
+ );
1551
+ }
1552
+ } finally {
1553
+ state.ancestors.delete(canonicalDirectory);
1554
+ }
1555
+ }
1556
+ async function enterPackageResourceDirectory(directory, resourceType, depth, state, enforceTrust) {
1557
+ throwIfAttachmentAborted(state.signal);
1558
+ if (depth > MAX_EXTENSION_SCAN_DEPTH) throwExtensionScanLimit();
1559
+ let canonicalDirectory;
1560
+ try {
1561
+ canonicalDirectory = await realpathAsync2(directory);
1562
+ } catch {
1563
+ throwIfAttachmentAborted(state.signal);
1564
+ if (resourceType === "skills") throwUnreadablePackageSkillDirectory();
1565
+ return void 0;
1566
+ }
1567
+ throwIfAttachmentAborted(state.signal);
1568
+ if (enforceTrust) {
1569
+ assertProjectPathTrusted(
1570
+ directory,
1571
+ canonicalDirectory,
1572
+ "extension package resource",
1573
+ state.cwd,
1574
+ state.canonicalCwd,
1575
+ state.projectTrusted
1576
+ );
1577
+ }
1578
+ if (state.ancestors.has(canonicalDirectory)) {
1579
+ throw new Error("Subagent extension package must not contain a recursive resource directory link.");
1580
+ }
1581
+ state.ancestors.add(canonicalDirectory);
1582
+ return canonicalDirectory;
1583
+ }
1584
+ async function inspectPiManifest(directory, state, enforceTrust = true) {
1585
+ const manifestPath = path3.join(directory, "package.json");
1586
+ const manifestStats = await statIfPresent(manifestPath);
1587
+ if (!manifestStats) return { hasPiManifest: false };
1588
+ if (!manifestStats.isFile()) {
1589
+ throw new Error("Subagent extension package manifest must be a regular file.");
1590
+ }
1591
+ if (enforceTrust) assertExtensionPackagePathTrusted(manifestPath, "extension package resource", state);
1592
+ state.metadataBytes += manifestStats.size;
1593
+ if (state.metadataBytes > MAX_EXTENSION_METADATA_BYTES) throwExtensionScanLimit();
1594
+ let document;
1595
+ try {
1596
+ document = JSON.parse(
1597
+ (await readFileAsync(manifestPath, { encoding: "utf8", signal: state.signal })).replace(/^\uFEFF/u, "")
1598
+ );
1599
+ } catch (error) {
1600
+ if (isAbortError(error)) throw error;
1601
+ throwIfAttachmentAborted(state.signal);
1602
+ return { hasPiManifest: false };
1603
+ }
1604
+ if (!isRecord(document) || !isRecord(document.pi)) return { hasPiManifest: false };
1605
+ return {
1606
+ hasPiManifest: true,
1607
+ manifest: {
1608
+ extensions: readManifestEntries(document.pi, "extensions"),
1609
+ skills: readManifestEntries(document.pi, "skills"),
1610
+ prompts: readManifestEntries(document.pi, "prompts"),
1611
+ themes: readManifestEntries(document.pi, "themes")
1612
+ }
1613
+ };
1614
+ }
1615
+ function readManifestEntries(manifest, resourceType) {
1616
+ const entries = manifest[resourceType];
1617
+ return Array.isArray(entries) && entries.every((entrypoint) => typeof entrypoint === "string") ? entries : void 0;
1618
+ }
1619
+ async function addPackageIgnoreRules(ignoreMatcher, directory, rootDirectory, state, enforceTrust) {
1620
+ await addAttachmentIgnoreRules(ignoreMatcher, directory, rootDirectory, state.signal, (ignorePath, bytes) => {
1621
+ if (enforceTrust) assertExtensionPackagePathTrusted(ignorePath, "extension package resource", state);
1622
+ state.metadataBytes += bytes;
1623
+ if (state.metadataBytes > MAX_EXTENSION_METADATA_BYTES) throwExtensionScanLimit();
1624
+ });
1625
+ }
1626
+ async function readBoundedPackageEntries(directory, state, onFailure) {
1627
+ let directoryHandle;
1628
+ try {
1629
+ directoryHandle = await opendirAsync2(directory);
1630
+ } catch (error) {
1631
+ if (isAbortError(error)) throw error;
1632
+ throwIfAttachmentAborted(state.signal);
1633
+ return failedPackageDirectoryRead(onFailure);
1634
+ }
1635
+ const entries = [];
1636
+ try {
1637
+ for await (const entry of directoryHandle) {
1638
+ throwIfAttachmentAborted(state.signal);
1639
+ state.entries++;
1640
+ if (state.entries > MAX_EXTENSION_SCAN_ENTRIES) throwExtensionScanLimit();
1641
+ entries.push(entry);
1642
+ }
1643
+ } catch (error) {
1644
+ if (isAbortError(error) || isExtensionScanLimitError(error)) throw error;
1645
+ throwIfAttachmentAborted(state.signal);
1646
+ return failedPackageDirectoryRead(onFailure);
1647
+ }
1648
+ throwIfAttachmentAborted(state.signal);
1649
+ return entries;
1650
+ }
1651
+ function failedPackageDirectoryRead(onFailure) {
1652
+ if (onFailure === "reject") throwUnresolvableExtensionEntrypoint();
1653
+ if (onFailure === "reject-skill") throwUnreadablePackageSkillDirectory();
1654
+ return [];
1655
+ }
1656
+ function throwUnreadablePackageSkillDirectory() {
1657
+ throw new Error("Subagent extension package must not contain an unreadable declared skill directory.");
1658
+ }
1659
+ async function hasRegularExtensionIndex(directory, state, enforceTrust = true) {
1660
+ for (const filename of ["index.ts", "index.js"]) {
1661
+ const indexPath = path3.join(directory, filename);
1662
+ const stats = await statIfPresent(indexPath);
1663
+ if (stats) {
1664
+ if (enforceTrust) assertExtensionPackagePathTrusted(indexPath, "extension entrypoint", state);
1665
+ return stats.isFile();
1666
+ }
1667
+ }
1668
+ return false;
1669
+ }
1670
+ function assertExtensionPackagePathTrusted(resourcePath, label, state) {
1671
+ if (state.projectTrusted) return;
1672
+ const canonicalPath = realpath(resourcePath, `Subagent ${label}`);
1673
+ assertProjectPathTrusted(resourcePath, canonicalPath, label, state.cwd, state.canonicalCwd, state.projectTrusted);
1674
+ }
1675
+ async function statIfPresent(value) {
1676
+ try {
1677
+ return await statAsync2(value);
1678
+ } catch {
1679
+ return void 0;
1680
+ }
1681
+ }
1682
+ function throwUnresolvableExtensionEntrypoint() {
1683
+ throw new Error("Subagent extension package must not contain a missing or unresolvable declared entrypoint.");
1684
+ }
1685
+ function isExtensionScanLimitError(error) {
1686
+ return error instanceof Error && error.name === "ExtensionScanLimitError";
1687
+ }
1688
+ function throwExtensionScanLimit() {
1689
+ const error = new Error(
1690
+ `Subagent extension attachment exceeds preflight limits (${MAX_EXTENSION_SCAN_ENTRIES} entries, depth ${MAX_EXTENSION_SCAN_DEPTH}, or ${MAX_EXTENSION_METADATA_BYTES} metadata bytes).`
1691
+ );
1692
+ error.name = "ExtensionScanLimitError";
1693
+ throw error;
1694
+ }
1695
+ function isExtensionOverridePattern(value) {
1696
+ return value.startsWith("!") || value.startsWith("+") || value.startsWith("-");
1697
+ }
1698
+ function hasExtensionGlob(value) {
1699
+ return value.includes("*") || value.includes("?");
1700
+ }
1701
+ function resolveExtensionToolName(value) {
1702
+ if (typeof value !== "string" || !value.trim()) throw new Error("Subagent extension tool name is required.");
1703
+ const name = value.trim();
1704
+ if (name.length > MAX_EXTENSION_TOOL_NAME_LENGTH || name.includes(",") || hasControlCharacter(name)) {
1705
+ throw new Error(
1706
+ `Subagent extension tool name must be at most ${MAX_EXTENSION_TOOL_NAME_LENGTH} characters without commas or control characters.`
1707
+ );
1708
+ }
1709
+ return name;
1710
+ }
1711
+ function isLocalPath(value) {
1712
+ if (value.startsWith("//") || value.startsWith("\\\\")) return false;
1713
+ if (path3.isAbsolute(value) || path3.win32.isAbsolute(value)) return true;
1714
+ return !/^[a-z][a-z0-9+.-]*:/iu.test(value);
1715
+ }
1716
+ function hasControlCharacter(value) {
1717
+ return [...value].some((character) => {
1718
+ const codePoint = character.codePointAt(0) ?? 0;
1719
+ return codePoint <= 31 || codePoint >= 127 && codePoint <= 159;
1720
+ });
1721
+ }
1722
+
686
1723
  // src/runtime.ts
687
1724
  var MAX_ACTIVE_JOBS = 8;
688
1725
  var MAX_RETAINED_TERMINAL_JOBS = 32;
@@ -714,6 +1751,9 @@ var SubagentRuntime = class {
714
1751
  this.sessionActive = true;
715
1752
  this.notifyJobsChanged();
716
1753
  }
1754
+ getSessionGeneration() {
1755
+ return this.generation;
1756
+ }
717
1757
  subscribeJobs(listener) {
718
1758
  this.jobListeners.add(listener);
719
1759
  return () => this.jobListeners.delete(listener);
@@ -744,24 +1784,25 @@ var SubagentRuntime = class {
744
1784
  generation: this.generation
745
1785
  });
746
1786
  let resolveTerminal;
747
- const terminal = new Promise((resolve2) => {
748
- resolveTerminal = resolve2;
1787
+ const terminal = new Promise((resolve4) => {
1788
+ resolveTerminal = resolve4;
749
1789
  });
750
1790
  let resolveControl;
751
1791
  let rejectControl;
752
- const controlReady = new Promise((resolve2, reject) => {
753
- resolveControl = resolve2;
1792
+ const controlReady = new Promise((resolve4, reject) => {
1793
+ resolveControl = resolve4;
754
1794
  rejectControl = reject;
755
1795
  });
756
1796
  void controlReady.catch(() => void 0);
757
1797
  const controller = new AbortController();
1798
+ const effectiveTools = [.../* @__PURE__ */ new Set([...input.tools, ...input.extensions.flatMap((extension) => extension.tools)])];
758
1799
  const job = {
759
1800
  jobId,
760
1801
  state: "queued",
761
1802
  createdAt: this.now(),
762
1803
  ...input.timeout !== void 0 ? { timeout: input.timeout } : {},
763
1804
  controller,
764
- tools: [...input.tools],
1805
+ tools: effectiveTools,
765
1806
  terminal,
766
1807
  resolveTerminal,
767
1808
  controlReady,
@@ -788,6 +1829,14 @@ var SubagentRuntime = class {
788
1829
  child = await this.runChild({
789
1830
  task: input.task,
790
1831
  tools: [...input.tools],
1832
+ skills: [...input.skills],
1833
+ extensions: input.extensions.map((extension) => ({
1834
+ path: extension.path,
1835
+ tools: [...extension.tools]
1836
+ })),
1837
+ toolSources: Object.fromEntries(
1838
+ Object.entries(input.toolSources).map(([tool, sources]) => [tool, [...sources]])
1839
+ ),
791
1840
  model: input.model,
792
1841
  thinkingLevel: input.thinkingLevel,
793
1842
  cwd: input.cwd,
@@ -893,21 +1942,21 @@ var SubagentRuntime = class {
893
1942
  let timeout;
894
1943
  let onAbort;
895
1944
  let unsubscribeMessage = () => void 0;
896
- const message = new Promise((resolve2) => {
897
- unsubscribeMessage = this.broker.subscribeInboundMessage(() => resolve2("message"));
1945
+ const message = new Promise((resolve4) => {
1946
+ unsubscribeMessage = this.broker.subscribeInboundMessage(() => resolve4("message"));
898
1947
  });
899
1948
  const outcome = await Promise.race([
900
1949
  job.terminal.then(() => "terminal"),
901
1950
  message,
902
1951
  ...timeoutMs !== void 0 ? [
903
- new Promise((resolve2) => {
904
- timeout = setTimeout(() => resolve2("timeout"), timeoutMs);
1952
+ new Promise((resolve4) => {
1953
+ timeout = setTimeout(() => resolve4("timeout"), timeoutMs);
905
1954
  timeout.unref();
906
1955
  })
907
1956
  ] : [],
908
1957
  ...signal ? [
909
- new Promise((resolve2) => {
910
- onAbort = () => resolve2("aborted");
1958
+ new Promise((resolve4) => {
1959
+ onAbort = () => resolve4("aborted");
911
1960
  signal.addEventListener("abort", onAbort, { once: true });
912
1961
  })
913
1962
  ] : []
@@ -1089,7 +2138,7 @@ function abortError2(message) {
1089
2138
 
1090
2139
  // src/tools.ts
1091
2140
  var MAX_TASK_BYTES = 50 * 1024;
1092
- var MAX_TOOLS = 64;
2141
+ var MAX_TOOLS = MAX_SELECTED_TOOLS;
1093
2142
  var MESSAGE_TYPE = "pi-subagents-message";
1094
2143
  var CHILD_CORE_TOOL_SET = new Set(CHILD_CORE_TOOL_NAMES);
1095
2144
  var THINKING_LEVEL_SET = new Set(SUBAGENT_THINKING_LEVELS);
@@ -1110,6 +2159,48 @@ var SpawnParameters = Type.Object(
1110
2159
  }
1111
2160
  )
1112
2161
  ),
2162
+ skills: Type.Optional(
2163
+ Type.Array(
2164
+ Type.String({
2165
+ description: "Local Markdown skill file or directory whose declared Pi skills all load and have names unique across attachments, resolved from the child working directory.",
2166
+ minLength: 1,
2167
+ maxLength: MAX_RESOURCE_PATH_BYTES
2168
+ }),
2169
+ {
2170
+ description: "Explicit local skills available to the child. Automatic skill discovery stays disabled.",
2171
+ maxItems: MAX_ATTACHED_SKILLS
2172
+ }
2173
+ )
2174
+ ),
2175
+ extensions: Type.Optional(
2176
+ Type.Array(
2177
+ Type.Object(
2178
+ {
2179
+ path: Type.String({
2180
+ description: "Local extension file or directory path, resolved from the child working directory.",
2181
+ minLength: 1,
2182
+ maxLength: MAX_RESOURCE_PATH_BYTES
2183
+ }),
2184
+ tools: Type.Array(
2185
+ Type.String({
2186
+ description: "Exact extension-specific tool name to activate initially; built-in names are rejected.",
2187
+ minLength: 1,
2188
+ maxLength: MAX_EXTENSION_TOOL_NAME_LENGTH
2189
+ }),
2190
+ {
2191
+ description: "Extension tools to activate. Use an empty list for lifecycle or provider behavior only. All child tool names share a 16 KiB startup bootstrap.",
2192
+ maxItems: MAX_SELECTED_TOOLS
2193
+ }
2194
+ )
2195
+ },
2196
+ { additionalProperties: false }
2197
+ ),
2198
+ {
2199
+ description: "Explicit trusted local extensions loaded in the isolated child process.",
2200
+ maxItems: MAX_ATTACHED_EXTENSIONS
2201
+ }
2202
+ )
2203
+ ),
1113
2204
  thinkingLevel: Type.Optional(
1114
2205
  StringEnum(SUBAGENT_THINKING_LEVELS, {
1115
2206
  description: "Child thinking level. Defaults to the main agent's effective level."
@@ -1168,7 +2259,7 @@ function registerSubagentTools(pi, dependencies = {}) {
1168
2259
  pi.registerTool({
1169
2260
  name: "subagent_spawn",
1170
2261
  label: "Subagent \xB7 Spawn",
1171
- description: "Use subagent_spawn to start one Pi subagent job and return its jobId immediately. The task defines the child's specialization, and the selected tools define its capabilities. The job may ask the main agent questions and publishes one asynchronous completion when terminal.",
2262
+ description: "Use subagent_spawn to start one Pi subagent job and return its jobId immediately. The task defines the child's specialization; selected core tools and explicit trusted local skills or extensions define its initial capabilities. The job may ask the main agent questions and publishes one asynchronous completion when terminal.",
1172
2263
  promptSnippet: "Use subagent_spawn to start one Pi subagent job",
1173
2264
  parameters: SpawnParameters,
1174
2265
  prepareArguments: prepareSpawnArguments,
@@ -1177,18 +2268,50 @@ function registerSubagentTools(pi, dependencies = {}) {
1177
2268
  assertNotNested();
1178
2269
  const task = validateTask(params.task, "subagent_spawn");
1179
2270
  const tools = resolveTools(params.tools);
1180
- const model = resolveChildModel(ctx);
2271
+ const cwd = ctx.cwd;
2272
+ const projectTrusted = ctx.isProjectTrusted();
2273
+ const sessionGeneration = runtime.getSessionGeneration();
2274
+ const attachments = await resolveResourceAttachments(
2275
+ { skills: params.skills, extensions: params.extensions },
2276
+ { cwd, projectTrusted, coreTools: tools, signal }
2277
+ );
2278
+ throwIfAborted2(signal, "Subagent spawn was cancelled");
2279
+ if (runtime.getSessionGeneration() !== sessionGeneration) {
2280
+ throw new Error("Subagent spawn session changed during attachment validation; retry the request.");
2281
+ }
2282
+ if (ctx.cwd !== cwd || ctx.isProjectTrusted() !== projectTrusted) {
2283
+ throw new Error("Subagent spawn context changed during attachment validation; retry the request.");
2284
+ }
2285
+ if (attachments.extensions.length > 0) {
2286
+ assertChildBootstrapCapacity([.../* @__PURE__ */ new Set([...attachments.effectiveTools, ...CHILD_COMMUNICATION_TOOL_NAMES])]);
2287
+ }
2288
+ const model = resolveChildModel(ctx, attachments.extensions.length > 0);
1181
2289
  const thinkingLevel = resolveThinkingLevel(params.thinkingLevel ?? ctx.thinkingLevel ?? pi.getThinkingLevel());
1182
2290
  resolveTimeoutMs(params.timeout);
2291
+ assertChildCommandCapacity(
2292
+ {
2293
+ tools,
2294
+ skills: attachments.skills,
2295
+ extensions: attachments.extensions,
2296
+ model,
2297
+ thinkingLevel,
2298
+ projectTrusted
2299
+ },
2300
+ dependencies.platform
2301
+ );
2302
+ throwIfAborted2(signal, "Subagent spawn was cancelled");
1183
2303
  return toolResult(
1184
2304
  runtime.start({
1185
2305
  task,
1186
2306
  tools,
2307
+ skills: attachments.skills,
2308
+ extensions: attachments.extensions,
2309
+ toolSources: attachments.toolSources,
1187
2310
  model,
1188
2311
  thinkingLevel,
1189
- cwd: ctx.cwd,
2312
+ cwd,
1190
2313
  timeout: params.timeout,
1191
- projectTrusted: ctx.isProjectTrusted()
2314
+ projectTrusted
1192
2315
  })
1193
2316
  );
1194
2317
  }
@@ -1322,11 +2445,11 @@ function resolveTools(value) {
1322
2445
  }
1323
2446
  return tools;
1324
2447
  }
1325
- function resolveChildModel(ctx) {
2448
+ function resolveChildModel(ctx, hasAttachedExtensions) {
1326
2449
  const model = ctx.model;
1327
2450
  if (!model) throw new Error("Subagent model is unavailable because no main-agent model is selected.");
1328
2451
  const provider = sanitizeTerminalText(model.provider).slice(0, 128);
1329
- if (ctx.modelRegistry.getRegisteredProviderIds().includes(model.provider)) {
2452
+ if (!hasAttachedExtensions && ctx.modelRegistry.getRegisteredProviderIds().includes(model.provider)) {
1330
2453
  throw new Error(`Subagent model provider ${provider} is unavailable because children disable parent extensions.`);
1331
2454
  }
1332
2455
  if (ctx.modelRegistry.getProviderAuthStatus(model.provider).source === "runtime") {
@@ -1348,15 +2471,6 @@ function prepareSpawnArguments(args) {
1348
2471
  function prepareWaitArguments(args) {
1349
2472
  return prepareTimeoutArguments(args);
1350
2473
  }
1351
- function prepareTimeoutArguments(args) {
1352
- if (!args || typeof args !== "object") return args;
1353
- if (!Object.hasOwn(args, "timeoutMs")) return args;
1354
- const record = args;
1355
- if (typeof record.timeoutMs !== "number") return record;
1356
- const { timeoutMs, ...prepared } = record;
1357
- if (prepared.timeout === void 0) return { ...prepared, timeout: timeoutMs / 1e3 };
1358
- return prepared;
1359
- }
1360
2474
  function resolveMainSendArguments(params) {
1361
2475
  validateMessage(params.message, "Subagent message");
1362
2476
  const recipient = optionalIdentifier(params.recipient, "recipient");