@pinet/slack-bridge 0.1.2 → 0.2.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.
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 +506 -38
  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";
@@ -10,8 +11,16 @@ import { HEARTBEAT_INTERVAL_MS } from "./broker/client.js";
10
11
  const PINET_DISPATCHER_EXAMPLES = {
11
12
  send: [{ action: "send", args: { to: "@worker", message: "Please review PR #123" } }],
12
13
  read: [
14
+ // Routine inbox drain: defaults are `unread_only: true` + `mark_read: true`,
15
+ // so this returns and consumes only this agent's unread rows. The compact
16
+ // output already includes capped per-message previews; no `full=true` needed.
17
+ { action: "read", args: { limit: 20 } },
18
+ // Same defaults, scoped to one thread.
13
19
  { action: "read", args: { thread_id: "a2a:<broker>:<worker>", limit: 20 } },
14
- { action: "read", args: { unread_only: false, mark_read: false, full: true } },
20
+ // Non-destructive latest-history peek (does NOT mark anything read or
21
+ // change unread state). Use only when you need to revisit recently-read
22
+ // context, not as a routine inbox read.
23
+ { action: "read", args: { unread_only: false, mark_read: false, limit: 5 } },
15
24
  ],
16
25
  free: [{ action: "free", args: { note: "Wrapped up <issue>" } }],
17
26
  snooze: [
@@ -19,14 +28,20 @@ const PINET_DISPATCHER_EXAMPLES = {
19
28
  { action: "snooze", args: { op: "clear" } },
20
29
  ],
21
30
  schedule: [{ action: "schedule", args: { delay: "30m", message: "Check queue state" } }],
22
- agents: [{ action: "agents", args: { repo: "<repo>", role: "worker", full: true } }],
31
+ agents: [{ action: "agents", args: { repo: "<repo>", role: "worker" } }],
32
+ spawn: [
33
+ {
34
+ action: "spawn",
35
+ args: { task: "Review PR #123", repo: "extensions", role: "reviewer", lane_id: "issue-123" },
36
+ },
37
+ ],
23
38
  ports: [
24
39
  { action: "ports", args: { op: "acquire", purpose: "preview", ttl_ms: 600000 } },
25
40
  { action: "ports", args: { op: "renew", lease_id: "<lease-id>", ttl_ms: 600000 } },
26
41
  { action: "ports", args: { op: "list", include_inactive: true } },
27
42
  ],
28
43
  lanes: [
29
- { action: "lanes", args: { op: "list", full: true } },
44
+ { action: "lanes", args: { op: "list" } },
30
45
  {
31
46
  action: "lanes",
32
47
  args: {
@@ -44,9 +59,10 @@ const PINET_DISPATCHER_EXAMPLES = {
44
59
  const PINET_OUTPUT_OPTION_PARAMETERS = {
45
60
  format: Type.Optional(Type.String({ description: 'Response presentation format: "cli" (default) or "json".' })),
46
61
  full: Type.Optional(Type.Boolean({
47
- description: "Include full verbose text/details; default cli output keeps data.details compact.",
62
+ description: "Include full verbose text/details. JSON format alone keeps data.details compact.",
48
63
  })),
49
64
  };
65
+ const PINET_COMPACT_LIST_LIMIT = 10;
50
66
  function getRecordString(record, key) {
51
67
  const value = record?.[key];
52
68
  return typeof value === "string" && value.trim() ? value : undefined;
@@ -75,6 +91,7 @@ function normalizeDispatcherAction(value) {
75
91
  "agents",
76
92
  "lanes",
77
93
  "ports",
94
+ "spawn",
78
95
  "reload",
79
96
  "exit",
80
97
  ];
@@ -119,7 +136,8 @@ function classifyPinetError(message) {
119
136
  message.includes("lease_id") ||
120
137
  message.includes("ttl_ms") ||
121
138
  message.includes("purpose") ||
122
- message.includes("port")) {
139
+ message.includes("port") ||
140
+ message.includes("spawn")) {
123
141
  return {
124
142
  class: "input",
125
143
  message,
@@ -159,7 +177,7 @@ function formatPinetHelpCliText(data) {
159
177
  const description = typeof data.description === "string" && data.description.trim()
160
178
  ? ` — ${data.description.trim()}`
161
179
  : "";
162
- return `Pinet ${data.action}${description}. Use args.format="json" or args.full=true for schema/details; JSON/full output can fill context quickly.`;
180
+ return `Pinet ${data.action}${description}. Use args.format="json" for the compact envelope, or args.full=true for verbose schema/debug details.`;
163
181
  }
164
182
  return null;
165
183
  }
@@ -172,14 +190,14 @@ function getPinetEnvelopeCliText(envelope) {
172
190
  if (helpText)
173
191
  return helpText;
174
192
  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.`;
193
+ return `Pinet${action} succeeded. Use args.format="json" for the compact envelope, or args.full=true for verbose details.`;
176
194
  }
177
195
  const errors = envelope.errors.map((error) => error.message).filter(Boolean);
178
196
  const hints = Array.from(new Set(envelope.errors.map((error) => error.hint).filter((hint) => hint.length > 0)));
179
197
  if (errors.length > 0) {
180
198
  return `Pinet ${envelope.status}: ${errors.join("; ")}${hints.length > 0 ? ` Hint: ${hints.join(" ")}` : ""}`;
181
199
  }
182
- return `Pinet ${envelope.status}. Use args.format="json" or args.full=true for details; JSON/full output can fill context quickly.`;
200
+ return `Pinet ${envelope.status}. Use args.format="json" for the compact envelope, or args.full=true for verbose details.`;
183
201
  }
184
202
  function getTolerantPinetOutputOptions(value) {
185
203
  if (!isRecord(value))
@@ -212,29 +230,165 @@ function shouldRenderStructuredPinetEnvelope(envelope, output) {
212
230
  const text = envelope.data.text;
213
231
  return typeof text !== "string" || text.length === 0;
214
232
  }
215
- function wrapDispatcherEnvelope(envelope, output = { format: "cli", full: false }) {
233
+ function wrapDispatcherEnvelope(envelope, output = { format: "cli", full: false }, expandedText, displayText) {
216
234
  return {
217
235
  content: [
218
236
  {
219
237
  type: "text",
220
238
  text: shouldRenderStructuredPinetEnvelope(envelope, output)
221
- ? JSON.stringify(envelope, null, 2)
239
+ ? JSON.stringify(envelope, null, output.full ? 2 : 0)
222
240
  : getPinetEnvelopeCliText(envelope),
223
241
  },
224
242
  ],
225
243
  details: envelope,
244
+ ...(expandedText ? { expandedText } : {}),
245
+ ...(displayText ? { displayText } : {}),
246
+ };
247
+ }
248
+ function isPinetDispatcherEnvelope(value) {
249
+ if (!isRecord(value))
250
+ return false;
251
+ if (value.status !== "succeeded" && value.status !== "failed")
252
+ return false;
253
+ return Array.isArray(value.errors) && Array.isArray(value.warnings);
254
+ }
255
+ function getFirstTextContent(result) {
256
+ return (result.content?.find((block) => block.type === "text" && typeof block.text === "string")
257
+ ?.text ?? "");
258
+ }
259
+ function parsePinetDispatcherEnvelopeFromText(text) {
260
+ if (!text.trim().startsWith("{"))
261
+ return null;
262
+ try {
263
+ const parsed = JSON.parse(text);
264
+ return isPinetDispatcherEnvelope(parsed) ? parsed : null;
265
+ }
266
+ catch {
267
+ return null;
268
+ }
269
+ }
270
+ function summarizeDetailValue(value) {
271
+ if (value === null)
272
+ return "null";
273
+ if (Array.isArray(value)) {
274
+ return `${value.length} item${value.length === 1 ? "" : "s"}`;
275
+ }
276
+ if (typeof value === "object") {
277
+ const entries = Object.entries(value);
278
+ if (entries.length === 0)
279
+ return "{}";
280
+ const inline = entries
281
+ .slice(0, 6)
282
+ .map(([key, nested]) => {
283
+ if (nested === null || ["string", "number", "boolean"].includes(typeof nested)) {
284
+ return `${key}=${String(nested)}`;
285
+ }
286
+ return `${key}=…`;
287
+ })
288
+ .join(", ");
289
+ return `{ ${inline}${entries.length > 6 ? ", …" : ""} }`;
290
+ }
291
+ return String(value);
292
+ }
293
+ // Renders `data.details` as compact key lines. Arrays and nested objects are
294
+ // summarized (count / inlined keys) so the expanded view never JSON-dumps a
295
+ // wall of structured data into the operator's TUI.
296
+ function formatPrimitiveDetails(details) {
297
+ return Object.entries(details).map(([key, value]) => `${key}: ${summarizeDetailValue(value)}`);
298
+ }
299
+ const PINET_SEND_PREVIEW_MAX = 80;
300
+ const PINET_SEND_BODY_MAX = 600;
301
+ // Single-line, whitespace-collapsed, length-capped preview of a sent message.
302
+ // Operator-facing only; kept short so model/CLI text stays compact.
303
+ function formatMessagePreview(message, maxLength = PINET_SEND_PREVIEW_MAX) {
304
+ const collapsed = message.replace(/\s+/g, " ").trim();
305
+ if (collapsed.length <= maxLength)
306
+ return collapsed;
307
+ return `${collapsed.slice(0, Math.max(0, maxLength - 1))}…`;
308
+ }
309
+ function countMessageLines(message) {
310
+ if (message.length === 0)
311
+ return 0;
312
+ return message.split(/\r?\n/).length;
313
+ }
314
+ // Capped multi-line body for the expanded TUI card. Preserves line breaks so the
315
+ // operator can see structure, but truncates very long bodies.
316
+ function capMessageBody(message, maxLength = PINET_SEND_BODY_MAX) {
317
+ if (message.length <= maxLength)
318
+ return message;
319
+ return `${message.slice(0, Math.max(0, maxLength - 1))}…`;
320
+ }
321
+ function formatSentMessageSuffix(message) {
322
+ const preview = formatMessagePreview(message);
323
+ if (!preview)
324
+ return "";
325
+ const lines = countMessageLines(message);
326
+ return ` · “${preview}”${lines > 1 ? ` (${lines} lines)` : ""}`;
327
+ }
328
+ function buildSentMessageExpandedText(fields, message) {
329
+ const lines = fields
330
+ .filter((entry) => entry[1] !== undefined)
331
+ .map(([label, value]) => `${label}: ${value}`);
332
+ const chars = message.length;
333
+ const lineCount = countMessageLines(message);
334
+ lines.push(`message (${lineCount} line${lineCount === 1 ? "" : "s"}, ${chars} chars):`);
335
+ lines.push(capMessageBody(message));
336
+ return lines.join("\n");
337
+ }
338
+ function formatPinetEnvelopeExpandedText(envelope, expandedText) {
339
+ const lines = [getPinetEnvelopeCliText(envelope)];
340
+ if (envelope.status === "failed") {
341
+ lines.push(`status: ${envelope.status}`);
342
+ for (const error of envelope.errors) {
343
+ lines.push(`error[${error.class}]: ${error.message}`);
344
+ if (error.hint)
345
+ lines.push(`hint: ${error.hint}`);
346
+ }
347
+ }
348
+ else if (expandedText && expandedText.trim().length > 0) {
349
+ lines.push(...expandedText.split("\n"));
350
+ }
351
+ else if (isRecord(envelope.data)) {
352
+ lines.push(`status: ${envelope.status}`);
353
+ const action = typeof envelope.data.action === "string" ? envelope.data.action : undefined;
354
+ if (action)
355
+ lines.push(`action: ${action}`);
356
+ const details = envelope.data.details;
357
+ if (isRecord(details)) {
358
+ lines.push("details:");
359
+ lines.push(...formatPrimitiveDetails(details).map((line) => ` ${line}`));
360
+ }
361
+ }
362
+ for (const warning of envelope.warnings) {
363
+ lines.push(`warning: ${warning}`);
364
+ }
365
+ return lines.join("\n");
366
+ }
367
+ export function formatPinetDispatcherResultForDisplay(result, expanded) {
368
+ const firstText = getFirstTextContent(result);
369
+ const envelope = isPinetDispatcherEnvelope(result.details)
370
+ ? result.details
371
+ : parsePinetDispatcherEnvelopeFromText(firstText);
372
+ if (!envelope) {
373
+ return { status: "unknown", text: firstText || "Pinet result." };
374
+ }
375
+ return {
376
+ status: envelope.status,
377
+ text: expanded
378
+ ? formatPinetEnvelopeExpandedText(envelope, result.expandedText)
379
+ : (result.displayText ?? getPinetEnvelopeCliText(envelope)),
226
380
  };
227
381
  }
228
382
  function selectPinetResultDetails(result, output) {
229
- if (output.format === "json" || output.full) {
230
- return result.details ?? null;
383
+ if (output.full) {
384
+ return result.fullDetails ?? result.details ?? null;
231
385
  }
232
386
  if (result.compactDetails !== undefined) {
233
387
  return result.compactDetails;
234
388
  }
235
389
  return result.details ?? null;
236
390
  }
237
- function buildPinetDispatcherHelpEnvelope(args, actions) {
391
+ function buildPinetDispatcherHelpEnvelope(args, actions, output) {
238
392
  const topic = typeof args.topic === "string" && args.topic.trim()
239
393
  ? args.topic.trim().toLowerCase()
240
394
  : undefined;
@@ -245,13 +399,19 @@ function buildPinetDispatcherHelpEnvelope(args, actions) {
245
399
  action: definition.name,
246
400
  description: definition.description,
247
401
  guardrail_tool: `pinet:${definition.name}`,
248
- args_schema: definition.parameters,
249
- examples: PINET_DISPATCHER_EXAMPLES[definition.name] ?? [],
402
+ ...(output.full
403
+ ? {
404
+ args_schema: definition.parameters,
405
+ examples: PINET_DISPATCHER_EXAMPLES[definition.name] ?? [],
406
+ }
407
+ : {}),
250
408
  }))
251
409
  .sort((a, b) => a.action.localeCompare(b.action));
252
410
  return buildPinetDispatcherEnvelope("succeeded", {
253
411
  actions: catalog,
254
- note: "Use args.topic to inspect a single action schema.",
412
+ note: output.full
413
+ ? "Use args.topic to inspect a single action schema."
414
+ : "Use args.topic to inspect a single action schema; add args.full=true for the full catalog schemas/examples.",
255
415
  });
256
416
  }
257
417
  const definition = actions.get(topic);
@@ -306,17 +466,19 @@ function runPinetSendAction(params, deps, toolName, output) {
306
466
  if (transferThreadId && isBroadcastChannelTarget(to)) {
307
467
  throw new Error("transfer_thread_id requires a direct agent target, not a broadcast channel.");
308
468
  }
469
+ const previewSuffix = formatSentMessageSuffix(message);
309
470
  if (deps.brokerRole() === "broker" && isBroadcastChannelTarget(to)) {
310
471
  const result = deps.sendPinetBroadcastMessage(to, message);
311
472
  const preview = result.recipients.slice(0, 5).join(", ");
312
473
  const suffix = result.recipients.length > 5 ? ", …" : "";
474
+ const recipientList = result.recipients.join(", ");
313
475
  return {
314
476
  content: [
315
477
  {
316
478
  type: "text",
317
479
  text: output.full
318
480
  ? `Broadcast sent to ${result.channel} (${result.recipients.length} agents: ${preview}${suffix}).`
319
- : `Pinet broadcast sent to ${result.channel} (${result.recipients.length} recipients).`,
481
+ : `Pinet broadcast sent to ${result.channel} (${result.recipients.length} recipients)${previewSuffix}.`,
320
482
  },
321
483
  ],
322
484
  details: {
@@ -324,6 +486,26 @@ function runPinetSendAction(params, deps, toolName, output) {
324
486
  messageIds: result.messageIds,
325
487
  recipients: result.recipients,
326
488
  },
489
+ compactDetails: {
490
+ channel: result.channel,
491
+ messageCount: result.messageIds.length,
492
+ recipientCount: result.recipients.length,
493
+ recipients: result.recipients.slice(0, PINET_COMPACT_LIST_LIMIT),
494
+ recipientsTruncated: Math.max(0, result.recipients.length - PINET_COMPACT_LIST_LIMIT),
495
+ },
496
+ fullDetails: {
497
+ channel: result.channel,
498
+ messageIds: result.messageIds,
499
+ recipients: result.recipients,
500
+ },
501
+ machineText: output.full
502
+ ? `Broadcast sent to ${result.channel} (${result.recipients.length} agents: ${preview}${suffix}).`
503
+ : `Pinet broadcast sent to ${result.channel} (${result.recipients.length} recipients).`,
504
+ expandedText: buildSentMessageExpandedText([
505
+ ["to", result.channel],
506
+ ["recipients", `${result.recipients.length} (${recipientList})`],
507
+ ["message ids", result.messageIds.join(", ")],
508
+ ], message),
327
509
  };
328
510
  }
329
511
  const result = transferThreadId
@@ -331,13 +513,16 @@ function runPinetSendAction(params, deps, toolName, output) {
331
513
  threadOwnershipTransfer: { mode: "transfer", threadId: transferThreadId },
332
514
  })
333
515
  : await deps.sendPinetAgentMessage(to, message);
516
+ const transferSuffix = result.transferredThreadId
517
+ ? ` (${result.transferredThreadChannel})`
518
+ : "";
334
519
  return {
335
520
  content: [
336
521
  {
337
522
  type: "text",
338
523
  text: output.full
339
524
  ? `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}` : ""}.`,
525
+ : `Pinet message sent to ${result.target}${result.transferredThreadId ? `; transferred Slack thread ${result.transferredThreadId}` : ""}${previewSuffix}.`,
341
526
  },
342
527
  ],
343
528
  details: {
@@ -348,6 +533,16 @@ function runPinetSendAction(params, deps, toolName, output) {
348
533
  ? { transferredThreadChannel: result.transferredThreadChannel }
349
534
  : {}),
350
535
  },
536
+ machineText: output.full
537
+ ? `Message sent to ${result.target} (id: ${result.messageId})${result.transferredThreadId ? ` and transferred Slack thread ${result.transferredThreadId}${result.transferredThreadChannel ? ` (${result.transferredThreadChannel})` : ""}` : ""}.`
538
+ : `Pinet message sent to ${result.target}${result.transferredThreadId ? `; transferred Slack thread ${result.transferredThreadId}` : ""}.`,
539
+ expandedText: buildSentMessageExpandedText([
540
+ ["to", result.target],
541
+ ["message id", result.messageId],
542
+ result.transferredThreadId
543
+ ? ["transferred thread", `${result.transferredThreadId}${transferSuffix}`]
544
+ : ["transferred thread", undefined],
545
+ ], message),
351
546
  };
352
547
  })();
353
548
  }
@@ -367,11 +562,12 @@ function runPinetReadAction(params, deps, toolName, output) {
367
562
  throw new Error("thread_id must be a non-empty string when provided.");
368
563
  }
369
564
  const result = await deps.readPinetInbox(options);
565
+ const shouldRenderFull = output.full;
370
566
  return {
371
567
  content: [
372
568
  {
373
569
  type: "text",
374
- text: output.full
570
+ text: shouldRenderFull
375
571
  ? formatPinetReadResultFull(result, options)
376
572
  : formatPinetReadResultCompact(result, options),
377
573
  },
@@ -379,6 +575,10 @@ function runPinetReadAction(params, deps, toolName, output) {
379
575
  details: result,
380
576
  compactDetails: buildCompactPinetReadDetails(result),
381
577
  fullDetails: result,
578
+ // When the model/CLI text is the compact one-liner, give the operator the
579
+ // full per-message rows on expand. In full/thread mode `text` already
580
+ // carries the rich view, so no separate expanded body is needed.
581
+ ...(shouldRenderFull ? {} : { expandedText: formatPinetReadResultFull(result, options) }),
382
582
  };
383
583
  })();
384
584
  }
@@ -455,6 +655,53 @@ function runPinetSnoozeAction(params, deps, toolName) {
455
655
  return { content: [{ type: "text", text: formatRalphSnoozeResult(status) }], details: status };
456
656
  })();
