@pinet/slack-bridge 0.1.2 → 0.2.0

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.
Files changed (47) hide show
  1. package/README.md +59 -32
  2. package/dist/broker/adapters/slack.d.ts +19 -1
  3. package/dist/broker/adapters/slack.js +111 -22
  4. package/dist/broker/client.d.ts +2 -1
  5. package/dist/broker/client.js +1 -0
  6. package/dist/broker/socket-server.js +18 -0
  7. package/dist/deploy-manifest.d.ts +5 -0
  8. package/dist/deploy-manifest.js +30 -1
  9. package/dist/follower-runtime.js +5 -1
  10. package/dist/helpers.d.ts +14 -0
  11. package/dist/helpers.js +45 -21
  12. package/dist/index.js +60 -0
  13. package/dist/pinet-commands.d.ts +6 -1
  14. package/dist/pinet-commands.js +166 -1
  15. package/dist/pinet-mesh-ops.d.ts +11 -0
  16. package/dist/pinet-mesh-ops.js +17 -0
  17. package/dist/pinet-tools.d.ts +47 -0
  18. package/dist/pinet-tools.js +496 -36
  19. package/dist/prompts/broker/tmux.md +2 -2
  20. package/dist/reaction-triggers.d.ts +1 -0
  21. package/dist/reaction-triggers.js +26 -15
  22. package/dist/runtime-agent-context.js +19 -0
  23. package/dist/runtime-mode.js +7 -1
  24. package/dist/single-player-runtime.js +22 -26
  25. package/dist/slack-access.d.ts +11 -0
  26. package/dist/slack-access.js +30 -0
  27. package/dist/slack-agents-command.d.ts +19 -0
  28. package/dist/slack-agents-command.js +90 -0
  29. package/dist/slack-export.d.ts +1 -1
  30. package/dist/slack-export.js +6 -4
  31. package/dist/slack-file-access.d.ts +34 -0
  32. package/dist/slack-file-access.js +209 -0
  33. package/dist/slack-message-context.d.ts +0 -1
  34. package/dist/slack-message-context.js +1 -6
  35. package/dist/slack-pinet-runtime-adapter.d.ts +4 -2
  36. package/dist/slack-pinet-runtime-adapter.js +12 -0
  37. package/dist/slack-tools.d.ts +6 -0
  38. package/dist/slack-tools.js +290 -36
  39. package/dist/slack-upload.d.ts +13 -1
  40. package/dist/slack-upload.js +29 -2
  41. package/dist/stale-slack-messages.d.ts +12 -0
  42. package/dist/stale-slack-messages.js +29 -0
  43. package/dist/subtree-broker-runtime.d.ts +109 -0
  44. package/dist/subtree-broker-runtime.js +558 -0
  45. package/manifest.yaml +9 -0
  46. package/package.json +9 -7
  47. package/skills/slack-bridge/SKILL.md +60 -1
@@ -2,6 +2,7 @@ import os from "node:os";
2
2
  import { buildCompactPinetReadDetails, formatPinetReadResultCompact, formatPinetReadResultFull, } from "@pinet/pinet-core/pinet-read-formatting";
3
3
  import { normalizePinetOutputOptions, } from "@pinet/pinet-core/output-options";
4
4
  import { parseScheduledWakeupDelay, resolveScheduledWakeupFireAt, } from "@pinet/pinet-core/scheduled-wakeups";
5
+ import { Text } from "@earendil-works/pi-tui";
5
6
  import { Type } from "@sinclair/typebox";
6
7
  import { buildAgentDisplayInfo, filterAgentsForMeshVisibility, formatAgentList, rankAgentsForRouting, } from "./helpers.js";
7
8
  import { isBroadcastChannelTarget } from "./broker/agent-messaging.js";
@@ -19,14 +20,20 @@ const PINET_DISPATCHER_EXAMPLES = {
19
20
  { action: "snooze", args: { op: "clear" } },
20
21
  ],
21
22
  schedule: [{ action: "schedule", args: { delay: "30m", message: "Check queue state" } }],
22
- agents: [{ action: "agents", args: { repo: "<repo>", role: "worker", full: true } }],
23
+ agents: [{ action: "agents", args: { repo: "<repo>", role: "worker" } }],
24
+ spawn: [
25
+ {
26
+ action: "spawn",
27
+ args: { task: "Review PR #123", repo: "extensions", role: "reviewer", lane_id: "issue-123" },
28
+ },
29
+ ],
23
30
  ports: [
24
31
  { action: "ports", args: { op: "acquire", purpose: "preview", ttl_ms: 600000 } },
25
32
  { action: "ports", args: { op: "renew", lease_id: "<lease-id>", ttl_ms: 600000 } },
26
33
  { action: "ports", args: { op: "list", include_inactive: true } },
27
34
  ],
