@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.
- 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 +506 -38
- 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";
|
|
@@ -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
|
-
|
|
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"
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
249
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
547
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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") {
|