457
657
  }
658
+ function runPinetSpawnAction(params, deps, toolName, output) {
659
+ return (async () => {
660
+ const task = getMaybeString(params, "task");
661
+ const repo = getMaybeString(params, "repo");
662
+ const role = getMaybeString(params, "role") ?? "subworker";
663
+ const laneId = getMaybeString(params, "lane_id");
664
+ deps.requireToolPolicy(toolName, undefined, `task=${task ?? ""} | repo=${repo ?? ""} | role=${role} | lane_id=${laneId ?? ""}`);
665
+ if (!deps.pinetEnabled()) {
666
+ throw new Error("Pinet is not running. Use /pinet start or /pinet follow first.");
667
+ }
668
+ if (deps.brokerRole() !== "follower") {
669
+ throw new Error("spawn is worker-only; the broker should launch top-level followers, not own subtrees.");
670
+ }
671
+ if (!task)
672
+ throw new Error("spawn requires task");
673
+ if (!repo)
674
+ throw new Error("spawn requires repo");
675
+ if (!deps.spawnSubtreeWorker) {
676
+ throw new Error("subtree worker launcher is unavailable in this runtime. Start a worker subtree with /pinet subtree start and retry.");
677
+ }
678
+ const result = await deps.spawnSubtreeWorker({
679
+ task,
680
+ repo,
681
+ role,
682
+ ...(laneId ? { laneId } : {}),
683
+ });
684
+ return {
685
+ content: [
686
+ {
687
+ type: "text",
688
+ text: output.full
689
+ ? `Pinet subtree worker started: ${result.agentName} (${result.agentId}) in tmux session ${result.sessionName}. Task message ${result.messageId} delivered. Monitor: ${result.monitorCommand}`
690
+ : `Pinet subtree worker started: ${result.agentName} (${result.agentId}). Task message ${result.messageId} delivered.`,
691
+ },
692
+ ],
693
+ details: result,
694
+ compactDetails: {
695
+ status: result.status,
696
+ agentId: result.agentId,
697
+ agentName: result.agentName,
698
+ messageId: result.messageId,
699
+ threadId: result.threadId,
700
+ },
701
+ fullDetails: result,
702
+ };
703
+ })();
704
+ }
458
705
  function runPinetRemoteControlAction(params, deps, toolName, command) {
459
706
  return (async () => {
460
707
  const target = typeof params.target === "string" ? params.target.trim() : "";
@@ -495,7 +742,7 @@ function runPinetScheduleAction(params, deps, toolName, output) {
495
742
  type: "text",
496
743
  text: output.full
497
744
  ? `Wake-up scheduled for ${wakeup.fireAt} (id: ${wakeup.id}).`
498
- : `Pinet wake-up scheduled for ${wakeup.fireAt}.`,
745
+ : `Pinet wake-up scheduled for ${wakeup.fireAt} (id ${wakeup.id}).`,
499
746
  },
500
747
  ],
501
748
  details: wakeup,
@@ -509,7 +756,7 @@ function runPinetScheduleAction(params, deps, toolName, output) {
509
756
  type: "text",
510
757
  text: output.full
511
758
  ? `Wake-up scheduled for ${wakeup.fireAt} (id: ${wakeup.id}).`
512
- : `Pinet wake-up scheduled for ${wakeup.fireAt}.`,
759
+ : `Pinet wake-up scheduled for ${wakeup.fireAt} (id ${wakeup.id}).`,
513
760
  },
514
761
  ],
515
762
  details: wakeup,
@@ -527,11 +774,25 @@ function getAgentBranch(agent) {
527
774
  function getAgentRole(agent) {
528
775
  return getRecordString(agent.metadata, "role");
529
776
  }
777
+ function truncatePinetDetailText(value, maxLength = 160) {
778
+ if (!value)
779
+ return null;
780
+ const collapsed = value.replace(/\s+/g, " ").trim();
781
+ if (!collapsed)
782
+ return null;
783
+ if (collapsed.length <= maxLength)
784
+ return collapsed;
785
+ return `${collapsed.slice(0, Math.max(0, maxLength - 1))}…`;
786
+ }
530
787
  function buildCompactAgentDetails(agents, hint) {
788
+ const shownAgents = agents.slice(0, PINET_COMPACT_LIST_LIMIT);
531
789
  return {
532
790
  count: agents.length,
791
+ total: agents.length,
792
+ shown: shownAgents.length,
793
+ truncated: Math.max(0, agents.length - shownAgents.length),
533
794
  hint,
534
- agents: agents.map((agent) => {
795
+ agents: shownAgents.map((agent) => {
535
796
  const capabilityTags = agent.capabilityTags ?? [];
536
797
  return {
537
798
  id: agent.id,
@@ -543,8 +804,11 @@ function buildCompactAgentDetails(agents, hint) {
543
804
  branch: getAgentBranch(agent) ?? null,
544
805
  role: getAgentRole(agent) ?? null,
545
806
  brokerManaged: agent.metadata?.brokerManaged === true,
546
- launchSource: agent.metadata?.launchSource ?? null,
547
- tmuxSession: agent.metadata?.tmuxSession ?? null,
807
+ parentAgentId: agent.metadata?.parentAgentId ?? null,
808
+ treeDepth: agent.metadata?.treeDepth ?? 0,
809
+ supervisionState: agent.metadata?.supervisionState ?? "root",
810
+ subtreeRole: agent.metadata?.subtreeRole ?? null,
811
+ laneId: agent.metadata?.laneId ?? null,
548
812
  routingScore: agent.routingScore ?? null,
549
813
  ...(agent.pendingInboxCount != null && agent.pendingInboxCount > 0
550
814
  ? { pendingInboxCount: agent.pendingInboxCount }
@@ -555,6 +819,42 @@ function buildCompactAgentDetails(agents, hint) {
555
819
  }),
556
820
  };
557
821
  }
822
+ function filterAgentsForHierarchyScope(agents, scope, parentAgentId) {
823
+ if (scope === "all" || scope === "visible")
824
+ return agents;
825
+ if (!parentAgentId)
826
+ return [];
827
+ if (scope === "children") {
828
+ return agents.filter((agent) => agent.parentAgentId === parentAgentId);
829
+ }
830
+ const descendants = [];
831
+ const seen = new Set();
832
+ const queue = agents.filter((agent) => agent.parentAgentId === parentAgentId);
833
+ while (queue.length > 0) {
834
+ const child = queue.shift();
835
+ if (!child || seen.has(child.id))
836
+ continue;
837
+ seen.add(child.id);
838
+ descendants.push(child);
839
+ queue.push(...agents.filter((agent) => agent.parentAgentId === child.id));
840
+ }
841
+ return descendants;
842
+ }
843
+ function formatAgentStatusBreakdown(agents) {
844
+ if (agents.length === 0)
845
+ return "";
846
+ const working = agents.filter((agent) => agent.status === "working").length;
847
+ const idle = agents.length - working;
848
+ const ghost = agents.filter((agent) => agent.health === "ghost").length;
849
+ const stale = agents.filter((agent) => agent.health === "stale").length;
850
+ const parts = [
851
+ `${working} working`,
852
+ `${idle} idle`,
853
+ ghost > 0 ? `${ghost} ghost` : null,
854
+ stale > 0 ? `${stale} stale` : null,
855
+ ].filter((item) => Boolean(item));
856
+ return ` (${parts.join(", ")})`;
857
+ }
558
858
  function formatCompactAgentList(agents, hint) {
559
859
  const hintParts = [
560
860
  hint.repo ? `repo=${hint.repo}` : null,
@@ -565,7 +865,56 @@ function formatCompactAgentList(agents, hint) {
565
865
  : null,
566
866
  ].filter((item) => Boolean(item));
567
867
  const hintSuffix = hintParts.length > 0 ? `; hints ${hintParts.join(" · ")}` : "";
568
- return `Pinet agents: ${agents.length} visible${hintSuffix}.`;
868
+ const scopeSuffix = hint.scope && hint.scope !== "visible" ? `; scope=${hint.scope}` : "";
869
+ const breakdown = formatAgentStatusBreakdown(agents);
870
+ return `Pinet agents: ${agents.length} visible${breakdown}${hintSuffix}${scopeSuffix}.`;
871
+ }
872
+ const PINET_AGENTS_EXPANDED_MAX_ROWS = 30;
873
+ // Compact, aligned per-agent rows for the expanded TUI card. Avoids JSON dumps;
874
+ // shows name/id/status-health/repo-branch/lane-or-role/pending-inbox + flags.
875
+ function formatPinetAgentsExpanded(agents) {
876
+ if (agents.length === 0)
877
+ return "(no agents connected)";
878
+ const shown = agents.slice(0, PINET_AGENTS_EXPANDED_MAX_ROWS);
879
+ const rows = shown.map((agent) => {
880
+ const name = `${agent.emoji} ${agent.name}`;
881
+ const id = agent.id.length > 8 ? agent.id.slice(0, 8) : agent.id;
882
+ const healthSuffix = agent.health && agent.health !== "healthy" ? `/${agent.health}` : "";
883
+ const stuckSuffix = agent.stuck ? " stuck" : "";
884
+ const state = `${agent.status}${healthSuffix}${stuckSuffix}`;
885
+ const repo = getAgentRepo(agent);
886
+ const branch = getAgentBranch(agent);
887
+ const where = repo ? `${repo}${branch ? `/${branch}` : ""}` : (branch ?? "—");
888
+ const laneId = typeof agent.metadata?.laneId === "string" ? agent.metadata.laneId : undefined;
889
+ const role = getAgentRole(agent);
890
+ const laneOrRole = laneId ? `lane:${laneId}` : (role ?? "");
891
+ const inbox = agent.pendingInboxCount != null && agent.pendingInboxCount > 0
892
+ ? `inbox:${agent.pendingInboxCount}`
893
+ : "";
894
+ return { name, id, state, where, laneOrRole, inbox };
895
+ });
896
+ const clip = (value, width) => value.length <= width ? value : `${value.slice(0, Math.max(0, width - 1))}…`;
897
+ const pad = (value, width) => value.length >= width ? value : `${value}${" ".repeat(width - value.length)}`;
898
+ const colWidth = (pick, cap) => Math.min(cap, rows.reduce((max, row) => Math.max(max, pick(row).length), 0));
899
+ const nameW = colWidth((row) => row.name, 28);
900
+ const idW = colWidth((row) => row.id, 8);
901
+ const stateW = colWidth((row) => row.state, 20);
902
+ const whereW = colWidth((row) => row.where, 30);
903
+ const laneW = colWidth((row) => row.laneOrRole, 18);
904
+ const lines = rows.map((row) => [
905
+ pad(clip(row.name, nameW), nameW),
906
+ pad(row.id, idW),
907
+ pad(clip(row.state, stateW), stateW),
908
+ pad(clip(row.where, whereW), whereW),
909
+ pad(clip(row.laneOrRole, laneW), laneW),
910
+ row.inbox,
911
+ ]
912
+ .join(" ")
913
+ .trimEnd());
914
+ if (agents.length > shown.length) {
915
+ lines.push(`… +${agents.length - shown.length} more`);
916
+ }
917
+ return lines.join("\n");
569
918
  }
570
919
  function formatPinetLaneSummary(lane) {
571
920
  const refs = [
@@ -639,6 +988,46 @@ function getMaybeMetadata(params) {
639
988
  return null;
640
989
  return isRecord(value) ? value : undefined;
641
990
  }
991
+ function buildCompactPortLeaseDetails(leases) {
992
+ const shownLeases = leases.slice(0, PINET_COMPACT_LIST_LIMIT);
993
+ return {
994
+ leaseCount: leases.length,
995
+ total: leases.length,
996
+ shown: shownLeases.length,
997
+ truncated: Math.max(0, leases.length - shownLeases.length),
998
+ leases: shownLeases.map((lease) => ({
999
+ leaseId: lease.id,
1000
+ host: lease.host,
1001
+ port: lease.port,
1002
+ status: lease.status,
1003
+ purpose: lease.purpose,
1004
+ ownerAgentId: lease.ownerAgentId ?? null,
1005
+ expiresAt: lease.expiresAt,
1006
+ })),
1007
+ };
1008
+ }
1009
+ function buildCompactLaneDetails(lanes) {
1010
+ const shownLanes = lanes.slice(0, PINET_COMPACT_LIST_LIMIT);
1011
+ return {
1012
+ laneCount: lanes.length,
1013
+ total: lanes.length,
1014
+ shown: shownLanes.length,
1015
+ truncated: Math.max(0, lanes.length - shownLanes.length),
1016
+ lanes: shownLanes.map((lane) => ({
1017
+ laneId: lane.laneId,
1018
+ state: lane.state,
1019
+ name: truncatePinetDetailText(lane.name),
1020
+ issueNumber: lane.issueNumber,
1021
+ prNumber: lane.prNumber,
1022
+ ownerAgentId: lane.ownerAgentId,
1023
+ implementationLeadAgentId: lane.implementationLeadAgentId,
1024
+ pmMode: lane.pmMode,
1025
+ participantCount: lane.participants.length,
1026
+ summary: truncatePinetDetailText(lane.summary),
1027
+ lastActivityAt: lane.lastActivityAt,
1028
+ })),
1029
+ };
1030
+ }
642
1031
  function formatPortLeaseSummary(lease) {
643
1032
  return `${lease.host}:${lease.port} ${lease.status} lease=${lease.id} purpose=${lease.purpose} owner=${lease.ownerAgentId ?? "none"} expires=${lease.expiresAt}`;
644
1033
  }
@@ -753,7 +1142,7 @@ function runPinetPortsAction(params, deps, toolName, output) {
753
1142
  return {
754
1143
  content: [{ type: "text", text: `Pinet port leases expired: ${leases.length}.` }],
755
1144
  details: { leases },
756
- compactDetails: { leaseCount: leases.length, leases: leases.slice(0, 10) },
1145
+ compactDetails: buildCompactPortLeaseDetails(leases),
757
1146
  fullDetails: { leases },
758
1147
  };
759
1148
  }
@@ -773,8 +1162,12 @@ function runPinetPortsAction(params, deps, toolName, output) {
773
1162
  return {
774
1163
  content: [{ type: "text", text: formatPortLeases(leases, output.full) }],
775
1164
  details: { leases },
776
- compactDetails: { leaseCount: leases.length, leases: leases.slice(0, 10) },
1165
+ compactDetails: buildCompactPortLeaseDetails(leases),
777
1166
  fullDetails: { leases },
1167
+ // Compact text is a count; show the per-lease rows on expand.
1168
+ ...(output.full || leases.length === 0
1169
+ ? {}
1170
+ : { expandedText: formatPortLeases(leases, true) }),
778
1171
  };
779
1172
  }
780
1173
  throw new Error("ports op must be acquire, renew, release, status, list, or expire");
@@ -803,8 +1196,12 @@ function runPinetLanesAction(params, deps, toolName, output) {
803
1196
  return {
804
1197
  content: [{ type: "text", text: formatPinetLanes(lanes, output.full) }],
805
1198
  details: { lanes },
806
- compactDetails: { laneCount: lanes.length, lanes: lanes.slice(0, 10) },
1199
+ compactDetails: buildCompactLaneDetails(lanes),
807
1200
  fullDetails: { lanes },
1201
+ // Compact text is a count; show the per-lane rows on expand.
1202
+ ...(output.full || lanes.length === 0
1203
+ ? {}
1204
+ : { expandedText: formatPinetLanes(lanes, true) }),
808
1205
  };
809
1206
  }
810
1207
  if (op === "upsert") {
@@ -893,10 +1290,20 @@ function runPinetLanesAction(params, deps, toolName, output) {
893
1290
  }
894
1291
  function runPinetAgentsAction(params, deps, toolName, output) {
895
1292
  return (async () => {
1293
+ const scope = params.scope === "children" || params.scope === "subtree" || params.scope === "all"
1294
+ ? params.scope
1295
+ : "visible";
1296
+ const requestedParentAgentId = typeof params.parent_agent === "string" ? params.parent_agent : undefined;
1297
+ const subtreeSelfAgentId = deps.getSubtreeSelfAgentId?.() ?? undefined;
1298
+ const parentAgentId = requestedParentAgentId ??
1299
+ (deps.brokerRole() === "follower" && (scope === "children" || scope === "subtree")
1300
+ ? subtreeSelfAgentId
1301
+ : undefined);
896
1302
  const hint = {
897
1303
  repo: typeof params.repo === "string" ? params.repo : undefined,
898
1304
  branch: typeof params.branch === "string" ? params.branch : undefined,
899
1305
  role: typeof params.role === "string" ? params.role : undefined,
1306
+ ...(scope !== "visible" ? { scope } : {}),
900
1307
  requiredTools: typeof params.required_tools === "string"
901
1308
  ? params.required_tools
902
1309
  .split(",")
@@ -905,7 +1312,7 @@ function runPinetAgentsAction(params, deps, toolName, output) {
905
1312
  : undefined,
906
1313
  task: typeof params.task === "string" ? params.task : undefined,
907
1314
  };
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}`);
1315
+ 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
1316
  if (!deps.pinetEnabled()) {
910
1317
  throw new Error("Pinet is not running. Use /pinet start or /pinet follow first.");
911
1318
  }
@@ -923,12 +1330,23 @@ function runPinetAgentsAction(params, deps, toolName, output) {
923
1330
  rawAgents = deps.listBrokerAgents();
924
1331
  }
925
1332
  else if (deps.brokerRole() === "follower") {
926
- rawAgents = await deps.listFollowerAgents(includeGhosts);
1333
+ const wantsLocalSubtree = scope === "children" || scope === "subtree";
1334
+ const subtreeAgents = wantsLocalSubtree ? deps.listSubtreeAgents?.(includeGhosts) : null;
1335
+ if (wantsLocalSubtree && subtreeAgents) {
1336
+ rawAgents = subtreeAgents;
1337
+ }
1338
+ else if (wantsLocalSubtree && deps.listSubtreeAgents && !subtreeAgents) {
1339
+ throw new Error("No active worker-owned subtree broker. Run /pinet subtree start first.");
1340
+ }
1341
+ else {
1342
+ rawAgents = await deps.listFollowerAgents(includeGhosts);
1343
+ }
927
1344
  }
928
1345
  else {
929
1346
  throw new Error("Pinet is in an unexpected state.");
930
1347
  }
931
- const visibleAgents = filterAgentsForMeshVisibility(rawAgents, {
1348
+ const scopedRawAgents = filterAgentsForHierarchyScope(rawAgents, scope, parentAgentId);
1349
+ const visibleAgents = filterAgentsForMeshVisibility(scopedRawAgents, {
932
1350
  now: nowMs,
933
1351
  includeGhosts,
934
1352
  recentDisconnectWindowMs: recentGhostWindowMs,
@@ -941,6 +1359,7 @@ function runPinetAgentsAction(params, deps, toolName, output) {
941
1359
  details: { agents, hint },
942
1360
  compactDetails: buildCompactAgentDetails(agents, hint),
943
1361
  fullDetails: { agents, hint },
1362
+ expandedText: formatPinetAgentsExpanded(agents),
944
1363
  };
945
1364
  })();
946
1365
  }
@@ -966,7 +1385,7 @@ export function registerPinetTools(pi, deps) {
966
1385
  });
967
1386
  registerAction({
968
1387
  name: "read",
969
- description: "Read this agent's durable SQLite-backed Pinet inbox context with unread/read semantics. Ordinary workers only see rows addressed to their own agent identity; broker coordination visibility is limited to broker-addressed inbox rows.",
1388
+ description: "Read this agent's durable SQLite-backed Pinet inbox context with unread/read semantics. Defaults to draining unread rows (`unread_only: true`, `mark_read: true`) with compact per-message previews — no `full=true` required for routine reads. Override `unread_only`/`mark_read` only for a non-destructive latest-history peek. Ordinary workers only see rows addressed to their own agent identity; broker coordination visibility is limited to broker-addressed inbox rows.",
970
1389
  parameters: Type.Object({
971
1390
  thread_id: Type.Optional(Type.String({
972
1391
  description: "Optional Pinet/Slack broker thread ID to filter this agent's own inbox rows; it does not grant cross-agent thread access.",
@@ -998,6 +1417,18 @@ export function registerPinetTools(pi, deps) {
998
1417
  }),
999
1418
  execute: (_id, params, _output) => runPinetSnoozeAction(params, deps, "pinet:snooze"),
1000
1419
  });
1420
+ registerAction({
1421
+ name: "spawn",
1422
+ description: "Worker-only: launch a tmux-backed child worker into this worker's active subtree broker and deliver the task over Pinet.",
1423
+ parameters: Type.Object({
1424
+ task: Type.String({ description: "Scoped task prompt for the child worker" }),
1425
+ repo: Type.String({ description: "Repo/workspace scope for the child worker" }),
1426
+ role: Type.Optional(Type.String({ description: "Child subtree role, e.g. reviewer" })),
1427
+ lane_id: Type.Optional(Type.String({ description: "Optional durable Pinet lane id" })),
1428
+ ...PINET_OUTPUT_OPTION_PARAMETERS,
1429
+ }),
1430
+ execute: (_id, params, output) => runPinetSpawnAction(params, deps, "pinet:spawn", output),
1431
+ });
1001
1432
  registerAction({
1002
1433
  name: "reload",
1003
1434
  description: "Ask another connected Pinet agent to reload itself.",
@@ -1039,6 +1470,10 @@ export function registerPinetTools(pi, deps) {
1039
1470
  include_ghosts: Type.Optional(Type.Boolean({
1040
1471
  description: "Include recently disconnected/resumable agents. Defaults false so graceful exits do not look like actionable ghosts.",
1041
1472
  })),
1473
+ scope: Type.Optional(Type.String({
1474
+ description: "Hierarchy filter: visible (default), children, subtree, or all. children/subtree require parent_agent for explicit inspection.",
1475
+ })),
1476
+ parent_agent: Type.Optional(Type.String({ description: "Parent agent id for scope=children or scope=subtree" })),
1042
1477
  ...PINET_OUTPUT_OPTION_PARAMETERS,
1043
1478
  }),
1044
1479
  execute: (_id, params, output) => runPinetAgentsAction(params, deps, "pinet:agents", output),
@@ -1114,15 +1549,44 @@ export function registerPinetTools(pi, deps) {
1114
1549
  name: "pinet",
1115
1550
  label: "Pinet Dispatcher",
1116
1551
  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.',
1552
+ 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
1553
  parameters: Type.Object({
1119
1554
  action: Type.String({
1120
- description: "Action name: help, send, read, free, snooze, schedule, agents, lanes, ports, reload, or exit.",
1555
+ 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
1556
  }),
1122
1557
  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.',
1558
+ 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
1559
  })),
1125
1560
  }),
1561
+ renderCall(args, theme) {
1562
+ const action = typeof args.action === "string" && args.action.trim() ? args.action.trim() : "?";
1563
+ let suffix = "";
1564
+ if (isRecord(args.args)) {
1565
+ const topic = typeof args.args.topic === "string" ? args.args.topic.trim() : "";
1566
+ const op = typeof args.args.op === "string" ? args.args.op.trim() : "";
1567
+ if (topic)
1568
+ suffix = ` ${theme.fg("dim", `topic=${topic}`)}`;
1569
+ else if (op)
1570
+ suffix = ` ${theme.fg("dim", `op=${op}`)}`;
1571
+ }
1572
+ return new Text(`${theme.fg("toolTitle", theme.bold("pinet"))} ${theme.fg("muted", action)}${suffix}`, 0, 0);
1573
+ },
1574
+ renderResult(result, { expanded, isPartial }, theme) {
1575
+ if (isPartial) {
1576
+ return new Text(theme.fg("warning", "Pinet running…"), 0, 0);
1577
+ }
1578
+ const display = formatPinetDispatcherResultForDisplay(result, expanded);
1579
+ const color = display.status === "failed"
1580
+ ? "error"
1581
+ : display.status === "succeeded"
1582
+ ? "success"
1583
+ : "muted";
1584
+ const icon = display.status === "failed" ? "✗" : display.status === "succeeded" ? "✓" : "•";
1585
+ const lines = display.text.split("\n");
1586
+ const first = lines[0] ?? "Pinet result.";
1587
+ const rest = lines.slice(1).map((line) => theme.fg("dim", line));
1588
+ return new Text([`${theme.fg(color, icon)} ${theme.fg(color, first)}`, ...rest].join("\n"), 0, 0);
1589
+ },
1126
1590
  async execute(toolCallId, params) {
1127
1591
  let normalizedAction;
1128
1592
  try {
@@ -1154,7 +1618,7 @@ export function registerPinetTools(pi, deps) {
1154
1618
  },
1155
1619
  ]), getTolerantPinetOutputOptions(args));
1156
1620
  }
1157
- return wrapDispatcherEnvelope(buildPinetDispatcherHelpEnvelope(args, actionDefinitions), output);
1621
+ return wrapDispatcherEnvelope(buildPinetDispatcherHelpEnvelope(args, actionDefinitions, output), output);
1158
1622
  }
1159
1623
  const definition = actionDefinitions.get(normalizedAction);
1160
1624
  if (!definition) {
@@ -1193,11 +1657,15 @@ export function registerPinetTools(pi, deps) {
1193
1657
  }
1194
1658
  try {
1195
1659
  const result = await definition.execute(toolCallId, params.args, output);
1660
+ const expandedText = typeof result.expandedText === "string" && result.expandedText.trim().length > 0
1661
+ ? result.expandedText
1662
+ : undefined;
1663
+ const displayText = result.content[0]?.text ?? "";
1196
1664
  return wrapDispatcherEnvelope(buildPinetDispatcherEnvelope("succeeded", {
1197
1665
  action: definition.name,
1198
- text: result.content[0]?.text ?? "",
1666
+ text: output.format === "json" ? (result.machineText ?? displayText) : displayText,
1199
1667
  details: selectPinetResultDetails(result, output),
1200
- }), output);
1668
+ }), output, expandedText, displayText);
1201
1669
  }
1202
1670
  catch (error) {
1203
1671
  if (error instanceof Error && error.name === "AbortError") {