28
35
  lanes: [
29
- { action: "lanes", args: { op: "list", full: true } },
36
+ { action: "lanes", args: { op: "list" } },
30
37
  {
31
38
  action: "lanes",
32
39
  args: {
@@ -44,9 +51,10 @@ const PINET_DISPATCHER_EXAMPLES = {
44
51
  const PINET_OUTPUT_OPTION_PARAMETERS = {
45
52
  format: Type.Optional(Type.String({ description: 'Response presentation format: "cli" (default) or "json".' })),
46
53
  full: Type.Optional(Type.Boolean({
47
- description: "Include full verbose text/details; default cli output keeps data.details compact.",
54
+ description: "Include full verbose text/details. JSON format alone keeps data.details compact.",
48
55
  })),
49
56
  };
57
+ const PINET_COMPACT_LIST_LIMIT = 10;
50
58
  function getRecordString(record, key) {
51
59
  const value = record?.[key];
52
60
  return typeof value === "string" && value.trim() ? value : undefined;
@@ -75,6 +83,7 @@ function normalizeDispatcherAction(value) {
75
83
  "agents",
76
84
  "lanes",
77
85
  "ports",
86
+ "spawn",
78
87
  "reload",
79
88
  "exit",
80
89
  ];
@@ -119,7 +128,8 @@ function classifyPinetError(message) {
119
128
  message.includes("lease_id") ||
120
129
  message.includes("ttl_ms") ||
121
130
  message.includes("purpose") ||
122
- message.includes("port")) {
131
+ message.includes("port") ||
132
+ message.includes("spawn")) {
123
133
  return {
124
134
  class: "input",
125
135
  message,
@@ -159,7 +169,7 @@ function formatPinetHelpCliText(data) {
159
169
  const description = typeof data.description === "string" && data.description.trim()
160
170
  ? ` — ${data.description.trim()}`
161
171
  : "";
162
- return `Pinet ${data.action}${description}. Use args.format="json" or args.full=true for schema/details; JSON/full output can fill context quickly.`;
172
+ return `Pinet ${data.action}${description}. Use args.format="json" for the compact envelope, or args.full=true for verbose schema/debug details.`;
163
173
  }
164
174
  return null;
165
175
  }
@@ -172,14 +182,14 @@ function getPinetEnvelopeCliText(envelope) {
172
182
  if (helpText)
173
183
  return helpText;
174
184
  const action = typeof envelope.data.action === "string" ? ` ${envelope.data.action}` : "";
175
- return `Pinet${action} succeeded. Use args.format="json" or args.full=true for details; JSON/full output can fill context quickly.`;
185
+ return `Pinet${action} succeeded. Use args.format="json" for the compact envelope, or args.full=true for verbose details.`;
176
186
  }
177
187
  const errors = envelope.errors.map((error) => error.message).filter(Boolean);
178
188
  const hints = Array.from(new Set(envelope.errors.map((error) => error.hint).filter((hint) => hint.length > 0)));
179
189
  if (errors.length > 0) {
180
190
  return `Pinet ${envelope.status}: ${errors.join("; ")}${hints.length > 0 ? ` Hint: ${hints.join(" ")}` : ""}`;
181
191
  }
182
- return `Pinet ${envelope.status}. Use args.format="json" or args.full=true for details; JSON/full output can fill context quickly.`;
192
+ return `Pinet ${envelope.status}. Use args.format="json" for the compact envelope, or args.full=true for verbose details.`;
183
193
  }
184
194
  function getTolerantPinetOutputOptions(value) {
185
195
  if (!isRecord(value))
@@ -212,29 +222,165 @@ function shouldRenderStructuredPinetEnvelope(envelope, output) {
212
222
  const text = envelope.data.text;
213
223
  return typeof text !== "string" || text.length === 0;
214
224
  }
215
- function wrapDispatcherEnvelope(envelope, output = { format: "cli", full: false }) {
225
+ function wrapDispatcherEnvelope(envelope, output = { format: "cli", full: false }, expandedText, displayText) {
216
226
  return {
217
227
  content: [
218
228
  {
219
229
  type: "text",
220
230
  text: shouldRenderStructuredPinetEnvelope(envelope, output)
221
- ? JSON.stringify(envelope, null, 2)
231
+ ? JSON.stringify(envelope, null, output.full ? 2 : 0)
222
232
  : getPinetEnvelopeCliText(envelope),
223
233
  },
224
234
  ],
225
235
  details: envelope,
236
+ ...(expandedText ? { expandedText } : {}),
237
+ ...(displayText ? { displayText } : {}),
238
+ };
239
+ }
240
+ function isPinetDispatcherEnvelope(value) {
241
+ if (!isRecord(value))
242
+ return false;
243
+ if (value.status !== "succeeded" && value.status !== "failed")
244
+ return false;
245
+ return Array.isArray(value.errors) && Array.isArray(value.warnings);
246
+ }
247
+ function getFirstTextContent(result) {
248
+ return (result.content?.find((block) => block.type === "text" && typeof block.text === "string")
249
+ ?.text ?? "");
250
+ }
251
+ function parsePinetDispatcherEnvelopeFromText(text) {
252
+ if (!text.trim().startsWith("{"))
253
+ return null;
254
+ try {
255
+ const parsed = JSON.parse(text);
256
+ return isPinetDispatcherEnvelope(parsed) ? parsed : null;
257
+ }
258
+ catch {
259
+ return null;
260
+ }
261
+ }
262
+ function summarizeDetailValue(value) {
263
+ if (value === null)
264
+ return "null";
265
+ if (Array.isArray(value)) {
266
+ return `${value.length} item${value.length === 1 ? "" : "s"}`;
267
+ }
268
+ if (typeof value === "object") {
269
+ const entries = Object.entries(value);
270
+ if (entries.length === 0)
271
+ return "{}";
272
+ const inline = entries
273
+ .slice(0, 6)
274
+ .map(([key, nested]) => {
275
+ if (nested === null || ["string", "number", "boolean"].includes(typeof nested)) {
276
+ return `${key}=${String(nested)}`;
277
+ }
278
+ return `${key}=…`;
279
+ })
280
+ .join(", ");
281
+ return `{ ${inline}${entries.length > 6 ? ", …" : ""} }`;
282
+ }
283
+ return String(value);
284
+ }
285
+ // Renders `data.details` as compact key lines. Arrays and nested objects are
286
+ // summarized (count / inlined keys) so the expanded view never JSON-dumps a
287
+ // wall of structured data into the operator's TUI.
288
+ function formatPrimitiveDetails(details) {
289
+ return Object.entries(details).map(([key, value]) => `${key}: ${summarizeDetailValue(value)}`);
290
+ }
291
+ const PINET_SEND_PREVIEW_MAX = 80;
292
+ const PINET_SEND_BODY_MAX = 600;
293
+ // Single-line, whitespace-collapsed, length-capped preview of a sent message.
294
+ // Operator-facing only; kept short so model/CLI text stays compact.
295
+ function formatMessagePreview(message, maxLength = PINET_SEND_PREVIEW_MAX) {
296
+ const collapsed = message.replace(/\s+/g, " ").trim();
297
+ if (collapsed.length <= maxLength)
298
+ return collapsed;
299
+ return `${collapsed.slice(0, Math.max(0, maxLength - 1))}…`;
300
+ }
301
+ function countMessageLines(message) {
302
+ if (message.length === 0)
303
+ return 0;
304
+ return message.split(/\r?\n/).length;
305
+ }
306
+ // Capped multi-line body for the expanded TUI card. Preserves line breaks so the
307
+ // operator can see structure, but truncates very long bodies.
308
+ function capMessageBody(message, maxLength = PINET_SEND_BODY_MAX) {
309
+ if (message.length <= maxLength)
310
+ return message;
311
+ return `${message.slice(0, Math.max(0, maxLength - 1))}…`;
312
+ }
313
+ function formatSentMessageSuffix(message) {
314
+ const preview = formatMessagePreview(message);
315
+ if (!preview)
316
+ return "";
317
+ const lines = countMessageLines(message);
318
+ return ` · “${preview}”${lines > 1 ? ` (${lines} lines)` : ""}`;
319
+ }
320
+ function buildSentMessageExpandedText(fields, message) {
321
+ const lines = fields
322
+ .filter((entry) => entry[1] !== undefined)
323
+ .map(([label, value]) => `${label}: ${value}`);
324
+ const chars = message.length;
325
+ const lineCount = countMessageLines(message);
326
+ lines.push(`message (${lineCount} line${lineCount === 1 ? "" : "s"}, ${chars} chars):`);
327
+ lines.push(capMessageBody(message));
328
+ return lines.join("\n");
329
+ }
330
+ function formatPinetEnvelopeExpandedText(envelope, expandedText) {
331
+ const lines = [getPinetEnvelopeCliText(envelope)];
332
+ if (envelope.status === "failed") {
333
+ lines.push(`status: ${envelope.status}`);
334
+ for (const error of envelope.errors) {
335
+ lines.push(`error[${error.class}]: ${error.message}`);
336
+ if (error.hint)
337
+ lines.push(`hint: ${error.hint}`);
338
+ }
339
+ }
340
+ else if (expandedText && expandedText.trim().length > 0) {
341
+ lines.push(...expandedText.split("\n"));
342
+ }
343
+ else if (isRecord(envelope.data)) {
344
+ lines.push(`status: ${envelope.status}`);
345
+ const action = typeof envelope.data.action === "string" ? envelope.data.action : undefined;
346
+ if (action)
347
+ lines.push(`action: ${action}`);
348
+ const details = envelope.data.details;
349
+ if (isRecord(details)) {
350
+ lines.push("details:");
351
+ lines.push(...formatPrimitiveDetails(details).map((line) => ` ${line}`));
352
+ }
353
+ }
354
+ for (const warning of envelope.warnings) {
355
+ lines.push(`warning: ${warning}`);
356
+ }
357
+ return lines.join("\n");
358
+ }
359
+ export function formatPinetDispatcherResultForDisplay(result, expanded) {
360
+ const firstText = getFirstTextContent(result);
361
+ const envelope = isPinetDispatcherEnvelope(result.details)
362
+ ? result.details
363
+ : parsePinetDispatcherEnvelopeFromText(firstText);
364
+ if (!envelope) {
365
+ return { status: "unknown", text: firstText || "Pinet result." };
366
+ }
367
+ return {
368
+ status: envelope.status,
369
+ text: expanded
370
+ ? formatPinetEnvelopeExpandedText(envelope, result.expandedText)
371
+ : (result.displayText ?? getPinetEnvelopeCliText(envelope)),
226
372
  };
227
373
  }
228
374
  function selectPinetResultDetails(result, output) {
229
- if (output.format === "json" || output.full) {
230
- return result.details ?? null;
375
+ if (output.full) {
376
+ return result.fullDetails ?? result.details ?? null;
231
377
  }
232
378
  if (result.compactDetails !== undefined) {
233
379
  return result.compactDetails;
234
380
  }
235
381
  return result.details ?? null;
236
382
  }
237
- function buildPinetDispatcherHelpEnvelope(args, actions) {
383
+ function buildPinetDispatcherHelpEnvelope(args, actions, output) {
238
384
  const topic = typeof args.topic === "string" && args.topic.trim()
239
385
  ? args.topic.trim().toLowerCase()
240
386
  : undefined;
@@ -245,13 +391,19 @@ function buildPinetDispatcherHelpEnvelope(args, actions) {
245
391
  action: definition.name,
246
392
  description: definition.description,
247
393
  guardrail_tool: `pinet:${definition.name}`,
248
- args_schema: definition.parameters,
249
- examples: PINET_DISPATCHER_EXAMPLES[definition.name] ?? [],
394
+ ...(output.full
395
+ ? {
396
+ args_schema: definition.parameters,
397
+ examples: PINET_DISPATCHER_EXAMPLES[definition.name] ?? [],
398
+ }
399
+ : {}),
250
400
  }))
251
401
  .sort((a, b) => a.action.localeCompare(b.action));
252
402
  return buildPinetDispatcherEnvelope("succeeded", {
253
403
  actions: catalog,
254
- note: "Use args.topic to inspect a single action schema.",
404
+ note: output.full
405
+ ? "Use args.topic to inspect a single action schema."
406
+ : "Use args.topic to inspect a single action schema; add args.full=true for the full catalog schemas/examples.",
255
407
  });
256
408
  }
257
409
  const definition = actions.get(topic);
@@ -306,17 +458,19 @@ function runPinetSendAction(params, deps, toolName, output) {
306
458
  if (transferThreadId && isBroadcastChannelTarget(to)) {
307
459
  throw new Error("transfer_thread_id requires a direct agent target, not a broadcast channel.");
308
460
  }
461
+ const previewSuffix = formatSentMessageSuffix(message);
309
462
  if (deps.brokerRole() === "broker" && isBroadcastChannelTarget(to)) {
310
463
  const result = deps.sendPinetBroadcastMessage(to, message);
311
464
  const preview = result.recipients.slice(0, 5).join(", ");
312
465
  const suffix = result.recipients.length > 5 ? ", …" : "";
466
+ const recipientList = result.recipients.join(", ");
313
467
  return {
314
468
  content: [
315
469
  {
316
470
  type: "text",
317
471
  text: output.full
318
472
  ? `Broadcast sent to ${result.channel} (${result.recipients.length} agents: ${preview}${suffix}).`
319
- : `Pinet broadcast sent to ${result.channel} (${result.recipients.length} recipients).`,
473
+ : `Pinet broadcast sent to ${result.channel} (${result.recipients.length} recipients)${previewSuffix}.`,
320
474
  },
321
475
  ],
322
476
  details: {
@@ -324,6 +478,26 @@ function runPinetSendAction(params, deps, toolName, output) {
324
478
  messageIds: result.messageIds,
325
479
  recipients: result.recipients,
326
480
  },
481
+ compactDetails: {
482
+ channel: result.channel,
483
+ messageCount: result.messageIds.length,
484
+ recipientCount: result.recipients.length,
485
+ recipients: result.recipients.slice(0, PINET_COMPACT_LIST_LIMIT),
486
+ recipientsTruncated: Math.max(0, result.recipients.length - PINET_COMPACT_LIST_LIMIT),
487
+ },
488
+ fullDetails: {
489
+ channel: result.channel,
490
+ messageIds: result.messageIds,
491
+ recipients: result.recipients,
492
+ },
493
+ machineText: output.full
494
+ ? `Broadcast sent to ${result.channel} (${result.recipients.length} agents: ${preview}${suffix}).`
495
+ : `Pinet broadcast sent to ${result.channel} (${result.recipients.length} recipients).`,
496
+ expandedText: buildSentMessageExpandedText([
497
+ ["to", result.channel],
498
+ ["recipients", `${result.recipients.length} (${recipientList})`],
499
+ ["message ids", result.messageIds.join(", ")],
500
+ ], message),
327
501
  };
328
502
  }
329
503
  const result = transferThreadId
@@ -331,13 +505,16 @@ function runPinetSendAction(params, deps, toolName, output) {
331
505
  threadOwnershipTransfer: { mode: "transfer", threadId: transferThreadId },
332
506
  })
333
507
  : await deps.sendPinetAgentMessage(to, message);
508
+ const transferSuffix = result.transferredThreadId
509
+ ? ` (${result.transferredThreadChannel})`
510
+ : "";
334
511
  return {
335
512
  content: [
336
513
  {
337
514
  type: "text",
338
515
  text: output.full
339
516
  ? `Message sent to ${result.target} (id: ${result.messageId})${result.transferredThreadId ? ` and transferred Slack thread ${result.transferredThreadId}${result.transferredThreadChannel ? ` (${result.transferredThreadChannel})` : ""}` : ""}.`
340
- : `Pinet message sent to ${result.target}${result.transferredThreadId ? `; transferred Slack thread ${result.transferredThreadId}` : ""}.`,
517
+ : `Pinet message sent to ${result.target}${result.transferredThreadId ? `; transferred Slack thread ${result.transferredThreadId}` : ""}${previewSuffix}.`,
341
518
  },
342
519
  ],
343
520
  details: {
@@ -348,6 +525,16 @@ function runPinetSendAction(params, deps, toolName, output) {
348
525
  ? { transferredThreadChannel: result.transferredThreadChannel }
349
526
  : {}),
350
527
  },
528
+ machineText: output.full
529
+ ? `Message sent to ${result.target} (id: ${result.messageId})${result.transferredThreadId ? ` and transferred Slack thread ${result.transferredThreadId}${result.transferredThreadChannel ? ` (${result.transferredThreadChannel})` : ""}` : ""}.`
530
+ : `Pinet message sent to ${result.target}${result.transferredThreadId ? `; transferred Slack thread ${result.transferredThreadId}` : ""}.`,
531
+ expandedText: buildSentMessageExpandedText([
532
+ ["to", result.target],
533
+ ["message id", result.messageId],
534
+ result.transferredThreadId
535
+ ? ["transferred thread", `${result.transferredThreadId}${transferSuffix}`]
536
+ : ["transferred thread", undefined],
537
+ ], message),
351
538
  };
352
539
  })();
353
540
  }
@@ -367,11 +554,12 @@ function runPinetReadAction(params, deps, toolName, output) {
367
554
  throw new Error("thread_id must be a non-empty string when provided.");
368
555
  }
369
556
  const result = await deps.readPinetInbox(options);
557
+ const shouldRenderFull = output.full;
370
558
  return {
371
559
  content: [
372
560
  {
373
561
  type: "text",
374
- text: output.full
562
+ text: shouldRenderFull
375
563
  ? formatPinetReadResultFull(result, options)
376
564
  : formatPinetReadResultCompact(result, options),
377
565
  },
@@ -379,6 +567,10 @@ function runPinetReadAction(params, deps, toolName, output) {
379
567
  details: result,
380
568
  compactDetails: buildCompactPinetReadDetails(result),
381
569
  fullDetails: result,
570
+ // When the model/CLI text is the compact one-liner, give the operator the
571
+ // full per-message rows on expand. In full/thread mode `text` already
572
+ // carries the rich view, so no separate expanded body is needed.
573
+ ...(shouldRenderFull ? {} : { expandedText: formatPinetReadResultFull(result, options) }),
382
574
  };
383
575
  })();
384
576
  }
@@ -455,6 +647,53 @@ function runPinetSnoozeAction(params, deps, toolName) {
455
647
  return { content: [{ type: "text", text: formatRalphSnoozeResult(status) }], details: status };
456
648
  })();
457
649
  }
650
+ function runPinetSpawnAction(params, deps, toolName, output) {
651
+ return (async () => {
652
+ const task = getMaybeString(params, "task");
653
+ const repo = getMaybeString(params, "repo");
654
+ const role = getMaybeString(params, "role") ?? "subworker";
655
+ const laneId = getMaybeString(params, "lane_id");
656
+ deps.requireToolPolicy(toolName, undefined, `task=${task ?? ""} | repo=${repo ?? ""} | role=${role} | lane_id=${laneId ?? ""}`);
657
+ if (!deps.pinetEnabled()) {
658
+ throw new Error("Pinet is not running. Use /pinet start or /pinet follow first.");
659
+ }
660
+ if (deps.brokerRole() !== "follower") {
661
+ throw new Error("spawn is worker-only; the broker should launch top-level followers, not own subtrees.");
662
+ }
663
+ if (!task)
664
+ throw new Error("spawn requires task");
665
+ if (!repo)
666
+ throw new Error("spawn requires repo");
667
+ if (!deps.spawnSubtreeWorker) {
668
+ throw new Error("subtree worker launcher is unavailable in this runtime. Start a worker subtree with /pinet subtree start and retry.");
669
+ }
670
+ const result = await deps.spawnSubtreeWorker({
671
+ task,
672
+ repo,
673
+ role,
674
+ ...(laneId ? { laneId } : {}),
675
+ });
676
+ return {
677
+ content: [
678
+ {
679
+ type: "text",
680
+ text: output.full
681
+ ? `Pinet subtree worker started: ${result.agentName} (${result.agentId}) in tmux session ${result.sessionName}. Task message ${result.messageId} delivered. Monitor: ${result.monitorCommand}`
682
+ : `Pinet subtree worker started: ${result.agentName} (${result.agentId}). Task message ${result.messageId} delivered.`,
683
+ },
684
+ ],
685
+ details: result,
686
+ compactDetails: {
687
+ status: result.status,
688
+ agentId: result.agentId,
689
+ agentName: result.agentName,
690
+ messageId: result.messageId,
691
+ threadId: result.threadId,
692
+ },
693
+ fullDetails: result,
694
+ };
695
+ })();
696
+ }
458
697
  function runPinetRemoteControlAction(params, deps, toolName, command) {
459
698
  return (async () => {
460
699
  const target = typeof params.target === "string" ? params.target.trim() : "";
@@ -495,7 +734,7 @@ function runPinetScheduleAction(params, deps, toolName, output) {
495
734
  type: "text",
496
735
  text: output.full
497
736
  ? `Wake-up scheduled for ${wakeup.fireAt} (id: ${wakeup.id}).`
498
- : `Pinet wake-up scheduled for ${wakeup.fireAt}.`,
737
+ : `Pinet wake-up scheduled for ${wakeup.fireAt} (id ${wakeup.id}).`,
499
738
  },
500
739
  ],
501
740
  details: wakeup,
@@ -509,7 +748,7 @@ function runPinetScheduleAction(params, deps, toolName, output) {
509
748
  type: "text",
510
749
  text: output.full
511
750
  ? `Wake-up scheduled for ${wakeup.fireAt} (id: ${wakeup.id}).`
512
- : `Pinet wake-up scheduled for ${wakeup.fireAt}.`,
751
+ : `Pinet wake-up scheduled for ${wakeup.fireAt} (id ${wakeup.id}).`,
513
752
  },
514
753
  ],
515
754
  details: wakeup,
@@ -527,11 +766,25 @@ function getAgentBranch(agent) {
527
766
  function getAgentRole(agent) {
528
767
  return getRecordString(agent.metadata, "role");
529
768
  }
769
+ function truncatePinetDetailText(value, maxLength = 160) {
770
+ if (!value)
771
+ return null;
772
+ const collapsed = value.replace(/\s+/g, " ").trim();
773
+ if (!collapsed)
774
+ return null;
775
+ if (collapsed.length <= maxLength)
776
+ return collapsed;
777
+ return `${collapsed.slice(0, Math.max(0, maxLength - 1))}…`;
778
+ }
530
779
  function buildCompactAgentDetails(agents, hint) {
780
+ const shownAgents = agents.slice(0, PINET_COMPACT_LIST_LIMIT);
531
781
  return {
532
782
  count: agents.length,
783
+ total: agents.length,
784
+ shown: shownAgents.length,
785
+ truncated: Math.max(0, agents.length - shownAgents.length),
533
786
  hint,
534
- agents: agents.map((agent) => {
787
+ agents: shownAgents.map((agent) => {
535
788
  const capabilityTags = agent.capabilityTags ?? [];
536
789
  return {
537
790
  id: agent.id,
@@ -543,8 +796,11 @@ function buildCompactAgentDetails(agents, hint) {
543
796
  branch: getAgentBranch(agent) ?? null,
544
797
  role: getAgentRole(agent) ?? null,
545
798
  brokerManaged: agent.metadata?.brokerManaged === true,
546
- launchSource: agent.metadata?.launchSource ?? null,
547
- tmuxSession: agent.metadata?.tmuxSession ?? null,
799
+ parentAgentId: agent.metadata?.parentAgentId ?? null,
800
+ treeDepth: agent.metadata?.treeDepth ?? 0,
801
+ supervisionState: agent.metadata?.supervisionState ?? "root",
802
+ subtreeRole: agent.metadata?.subtreeRole ?? null,
803
+ laneId: agent.metadata?.laneId ?? null,
548
804
  routingScore: agent.routingScore ?? null,
549
805
  ...(agent.pendingInboxCount != null && agent.pendingInboxCount > 0
550
806
  ? { pendingInboxCount: agent.pendingInboxCount }
@@ -555,6 +811,42 @@ function buildCompactAgentDetails(agents, hint) {
555
811
  }),
556
812
  };
557
813
  }
814
+ function filterAgentsForHierarchyScope(agents, scope, parentAgentId) {
815
+ if (scope === "all" || scope === "visible")
816
+ return agents;
817
+ if (!parentAgentId)
818
+ return [];
819
+ if (scope === "children") {
820
+ return agents.filter((agent) => agent.parentAgentId === parentAgentId);
821
+ }
822
+ const descendants = [];
823
+ const seen = new Set();
824
+ const queue = agents.filter((agent) => agent.parentAgentId === parentAgentId);
825
+ while (queue.length > 0) {
826
+ const child = queue.shift();
827
+ if (!child || seen.has(child.id))
828
+ continue;
829
+ seen.add(child.id);
830
+ descendants.push(child);
831
+ queue.push(...agents.filter((agent) => agent.parentAgentId === child.id));
832
+ }
833
+ return descendants;
834
+ }
835
+ function formatAgentStatusBreakdown(agents) {
836
+ if (agents.length === 0)
837
+ return "";
838
+ const working = agents.filter((agent) => agent.status === "working").length;
839
+ const idle = agents.length - working;
840
+ const ghost = agents.filter((agent) => agent.health === "ghost").length;
841
+ const stale = agents.filter((agent) => agent.health === "stale").length;
842
+ const parts = [
843
+ `${working} working`,
844
+ `${idle} idle`,
845
+ ghost > 0 ? `${ghost} ghost` : null,
846
+ stale > 0 ? `${stale} stale` : null,
847
+ ].filter((item) => Boolean(item));
848
+ return ` (${parts.join(", ")})`;
849
+ }
558
850
  function formatCompactAgentList(agents, hint) {
559
851
  const hintParts = [
560
852
  hint.repo ? `repo=${hint.repo}` : null,
@@ -565,7 +857,56 @@ function formatCompactAgentList(agents, hint) {
565
857
  : null,
566
858
  ].filter((item) => Boolean(item));
567
859
  const hintSuffix = hintParts.length > 0 ? `; hints ${hintParts.join(" · ")}` : "";
568
- return `Pinet agents: ${agents.length} visible${hintSuffix}.`;
860
+ const scopeSuffix = hint.scope && hint.scope !== "visible" ? `; scope=${hint.scope}` : "";
861
+ const breakdown = formatAgentStatusBreakdown(agents);
862
+ return `Pinet agents: ${agents.length} visible${breakdown}${hintSuffix}${scopeSuffix}.`;
863
+ }
864
+ const PINET_AGENTS_EXPANDED_MAX_ROWS = 30;
865
+ // Compact, aligned per-agent rows for the expanded TUI card. Avoids JSON dumps;
866
+ // shows name/id/status-health/repo-branch/lane-or-role/pending-inbox + flags.
867
+ function formatPinetAgentsExpanded(agents) {
868
+ if (agents.length === 0)
869
+ return "(no agents connected)";
870
+ const shown = agents.slice(0, PINET_AGENTS_EXPANDED_MAX_ROWS);
871
+ const rows = shown.map((agent) => {
872
+ const name = `${agent.emoji} ${agent.name}`;
873
+ const id = agent.id.length > 8 ? agent.id.slice(0, 8) : agent.id;
874
+ const healthSuffix = agent.health && agent.health !== "healthy" ? `/${agent.health}` : "";
875
+ const stuckSuffix = agent.stuck ? " stuck" : "";
876
+ const state = `${agent.status}${healthSuffix}${stuckSuffix}`;
877
+ const repo = getAgentRepo(agent);
878
+ const branch = getAgentBranch(agent);
879
+ const where = repo ? `${repo}${branch ? `/${branch}` : ""}` : (branch ?? "—");
880
+ const laneId = typeof agent.metadata?.laneId === "string" ? agent.metadata.laneId : undefined;
881
+ const role = getAgentRole(agent);
882
+ const laneOrRole = laneId ? `lane:${laneId}` : (role ?? "");
883
+ const inbox = agent.pendingInboxCount != null && agent.pendingInboxCount > 0
884
+ ? `inbox:${agent.pendingInboxCount}`
885
+ : "";
886
+ return { name, id, state, where, laneOrRole, inbox };
887
+ });
888
+ const clip = (value, width) => value.length <= width ? value : `${value.slice(0, Math.max(0, width - 1))}…`;
889
+ const pad = (value, width) => value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
890
+ const colWidth = (pick, cap) => Math.min(cap, rows.reduce((max, row) => Math.max(max, pick(row).length), 0));
891
+ const nameW = colWidth((row) => row.name, 28);
892
+ const idW = colWidth((row) => row.id, 8);
893
+ const stateW = colWidth((row) => row.state, 20);
894
+ const whereW = colWidth((row) => row.where, 30);
895
+ const laneW = colWidth((row) => row.laneOrRole, 18);
896
+ const lines = rows.map((row) => [
897
+ pad(clip(row.name, nameW), nameW),
898
+ pad(row.id, idW),
899
+ pad(clip(row.state, stateW), stateW),
900
+ pad(clip(row.where, whereW), whereW),
901
+ pad(clip(row.laneOrRole, laneW), laneW),
902
+ row.inbox,
903
+ ]
904
+ .join(" ")
905
+ .trimEnd());
906
+ if (agents.length > shown.length) {
907
+ lines.push(`… +${agents.length - shown.length} more`);
908
+ }
909
+ return lines.join("\n");
569
910
  }
570
911
  function formatPinetLaneSummary(lane) {
571
912
  const refs = [
@@ -639,6 +980,46 @@ function getMaybeMetadata(params) {
639
980
  return null;
640
981
  return isRecord(value) ? value : undefined;
641
982
  }
983
+ function buildCompactPortLeaseDetails(leases) {
984
+ const shownLeases = leases.slice(0, PINET_COMPACT_LIST_LIMIT);
985
+ return {
986
+ leaseCount: leases.length,
987
+ total: leases.length,
988
+ shown: shownLeases.length,
989
+ truncated: Math.max(0, leases.length - shownLeases.length),
990
+ leases: shownLeases.map((lease) => ({
991
+ leaseId: lease.id,
992
+ host: lease.host,
993
+ port: lease.port,
994
+ status: lease.status,
995
+ purpose: lease.purpose,
996
+ ownerAgentId: lease.ownerAgentId ?? null,
997
+ expiresAt: lease.expiresAt,
998
+ })),
999
+ };
1000
+ }
1001
+ function buildCompactLaneDetails(lanes) {
1002
+ const shownLanes = lanes.slice(0, PINET_COMPACT_LIST_LIMIT);
1003
+ return {
1004
+ laneCount: lanes.length,
1005
+ total: lanes.length,
1006
+ shown: shownLanes.length,
1007
+ truncated: Math.max(0, lanes.length - shownLanes.length),
1008
+ lanes: shownLanes.map((lane) => ({
1009
+ laneId: lane.laneId,
1010
+ state: lane.state,
1011
+ name: truncatePinetDetailText(lane.name),
1012
+ issueNumber: lane.issueNumber,
1013
+ prNumber: lane.prNumber,
1014
+ ownerAgentId: lane.ownerAgentId,
1015
+ implementationLeadAgentId: lane.implementationLeadAgentId,
1016
+ pmMode: lane.pmMode,
1017
+ participantCount: lane.participants.length,
1018
+ summary: truncatePinetDetailText(lane.summary),
1019
+ lastActivityAt: lane.lastActivityAt,
1020
+ })),
1021
+ };
1022
+ }
642
1023
  function formatPortLeaseSummary(lease) {
643
1024
  return `${lease.host}:${lease.port} ${lease.status} lease=${lease.id} purpose=${lease.purpose} owner=${lease.ownerAgentId ?? "none"} expires=${lease.expiresAt}`;
644
1025
  }
@@ -753,7 +1134,7 @@ function runPinetPortsAction(params, deps, toolName, output) {
753
1134
  return {
754
1135
  content: [{ type: "text", text: `Pinet port leases expired: ${leases.length}.` }],
755
1136
  details: { leases },
756
- compactDetails: { leaseCount: leases.length, leases: leases.slice(0, 10) },
1137
+ compactDetails: buildCompactPortLeaseDetails(leases),
757
1138
  fullDetails: { leases },
758
1139
  };
759
1140
  }
@@ -773,8 +1154,12 @@ function runPinetPortsAction(params, deps, toolName, output) {
773
1154
  return {
774
1155
  content: [{ type: "text", text: formatPortLeases(leases, output.full) }],
775
1156
  details: { leases },
776
- compactDetails: { leaseCount: leases.length, leases: leases.slice(0, 10) },
1157
+ compactDetails: buildCompactPortLeaseDetails(leases),
777
1158
  fullDetails: { leases },
1159
+ // Compact text is a count; show the per-lease rows on expand.
1160
+ ...(output.full || leases.length === 0
1161
+ ? {}
1162
+ : { expandedText: formatPortLeases(leases, true) }),
778
1163
  };
779
1164
  }
780
1165
  throw new Error("ports op must be acquire, renew, release, status, list, or expire");
@@ -803,8 +1188,12 @@ function runPinetLanesAction(params, deps, toolName, output) {
803
1188
  return {
804
1189
  content: [{ type: "text", text: formatPinetLanes(lanes, output.full) }],
805
1190
  details: { lanes },
806
- compactDetails: { laneCount: lanes.length, lanes: lanes.slice(0, 10) },
1191
+ compactDetails: buildCompactLaneDetails(lanes),
807
1192
  fullDetails: { lanes },
1193
+ // Compact text is a count; show the per-lane rows on expand.
1194
+ ...(output.full || lanes.length === 0
1195
+ ? {}
1196
+ : { expandedText: formatPinetLanes(lanes, true) }),
808
1197
  };
809
1198
  }
810
1199
  if (op === "upsert") {
@@ -893,10 +1282,20 @@ function runPinetLanesAction(params, deps, toolName, output) {
893
1282
  }
894
1283
  function runPinetAgentsAction(params, deps, toolName, output) {
895
1284
  return (async () => {
1285
+ const scope = params.scope === "children" || params.scope === "subtree" || params.scope === "all"
1286
+ ? params.scope
1287
+ : "visible";
1288
+ const requestedParentAgentId = typeof params.parent_agent === "string" ? params.parent_agent : undefined;
1289
+ const subtreeSelfAgentId = deps.getSubtreeSelfAgentId?.() ?? undefined;
1290
+ const parentAgentId = requestedParentAgentId ??
1291
+ (deps.brokerRole() === "follower" && (scope === "children" || scope === "subtree")
1292
+ ? subtreeSelfAgentId
1293
+ : undefined);
896
1294
  const hint = {
897
1295
  repo: typeof params.repo === "string" ? params.repo : undefined,
898
1296
  branch: typeof params.branch === "string" ? params.branch : undefined,
899
1297
  role: typeof params.role === "string" ? params.role : undefined,
1298
+ ...(scope !== "visible" ? { scope } : {}),
900
1299
  requiredTools: typeof params.required_tools === "string"
901
1300
  ? params.required_tools
902
1301
  .split(",")
@@ -905,7 +1304,7 @@ function runPinetAgentsAction(params, deps, toolName, output) {
905
1304
  : undefined,
906
1305
  task: typeof params.task === "string" ? params.task : undefined,
907
1306
  };
908
- deps.requireToolPolicy(toolName, undefined, `repo=${hint.repo ?? ""} | branch=${hint.branch ?? ""} | role=${hint.role ?? ""} | required_tools=${params.required_tools ?? ""} | task=${hint.task ?? ""} | format=${output.format} | full=${output.full}`);
1307
+ deps.requireToolPolicy(toolName, undefined, `repo=${hint.repo ?? ""} | branch=${hint.branch ?? ""} | role=${hint.role ?? ""} | scope=${scope} | parent_agent=${parentAgentId ?? ""} | required_tools=${params.required_tools ?? ""} | task=${hint.task ?? ""} | format=${output.format} | full=${output.full}`);
909
1308
  if (!deps.pinetEnabled()) {
910
1309
  throw new Error("Pinet is not running. Use /pinet start or /pinet follow first.");
911
1310
  }
@@ -923,12 +1322,23 @@ function runPinetAgentsAction(params, deps, toolName, output) {
923
1322
  rawAgents = deps.listBrokerAgents();
924
1323
  }
925
1324
  else if (deps.brokerRole() === "follower") {
926
- rawAgents = await deps.listFollowerAgents(includeGhosts);
1325
+ const wantsLocalSubtree = scope === "children" || scope === "subtree";
1326
+ const subtreeAgents = wantsLocalSubtree ? deps.listSubtreeAgents?.(includeGhosts) : null;
1327
+ if (wantsLocalSubtree && subtreeAgents) {
1328
+ rawAgents = subtreeAgents;
1329
+ }
1330
+ else if (wantsLocalSubtree && deps.listSubtreeAgents && !subtreeAgents) {
1331
+ throw new Error("No active worker-owned subtree broker. Run /pinet subtree start first.");
1332
+ }
1333
+ else {
1334
+ rawAgents = await deps.listFollowerAgents(includeGhosts);
1335
+ }
927
1336
  }
928
1337
  else {
929
1338
  throw new Error("Pinet is in an unexpected state.");
930
1339
  }
931
- const visibleAgents = filterAgentsForMeshVisibility(rawAgents, {
1340
+ const scopedRawAgents = filterAgentsForHierarchyScope(rawAgents, scope, parentAgentId);
1341
+ const visibleAgents = filterAgentsForMeshVisibility(scopedRawAgents, {
932
1342
  now: nowMs,
933
1343
  includeGhosts,
934
1344
  recentDisconnectWindowMs: recentGhostWindowMs,
@@ -941,6 +1351,7 @@ function runPinetAgentsAction(params, deps, toolName, output) {
941
1351
  details: { agents, hint },
942
1352
  compactDetails: buildCompactAgentDetails(agents, hint),
943
1353
  fullDetails: { agents, hint },
1354
+ expandedText: formatPinetAgentsExpanded(agents),
944
1355
  };
945
1356
  })();
946
1357
  }
@@ -998,6 +1409,18 @@ export function registerPinetTools(pi, deps) {
998
1409
  }),
999
1410
  execute: (_id, params, _output) => runPinetSnoozeAction(params, deps, "pinet:snooze"),
1000
1411
  });
1412
+ registerAction({
1413
+ name: "spawn",
1414
+ description: "Worker-only: launch a tmux-backed child worker into this worker's active subtree broker and deliver the task over Pinet.",
1415
+ parameters: Type.Object({
1416
+ task: Type.String({ description: "Scoped task prompt for the child worker" }),
1417
+ repo: Type.String({ description: "Repo/workspace scope for the child worker" }),
1418
+ role: Type.Optional(Type.String({ description: "Child subtree role, e.g. reviewer" })),
1419
+ lane_id: Type.Optional(Type.String({ description: "Optional durable Pinet lane id" })),
1420
+ ...PINET_OUTPUT_OPTION_PARAMETERS,
1421
+ }),
1422
+ execute: (_id, params, output) => runPinetSpawnAction(params, deps, "pinet:spawn", output),
1423
+ });
1001
1424
  registerAction({
1002
1425
  name: "reload",
1003
1426
  description: "Ask another connected Pinet agent to reload itself.",
@@ -1039,6 +1462,10 @@ export function registerPinetTools(pi, deps) {
1039
1462
  include_ghosts: Type.Optional(Type.Boolean({
1040
1463
  description: "Include recently disconnected/resumable agents. Defaults false so graceful exits do not look like actionable ghosts.",
1041
1464
  })),
1465
+ scope: Type.Optional(Type.String({
1466
+ description: "Hierarchy filter: visible (default), children, subtree, or all. children/subtree require parent_agent for explicit inspection.",
1467
+ })),
1468
+ parent_agent: Type.Optional(Type.String({ description: "Parent agent id for scope=children or scope=subtree" })),
1042
1469
  ...PINET_OUTPUT_OPTION_PARAMETERS,
1043
1470
  }),
1044
1471
  execute: (_id, params, output) => runPinetAgentsAction(params, deps, "pinet:agents", output),
@@ -1114,15 +1541,44 @@ export function registerPinetTools(pi, deps) {
1114
1541
  name: "pinet",
1115
1542
  label: "Pinet Dispatcher",
1116
1543
  description: "Dispatch Pinet operations by action with compact help and schema discovery.",
1117
- promptSnippet: 'Use this compact dispatcher for Pinet actions: send, read, free, snooze, schedule, agents, lanes, ports, reload, exit, and help. Use /pinet start, /pinet follow, and /pinet unfollow for TUI lifecycle changes. Defaults to terse CLI text; pass args.format="json" or args.full=true for explicit detail, but avoid JSON/full unless needed because it can fill context quickly.',
1544
+ promptSnippet: 'Use this compact dispatcher for Pinet actions: send, read, free, snooze, schedule, agents, lanes, ports, reload, exit, spawn, and help. Use /pinet start, /pinet follow, /pinet unfollow, and /pinet subtree start for TUI lifecycle changes. Defaults to terse CLI text; pass args.format="json" for the compact envelope or args.full=true for verbose/debug detail.',
1118
1545
  parameters: Type.Object({
1119
1546
  action: Type.String({
1120
- description: "Action name: help, send, read, free, snooze, schedule, agents, lanes, ports, reload, or exit.",
1547
+ description: "Action name: help, send, read, free, snooze, schedule, agents, lanes, ports, reload, or exit. Also supports spawn for launching worker-owned subtree children.",
1121
1548
  }),
1122
1549
  args: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
1123
- description: 'Action arguments. Add format="cli"|"json" (or f/"-f") and full=true (or "--full": true) for explicit presentation control. Default cli keeps data.details compact; format="json" or full=true exposes full structured details and can fill context quickly, so use it only when needed.',
1550
+ description: 'Action arguments. Add format="cli"|"json" (or f/"-f") for presentation, and full=true (or "--full": true) only for verbose/debug details. Default cli and non-full json keep data.details compact.',
1124
1551
  })),
1125
1552
  }),
1553
+ renderCall(args, theme) {
1554
+ const action = typeof args.action === "string" && args.action.trim() ? args.action.trim() : "?";
1555
+ let suffix = "";
1556
+ if (isRecord(args.args)) {
1557
+ const topic = typeof args.args.topic === "string" ? args.args.topic.trim() : "";
1558
+ const op = typeof args.args.op === "string" ? args.args.op.trim() : "";
1559
+ if (topic)
1560
+ suffix = ` ${theme.fg("dim", `topic=${topic}`)}`;
1561
+ else if (op)
1562
+ suffix = ` ${theme.fg("dim", `op=${op}`)}`;
1563
+ }
1564
+ return new Text(`${theme.fg("toolTitle", theme.bold("pinet"))} ${theme.fg("muted", action)}${suffix}`, 0, 0);
1565
+ },
1566
+ renderResult(result, { expanded, isPartial }, theme) {
1567
+ if (isPartial) {
1568
+ return new Text(theme.fg("warning", "Pinet running…"), 0, 0);
1569
+ }
1570
+ const display = formatPinetDispatcherResultForDisplay(result, expanded);
1571
+ const color = display.status === "failed"
1572
+ ? "error"
1573
+ : display.status === "succeeded"
1574
+ ? "success"
1575
+ : "muted";
1576
+ const icon = display.status === "failed" ? "✗" : display.status === "succeeded" ? "✓" : "•";
1577
+ const lines = display.text.split("\n");
1578
+ const first = lines[0] ?? "Pinet result.";
1579
+ const rest = lines.slice(1).map((line) => theme.fg("dim", line));
1580
+ return new Text([`${theme.fg(color, icon)} ${theme.fg(color, first)}`, ...rest].join("\n"), 0, 0);
1581
+ },
1126
1582
  async execute(toolCallId, params) {
1127
1583
  let normalizedAction;
1128
1584
  try {
@@ -1154,7 +1610,7 @@ export function registerPinetTools(pi, deps) {
1154
1610
  },
1155
1611
  ]), getTolerantPinetOutputOptions(args));
1156
1612
  }
1157
- return wrapDispatcherEnvelope(buildPinetDispatcherHelpEnvelope(args, actionDefinitions), output);
1613
+ return wrapDispatcherEnvelope(buildPinetDispatcherHelpEnvelope(args, actionDefinitions, output), output);
1158
1614
  }
1159
1615
  const definition = actionDefinitions.get(normalizedAction);
1160
1616
  if (!definition) {
@@ -1193,11 +1649,15 @@ export function registerPinetTools(pi, deps) {
1193
1649
  }
1194
1650
  try {
1195
1651
  const result = await definition.execute(toolCallId, params.args, output);
1652
+ const expandedText = typeof result.expandedText === "string" && result.expandedText.trim().length > 0
1653
+ ? result.expandedText
1654
+ : undefined;
1655
+ const displayText = result.content[0]?.text ?? "";
1196
1656
  return wrapDispatcherEnvelope(buildPinetDispatcherEnvelope("succeeded", {
1197
1657
  action: definition.name,
1198
- text: result.content[0]?.text ?? "",
1658
+ text: output.format === "json" ? (result.machineText ?? displayText) : displayText,
1199
1659
  details: selectPinetResultDetails(result, output),
1200
- }), output);
1660
+ }), output, expandedText, displayText);
1201
1661
  }
1202
1662
  catch (error) {
1203
1663
  if (error instanceof Error && error.name === "AbortError") {