@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.
- package/README.md +59 -32
- package/dist/broker/adapters/slack.d.ts +19 -1
- package/dist/broker/adapters/slack.js +111 -22
- package/dist/broker/client.d.ts +2 -1
- package/dist/broker/client.js +1 -0
- package/dist/broker/socket-server.js +18 -0
- package/dist/deploy-manifest.d.ts +5 -0
- package/dist/deploy-manifest.js +30 -1
- package/dist/follower-runtime.js +5 -1
- package/dist/helpers.d.ts +14 -0
- package/dist/helpers.js +45 -21
- package/dist/index.js +60 -0
- package/dist/pinet-commands.d.ts +6 -1
- package/dist/pinet-commands.js +166 -1
- package/dist/pinet-mesh-ops.d.ts +11 -0
- package/dist/pinet-mesh-ops.js +17 -0
- package/dist/pinet-tools.d.ts +47 -0
- package/dist/pinet-tools.js +496 -36
- package/dist/prompts/broker/tmux.md +2 -2
- package/dist/reaction-triggers.d.ts +1 -0
- package/dist/reaction-triggers.js +26 -15
- package/dist/runtime-agent-context.js +19 -0
- package/dist/runtime-mode.js +7 -1
- package/dist/single-player-runtime.js +22 -26
- package/dist/slack-access.d.ts +11 -0
- package/dist/slack-access.js +30 -0
- package/dist/slack-agents-command.d.ts +19 -0
- package/dist/slack-agents-command.js +90 -0
- package/dist/slack-export.d.ts +1 -1
- package/dist/slack-export.js +6 -4
- package/dist/slack-file-access.d.ts +34 -0
- package/dist/slack-file-access.js +209 -0
- package/dist/slack-message-context.d.ts +0 -1
- package/dist/slack-message-context.js +1 -6
- package/dist/slack-pinet-runtime-adapter.d.ts +4 -2
- package/dist/slack-pinet-runtime-adapter.js +12 -0
- package/dist/slack-tools.d.ts +6 -0
- package/dist/slack-tools.js +290 -36
- package/dist/slack-upload.d.ts +13 -1
- package/dist/slack-upload.js +29 -2
- package/dist/stale-slack-messages.d.ts +12 -0
- package/dist/stale-slack-messages.js +29 -0
- package/dist/subtree-broker-runtime.d.ts +109 -0
- package/dist/subtree-broker-runtime.js +558 -0
- package/manifest.yaml +9 -0
- package/package.json +9 -7
- package/skills/slack-bridge/SKILL.md +60 -1
package/dist/pinet-tools.js
CHANGED
|
@@ -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"
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
249
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
547
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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") {
|