@pinet/slack-bridge 0.1.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/LICENSE +21 -0
- package/README.md +570 -0
- package/dist/activity-log.d.ts +62 -0
- package/dist/activity-log.js +293 -0
- package/dist/agent-completion-runtime.d.ts +17 -0
- package/dist/agent-completion-runtime.js +26 -0
- package/dist/agent-event-runtime.d.ts +13 -0
- package/dist/agent-event-runtime.js +27 -0
- package/dist/agent-prompt-guidance.d.ts +22 -0
- package/dist/agent-prompt-guidance.js +46 -0
- package/dist/broker/adapters/slack.d.ts +74 -0
- package/dist/broker/adapters/slack.js +563 -0
- package/dist/broker/adapters/types.d.ts +1 -0
- package/dist/broker/adapters/types.js +1 -0
- package/dist/broker/agent-messaging.d.ts +1 -0
- package/dist/broker/agent-messaging.js +1 -0
- package/dist/broker/auth.d.ts +1 -0
- package/dist/broker/auth.js +1 -0
- package/dist/broker/client.d.ts +167 -0
- package/dist/broker/client.js +624 -0
- package/dist/broker/control-plane-dashboard.d.ts +98 -0
- package/dist/broker/control-plane-dashboard.js +213 -0
- package/dist/broker/ghost-reaper.d.ts +49 -0
- package/dist/broker/ghost-reaper.js +209 -0
- package/dist/broker/index.d.ts +36 -0
- package/dist/broker/index.js +90 -0
- package/dist/broker/leader.d.ts +1 -0
- package/dist/broker/leader.js +1 -0
- package/dist/broker/maintenance.d.ts +1 -0
- package/dist/broker/maintenance.js +1 -0
- package/dist/broker/message-send.d.ts +1 -0
- package/dist/broker/message-send.js +1 -0
- package/dist/broker/paths.d.ts +1 -0
- package/dist/broker/paths.js +1 -0
- package/dist/broker/raw-tcp-loopback.d.ts +1 -0
- package/dist/broker/raw-tcp-loopback.js +1 -0
- package/dist/broker/router.d.ts +1 -0
- package/dist/broker/router.js +1 -0
- package/dist/broker/schema.d.ts +9 -0
- package/dist/broker/schema.js +63 -0
- package/dist/broker/socket-server.d.ts +120 -0
- package/dist/broker/socket-server.js +1087 -0
- package/dist/broker/types.d.ts +1 -0
- package/dist/broker/types.js +1 -0
- package/dist/broker-delivery.d.ts +10 -0
- package/dist/broker-delivery.js +26 -0
- package/dist/broker-inbound-persistence.d.ts +11 -0
- package/dist/broker-inbound-persistence.js +22 -0
- package/dist/broker-prompt-loader.d.ts +34 -0
- package/dist/broker-prompt-loader.js +185 -0
- package/dist/broker-runtime-access.d.ts +14 -0
- package/dist/broker-runtime-access.js +20 -0
- package/dist/broker-runtime.d.ts +100 -0
- package/dist/broker-runtime.js +533 -0
- package/dist/broker-thread-owner-hints.d.ts +10 -0
- package/dist/broker-thread-owner-hints.js +14 -0
- package/dist/canvases.d.ts +80 -0
- package/dist/canvases.js +221 -0
- package/dist/command-registration-runtime.d.ts +9 -0
- package/dist/command-registration-runtime.js +9 -0
- package/dist/core-tool-guardrails.d.ts +24 -0
- package/dist/core-tool-guardrails.js +66 -0
- package/dist/deploy-manifest.d.ts +27 -0
- package/dist/deploy-manifest.js +197 -0
- package/dist/follower-delivery.d.ts +16 -0
- package/dist/follower-delivery.js +70 -0
- package/dist/follower-runtime.d.ts +61 -0
- package/dist/follower-runtime.js +350 -0
- package/dist/git-metadata.d.ts +35 -0
- package/dist/git-metadata.js +88 -0
- package/dist/guardrails.d.ts +69 -0
- package/dist/guardrails.js +283 -0
- package/dist/helpers.d.ts +550 -0
- package/dist/helpers.js +2711 -0
- package/dist/home-tab.d.ts +27 -0
- package/dist/home-tab.js +242 -0
- package/dist/imessage-tools.d.ts +33 -0
- package/dist/imessage-tools.js +96 -0
- package/dist/inbox-drain-runtime.d.ts +30 -0
- package/dist/inbox-drain-runtime.js +71 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1330 -0
- package/dist/persisted-runtime-state.d.ts +52 -0
- package/dist/persisted-runtime-state.js +98 -0
- package/dist/pinet-activity-formatting.d.ts +20 -0
- package/dist/pinet-activity-formatting.js +43 -0
- package/dist/pinet-agent-status.d.ts +36 -0
- package/dist/pinet-agent-status.js +48 -0
- package/dist/pinet-commands.d.ts +70 -0
- package/dist/pinet-commands.js +402 -0
- package/dist/pinet-confirmation-replies.d.ts +7 -0
- package/dist/pinet-confirmation-replies.js +30 -0
- package/dist/pinet-control-plane-dashboard.d.ts +34 -0
- package/dist/pinet-control-plane-dashboard.js +86 -0
- package/dist/pinet-home-tabs.d.ts +36 -0
- package/dist/pinet-home-tabs.js +101 -0
- package/dist/pinet-maintenance-delivery.d.ts +22 -0
- package/dist/pinet-maintenance-delivery.js +45 -0
- package/dist/pinet-mesh-ops.d.ts +97 -0
- package/dist/pinet-mesh-ops.js +266 -0
- package/dist/pinet-registration-gate.d.ts +14 -0
- package/dist/pinet-registration-gate.js +42 -0
- package/dist/pinet-remote-control-acks.d.ts +16 -0
- package/dist/pinet-remote-control-acks.js +46 -0
- package/dist/pinet-remote-control.d.ts +17 -0
- package/dist/pinet-remote-control.js +74 -0
- package/dist/pinet-runtime-composition.d.ts +27 -0
- package/dist/pinet-runtime-composition.js +20 -0
- package/dist/pinet-tools.d.ts +67 -0
- package/dist/pinet-tools.js +1211 -0
- package/dist/prompts/broker/default.md +41 -0
- package/dist/prompts/broker/tmux.md +49 -0
- package/dist/ralph-loop.d.ts +76 -0
- package/dist/ralph-loop.js +524 -0
- package/dist/reaction-triggers.d.ts +25 -0
- package/dist/reaction-triggers.js +171 -0
- package/dist/repo-tool-guardrails.d.ts +20 -0
- package/dist/repo-tool-guardrails.js +81 -0
- package/dist/runtime-agent-context.d.ts +93 -0
- package/dist/runtime-agent-context.js +302 -0
- package/dist/runtime-mode.d.ts +10 -0
- package/dist/runtime-mode.js +39 -0
- package/dist/scheduled-wakeups.d.ts +1 -0
- package/dist/scheduled-wakeups.js +1 -0
- package/dist/session-ui-runtime.d.ts +20 -0
- package/dist/session-ui-runtime.js +163 -0
- package/dist/single-player-runtime.d.ts +71 -0
- package/dist/single-player-runtime.js +436 -0
- package/dist/skins/cosmere.json +1571 -0
- package/dist/skins/foundation.json +1304 -0
- package/dist/slack-access.d.ts +186 -0
- package/dist/slack-access.js +462 -0
- package/dist/slack-api.d.ts +2 -0
- package/dist/slack-api.js +1 -0
- package/dist/slack-block-kit.d.ts +36 -0
- package/dist/slack-block-kit.js +242 -0
- package/dist/slack-export.d.ts +31 -0
- package/dist/slack-export.js +206 -0
- package/dist/slack-message-context.d.ts +15 -0
- package/dist/slack-message-context.js +207 -0
- package/dist/slack-modals.d.ts +13 -0
- package/dist/slack-modals.js +79 -0
- package/dist/slack-pinet-runtime-adapter.d.ts +16 -0
- package/dist/slack-pinet-runtime-adapter.js +72 -0
- package/dist/slack-presence.d.ts +36 -0
- package/dist/slack-presence.js +89 -0
- package/dist/slack-request-runtime.d.ts +8 -0
- package/dist/slack-request-runtime.js +19 -0
- package/dist/slack-runtime-access.d.ts +28 -0
- package/dist/slack-runtime-access.js +107 -0
- package/dist/slack-scope-diagnostics.d.ts +31 -0
- package/dist/slack-scope-diagnostics.js +235 -0
- package/dist/slack-socket-dedup.d.ts +7 -0
- package/dist/slack-socket-dedup.js +135 -0
- package/dist/slack-thread-status.d.ts +41 -0
- package/dist/slack-thread-status.js +156 -0
- package/dist/slack-tool-policy-runtime.d.ts +36 -0
- package/dist/slack-tool-policy-runtime.js +96 -0
- package/dist/slack-tools.d.ts +52 -0
- package/dist/slack-tools.js +2688 -0
- package/dist/slack-turn-guardrails.d.ts +19 -0
- package/dist/slack-turn-guardrails.js +36 -0
- package/dist/slack-upload.d.ts +49 -0
- package/dist/slack-upload.js +224 -0
- package/dist/task-assignments.d.ts +45 -0
- package/dist/task-assignments.js +527 -0
- package/dist/thread-confirmations.d.ts +22 -0
- package/dist/thread-confirmations.js +139 -0
- package/dist/tool-registration-runtime.d.ts +13 -0
- package/dist/tool-registration-runtime.js +13 -0
- package/dist/ttl-cache.d.ts +47 -0
- package/dist/ttl-cache.js +105 -0
- package/manifest.yaml +57 -0
- package/package.json +60 -0
- package/skills/pinet-skin-creator/SKILL.md +109 -0
- package/skills/pinet-skin-creator/references/descriptor-format.md +99 -0
- package/skills/pinet-skin-creator/references/safety-checklist.md +56 -0
- package/skills/pinet-skin-creator/templates/pinet-skin-descriptor.json +77 -0
- package/skills/slack-bridge/SKILL.md +360 -0
|
@@ -0,0 +1,1211 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { buildCompactPinetReadDetails, formatPinetReadResultCompact, formatPinetReadResultFull, } from "@pinet/pinet-core/pinet-read-formatting";
|
|
3
|
+
import { normalizePinetOutputOptions, } from "@pinet/pinet-core/output-options";
|
|
4
|
+
import { parseScheduledWakeupDelay, resolveScheduledWakeupFireAt, } from "@pinet/pinet-core/scheduled-wakeups";
|
|
5
|
+
import { Type } from "@sinclair/typebox";
|
|
6
|
+
import { buildAgentDisplayInfo, filterAgentsForMeshVisibility, formatAgentList, rankAgentsForRouting, } from "./helpers.js";
|
|
7
|
+
import { isBroadcastChannelTarget } from "./broker/agent-messaging.js";
|
|
8
|
+
import { DEFAULT_HEARTBEAT_TIMEOUT_MS } from "./broker/socket-server.js";
|
|
9
|
+
import { HEARTBEAT_INTERVAL_MS } from "./broker/client.js";
|
|
10
|
+
const PINET_DISPATCHER_EXAMPLES = {
|
|
11
|
+
send: [{ action: "send", args: { to: "@worker", message: "Please review PR #123" } }],
|
|
12
|
+
read: [
|
|
13
|
+
{ action: "read", args: { thread_id: "a2a:<broker>:<worker>", limit: 20 } },
|
|
14
|
+
{ action: "read", args: { unread_only: false, mark_read: false, full: true } },
|
|
15
|
+
],
|
|
16
|
+
free: [{ action: "free", args: { note: "Wrapped up <issue>" } }],
|
|
17
|
+
snooze: [
|
|
18
|
+
{ action: "snooze", args: { op: "set", duration: "30m", reason: "no work available" } },
|
|
19
|
+
{ action: "snooze", args: { op: "clear" } },
|
|
20
|
+
],
|
|
21
|
+
schedule: [{ action: "schedule", args: { delay: "30m", message: "Check queue state" } }],
|
|
22
|
+
agents: [{ action: "agents", args: { repo: "<repo>", role: "worker", full: true } }],
|
|
23
|
+
ports: [
|
|
24
|
+
{ action: "ports", args: { op: "acquire", purpose: "preview", ttl_ms: 600000 } },
|
|
25
|
+
{ action: "ports", args: { op: "renew", lease_id: "<lease-id>", ttl_ms: 600000 } },
|
|
26
|
+
{ action: "ports", args: { op: "list", include_inactive: true } },
|
|
27
|
+
],
|
|
28
|
+
lanes: [
|
|
29
|
+
{ action: "lanes", args: { op: "list", full: true } },
|
|
30
|
+
{
|
|
31
|
+
action: "lanes",
|
|
32
|
+
args: {
|
|
33
|
+
op: "upsert",
|
|
34
|
+
lane_id: "issue-688",
|
|
35
|
+
issue_number: 688,
|
|
36
|
+
pm_mode: true,
|
|
37
|
+
state: "active",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
reload: [{ action: "reload", args: { target: "@worker" } }],
|
|
42
|
+
exit: [{ action: "exit", args: { target: "@worker" } }],
|
|
43
|
+
};
|
|
44
|
+
const PINET_OUTPUT_OPTION_PARAMETERS = {
|
|
45
|
+
format: Type.Optional(Type.String({ description: 'Response presentation format: "cli" (default) or "json".' })),
|
|
46
|
+
full: Type.Optional(Type.Boolean({
|
|
47
|
+
description: "Include full verbose text/details; default cli output keeps data.details compact.",
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
function getRecordString(record, key) {
|
|
51
|
+
const value = record?.[key];
|
|
52
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
53
|
+
}
|
|
54
|
+
function isRecord(value) {
|
|
55
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
56
|
+
}
|
|
57
|
+
function normalizeDispatcherAction(value) {
|
|
58
|
+
if (typeof value !== "string") {
|
|
59
|
+
throw new Error("action is required");
|
|
60
|
+
}
|
|
61
|
+
const normalized = value
|
|
62
|
+
.trim()
|
|
63
|
+
.replace(/^pinet:/, "")
|
|
64
|
+
.toLowerCase();
|
|
65
|
+
if (!normalized) {
|
|
66
|
+
throw new Error("action is required");
|
|
67
|
+
}
|
|
68
|
+
const allowed = [
|
|
69
|
+
"help",
|
|
70
|
+
"send",
|
|
71
|
+
"read",
|
|
72
|
+
"free",
|
|
73
|
+
"snooze",
|
|
74
|
+
"schedule",
|
|
75
|
+
"agents",
|
|
76
|
+
"lanes",
|
|
77
|
+
"ports",
|
|
78
|
+
"reload",
|
|
79
|
+
"exit",
|
|
80
|
+
];
|
|
81
|
+
if (!allowed.includes(normalized)) {
|
|
82
|
+
throw new Error(`Unknown Pinet action: ${normalized}`);
|
|
83
|
+
}
|
|
84
|
+
return normalized;
|
|
85
|
+
}
|
|
86
|
+
function getErrorMessage(error) {
|
|
87
|
+
if (error instanceof Error) {
|
|
88
|
+
return error.message;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
return JSON.stringify(error);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return String(error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function classifyPinetError(message) {
|
|
98
|
+
if (message.includes("requires confirmation for action")) {
|
|
99
|
+
return {
|
|
100
|
+
class: "state",
|
|
101
|
+
message,
|
|
102
|
+
retryable: false,
|
|
103
|
+
hint: 'Call slack with action "confirm_action" first in the original thread.',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (message.includes("is not running") || message.includes("unexpected state")) {
|
|
107
|
+
return {
|
|
108
|
+
class: "state",
|
|
109
|
+
message,
|
|
110
|
+
retryable: false,
|
|
111
|
+
hint: "Start or reattach Pinet, then retry.",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (message.includes("thread_id must be") ||
|
|
115
|
+
message.includes("message is required") ||
|
|
116
|
+
message.includes("target is required") ||
|
|
117
|
+
message.includes("duration is required") ||
|
|
118
|
+
message.includes("op must be") ||
|
|
119
|
+
message.includes("lease_id") ||
|
|
120
|
+
message.includes("ttl_ms") ||
|
|
121
|
+
message.includes("purpose") ||
|
|
122
|
+
message.includes("port")) {
|
|
123
|
+
return {
|
|
124
|
+
class: "input",
|
|
125
|
+
message,
|
|
126
|
+
retryable: false,
|
|
127
|
+
hint: "Fix the action arguments and retry.",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (message.includes("ECONN") || message.includes("fetch") || message.includes("ENOTFOUND")) {
|
|
131
|
+
return {
|
|
132
|
+
class: "network",
|
|
133
|
+
message,
|
|
134
|
+
retryable: true,
|
|
135
|
+
hint: "Retry when transport is reachable.",
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
class: "runtime",
|
|
140
|
+
message,
|
|
141
|
+
retryable: false,
|
|
142
|
+
hint: "Check inputs and runtime state, then retry.",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function buildPinetDispatcherEnvelope(status, data, errors = [], warnings = []) {
|
|
146
|
+
return { status, data, errors, warnings };
|
|
147
|
+
}
|
|
148
|
+
function formatPinetHelpCliText(data) {
|
|
149
|
+
const actions = Array.isArray(data.actions)
|
|
150
|
+
? data.actions
|
|
151
|
+
.map((action) => isRecord(action) && typeof action.action === "string" ? action.action : null)
|
|
152
|
+
.filter((action) => Boolean(action))
|
|
153
|
+
: [];
|
|
154
|
+
if (actions.length > 0) {
|
|
155
|
+
const note = typeof data.note === "string" && data.note.trim() ? ` ${data.note.trim()}` : "";
|
|
156
|
+
return `Pinet actions: ${actions.join(", ")}.${note}`;
|
|
157
|
+
}
|
|
158
|
+
if (typeof data.action === "string") {
|
|
159
|
+
const description = typeof data.description === "string" && data.description.trim()
|
|
160
|
+
? ` — ${data.description.trim()}`
|
|
161
|
+
: "";
|
|
162
|
+
return `Pinet ${data.action}${description}. Use args.format="json" or args.full=true for schema/details; JSON/full output can fill context quickly.`;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
function getPinetEnvelopeCliText(envelope) {
|
|
167
|
+
if (envelope.status === "succeeded" && isRecord(envelope.data)) {
|
|
168
|
+
const text = envelope.data.text;
|
|
169
|
+
if (typeof text === "string" && text.length > 0)
|
|
170
|
+
return text;
|
|
171
|
+
const helpText = formatPinetHelpCliText(envelope.data);
|
|
172
|
+
if (helpText)
|
|
173
|
+
return helpText;
|
|
174
|
+
const action = typeof envelope.data.action === "string" ? ` ${envelope.data.action}` : "";
|
|
175
|
+
return `Pinet${action} succeeded. Use args.format="json" or args.full=true for details; JSON/full output can fill context quickly.`;
|
|
176
|
+
}
|
|
177
|
+
const errors = envelope.errors.map((error) => error.message).filter(Boolean);
|
|
178
|
+
const hints = Array.from(new Set(envelope.errors.map((error) => error.hint).filter((hint) => hint.length > 0)));
|
|
179
|
+
if (errors.length > 0) {
|
|
180
|
+
return `Pinet ${envelope.status}: ${errors.join("; ")}${hints.length > 0 ? ` Hint: ${hints.join(" ")}` : ""}`;
|
|
181
|
+
}
|
|
182
|
+
return `Pinet ${envelope.status}. Use args.format="json" or args.full=true for details; JSON/full output can fill context quickly.`;
|
|
183
|
+
}
|
|
184
|
+
function getTolerantPinetOutputOptions(value) {
|
|
185
|
+
if (!isRecord(value))
|
|
186
|
+
return { format: "cli", full: false };
|
|
187
|
+
const rawFormat = value.format ?? value.f ?? value["-f"];
|
|
188
|
+
const normalizedFormat = rawFormat == null ? "cli" : String(rawFormat).trim().toLowerCase();
|
|
189
|
+
const format = normalizedFormat === "json" ? "json" : "cli";
|
|
190
|
+
const rawFull = value.full ?? value["--full"];
|
|
191
|
+
return { format, full: rawFull === true };
|
|
192
|
+
}
|
|
193
|
+
function getOptionalPinetOutputOptions(value) {
|
|
194
|
+
if (!isRecord(value))
|
|
195
|
+
return { format: "cli", full: false };
|
|
196
|
+
try {
|
|
197
|
+
return normalizePinetOutputOptions(value);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return getTolerantPinetOutputOptions(value);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function shouldRenderStructuredPinetEnvelope(envelope, output) {
|
|
204
|
+
if (output.format === "json")
|
|
205
|
+
return true;
|
|
206
|
+
if (!output.full)
|
|
207
|
+
return false;
|
|
208
|
+
if (envelope.status === "failed")
|
|
209
|
+
return true;
|
|
210
|
+
if (!isRecord(envelope.data))
|
|
211
|
+
return true;
|
|
212
|
+
const text = envelope.data.text;
|
|
213
|
+
return typeof text !== "string" || text.length === 0;
|
|
214
|
+
}
|
|
215
|
+
function wrapDispatcherEnvelope(envelope, output = { format: "cli", full: false }) {
|
|
216
|
+
return {
|
|
217
|
+
content: [
|
|
218
|
+
{
|
|
219
|
+
type: "text",
|
|
220
|
+
text: shouldRenderStructuredPinetEnvelope(envelope, output)
|
|
221
|
+
? JSON.stringify(envelope, null, 2)
|
|
222
|
+
: getPinetEnvelopeCliText(envelope),
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
details: envelope,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function selectPinetResultDetails(result, output) {
|
|
229
|
+
if (output.format === "json" || output.full) {
|
|
230
|
+
return result.details ?? null;
|
|
231
|
+
}
|
|
232
|
+
if (result.compactDetails !== undefined) {
|
|
233
|
+
return result.compactDetails;
|
|
234
|
+
}
|
|
235
|
+
return result.details ?? null;
|
|
236
|
+
}
|
|
237
|
+
function buildPinetDispatcherHelpEnvelope(args, actions) {
|
|
238
|
+
const topic = typeof args.topic === "string" && args.topic.trim()
|
|
239
|
+
? args.topic.trim().toLowerCase()
|
|
240
|
+
: undefined;
|
|
241
|
+
if (!topic) {
|
|
242
|
+
const catalog = Array.from(actions.values())
|
|
243
|
+
.filter((definition) => definition.name !== "help")
|
|
244
|
+
.map((definition) => ({
|
|
245
|
+
action: definition.name,
|
|
246
|
+
description: definition.description,
|
|
247
|
+
guardrail_tool: `pinet:${definition.name}`,
|
|
248
|
+
args_schema: definition.parameters,
|
|
249
|
+
examples: PINET_DISPATCHER_EXAMPLES[definition.name] ?? [],
|
|
250
|
+
}))
|
|
251
|
+
.sort((a, b) => a.action.localeCompare(b.action));
|
|
252
|
+
return buildPinetDispatcherEnvelope("succeeded", {
|
|
253
|
+
actions: catalog,
|
|
254
|
+
note: "Use args.topic to inspect a single action schema.",
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
const definition = actions.get(topic);
|
|
258
|
+
if (!definition || definition.name === "help") {
|
|
259
|
+
return buildPinetDispatcherEnvelope("failed", null, [
|
|
260
|
+
{
|
|
261
|
+
class: "input",
|
|
262
|
+
message: `Unknown Pinet action: ${topic}`,
|
|
263
|
+
retryable: false,
|
|
264
|
+
hint: 'Use action="help" to inspect available actions.',
|
|
265
|
+
},
|
|
266
|
+
]);
|
|
267
|
+
}
|
|
268
|
+
return buildPinetDispatcherEnvelope("succeeded", {
|
|
269
|
+
action: definition.name,
|
|
270
|
+
guardrail_tool: `pinet:${definition.name}`,
|
|
271
|
+
description: definition.description,
|
|
272
|
+
args_schema: definition.parameters,
|
|
273
|
+
examples: PINET_DISPATCHER_EXAMPLES[definition.name] ?? [],
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function buildPinetAgentsHintText(hint) {
|
|
277
|
+
return `Agent routing hints: ${[
|
|
278
|
+
hint.repo ? `repo=${hint.repo}` : null,
|
|
279
|
+
hint.branch ? `branch=${hint.branch}` : null,
|
|
280
|
+
hint.role ? `role=${hint.role}` : null,
|
|
281
|
+
hint.requiredTools && hint.requiredTools.length > 0
|
|
282
|
+
? `tools=${hint.requiredTools.join(",")}`
|
|
283
|
+
: null,
|
|
284
|
+
hint.task ? `task=${hint.task}` : null,
|
|
285
|
+
]
|
|
286
|
+
.filter((item) => Boolean(item))
|
|
287
|
+
.join(" · ")}`;
|
|
288
|
+
}
|
|
289
|
+
function runPinetSendAction(params, deps, toolName, output) {
|
|
290
|
+
return (async () => {
|
|
291
|
+
const to = typeof params.to === "string" ? params.to.trim() : "";
|
|
292
|
+
const message = typeof params.message === "string" ? params.message : "";
|
|
293
|
+
const transferThreadId = typeof params.transfer_thread_id === "string" && params.transfer_thread_id.trim().length > 0
|
|
294
|
+
? params.transfer_thread_id.trim()
|
|
295
|
+
: undefined;
|
|
296
|
+
if (!to) {
|
|
297
|
+
throw new Error("to is required");
|
|
298
|
+
}
|
|
299
|
+
if (!message) {
|
|
300
|
+
throw new Error("message is required");
|
|
301
|
+
}
|
|
302
|
+
deps.requireToolPolicy(toolName, undefined, `to=${to} | message=${message}${transferThreadId ? ` | transfer_thread_id=${transferThreadId}` : ""}`);
|
|
303
|
+
if (transferThreadId && deps.brokerRole() !== "broker") {
|
|
304
|
+
throw new Error("transfer_thread_id is broker-only and requires the broker role.");
|
|
305
|
+
}
|
|
306
|
+
if (transferThreadId && isBroadcastChannelTarget(to)) {
|
|
307
|
+
throw new Error("transfer_thread_id requires a direct agent target, not a broadcast channel.");
|
|
308
|
+
}
|
|
309
|
+
if (deps.brokerRole() === "broker" && isBroadcastChannelTarget(to)) {
|
|
310
|
+
const result = deps.sendPinetBroadcastMessage(to, message);
|
|
311
|
+
const preview = result.recipients.slice(0, 5).join(", ");
|
|
312
|
+
const suffix = result.recipients.length > 5 ? ", …" : "";
|
|
313
|
+
return {
|
|
314
|
+
content: [
|
|
315
|
+
{
|
|
316
|
+
type: "text",
|
|
317
|
+
text: output.full
|
|
318
|
+
? `Broadcast sent to ${result.channel} (${result.recipients.length} agents: ${preview}${suffix}).`
|
|
319
|
+
: `Pinet broadcast sent to ${result.channel} (${result.recipients.length} recipients).`,
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
details: {
|
|
323
|
+
channel: result.channel,
|
|
324
|
+
messageIds: result.messageIds,
|
|
325
|
+
recipients: result.recipients,
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const result = transferThreadId
|
|
330
|
+
? await deps.sendPinetAgentMessage(to, message, {
|
|
331
|
+
threadOwnershipTransfer: { mode: "transfer", threadId: transferThreadId },
|
|
332
|
+
})
|
|
333
|
+
: await deps.sendPinetAgentMessage(to, message);
|
|
334
|
+
return {
|
|
335
|
+
content: [
|
|
336
|
+
{
|
|
337
|
+
type: "text",
|
|
338
|
+
text: output.full
|
|
339
|
+
? `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}` : ""}.`,
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
details: {
|
|
344
|
+
messageId: result.messageId,
|
|
345
|
+
target: result.target,
|
|
346
|
+
...(result.transferredThreadId ? { transferredThreadId: result.transferredThreadId } : {}),
|
|
347
|
+
...(result.transferredThreadChannel
|
|
348
|
+
? { transferredThreadChannel: result.transferredThreadChannel }
|
|
349
|
+
: {}),
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
})();
|
|
353
|
+
}
|
|
354
|
+
function runPinetReadAction(params, deps, toolName, output) {
|
|
355
|
+
return (async () => {
|
|
356
|
+
deps.requireToolPolicy(toolName, undefined, `thread_id=${params.thread_id ?? ""} | limit=${params.limit ?? ""} | unread_only=${params.unread_only ?? ""} | mark_read=${params.mark_read ?? ""} | format=${output.format} | full=${output.full}`);
|
|
357
|
+
if (!deps.pinetEnabled()) {
|
|
358
|
+
throw new Error("Pinet is not running. Use /pinet start or /pinet follow first.");
|
|
359
|
+
}
|
|
360
|
+
const options = {
|
|
361
|
+
...(typeof params.thread_id === "string" ? { threadId: params.thread_id.trim() } : {}),
|
|
362
|
+
...(typeof params.limit === "number" ? { limit: params.limit } : {}),
|
|
363
|
+
...(typeof params.unread_only === "boolean" ? { unreadOnly: params.unread_only } : {}),
|
|
364
|
+
...(typeof params.mark_read === "boolean" ? { markRead: params.mark_read } : {}),
|
|
365
|
+
};
|
|
366
|
+
if (options.threadId !== undefined && options.threadId.length === 0) {
|
|
367
|
+
throw new Error("thread_id must be a non-empty string when provided.");
|
|
368
|
+
}
|
|
369
|
+
const result = await deps.readPinetInbox(options);
|
|
370
|
+
return {
|
|
371
|
+
content: [
|
|
372
|
+
{
|
|
373
|
+
type: "text",
|
|
374
|
+
text: output.full
|
|
375
|
+
? formatPinetReadResultFull(result, options)
|
|
376
|
+
: formatPinetReadResultCompact(result, options),
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
details: result,
|
|
380
|
+
compactDetails: buildCompactPinetReadDetails(result),
|
|
381
|
+
fullDetails: result,
|
|
382
|
+
};
|
|
383
|
+
})();
|
|
384
|
+
}
|
|
385
|
+
function runPinetFreeAction(params, deps, toolName, output) {
|
|
386
|
+
return (async () => {
|
|
387
|
+
const note = typeof params.note === "string" ? params.note.trim() : "";
|
|
388
|
+
deps.requireToolPolicy(toolName, undefined, `note=${note}`);
|
|
389
|
+
const result = await deps.signalAgentFree(undefined, { requirePinet: true });
|
|
390
|
+
const inboxSuffix = result.queuedInboxCount > 0
|
|
391
|
+
? ` ${result.queuedInboxCount} queued inbox item${result.queuedInboxCount === 1 ? " remains" : "s remain"}.`
|
|
392
|
+
: "";
|
|
393
|
+
const noteSuffix = note ? ` Note: ${note}.` : "";
|
|
394
|
+
return {
|
|
395
|
+
content: [
|
|
396
|
+
{
|
|
397
|
+
type: "text",
|
|
398
|
+
text: output.full
|
|
399
|
+
? `Marked this Pinet agent idle/free for new work.${noteSuffix}${inboxSuffix}`
|
|
400
|
+
: `Pinet free: idle${result.queuedInboxCount > 0 ? `; ${result.queuedInboxCount} queued` : ""}.`,
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
details: {
|
|
404
|
+
status: "idle",
|
|
405
|
+
note: note || null,
|
|
406
|
+
queuedInboxCount: result.queuedInboxCount,
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
})();
|
|
410
|
+
}
|
|
411
|
+
function formatRalphSnoozeResult(status) {
|
|
412
|
+
if (!status) {
|
|
413
|
+
return "RALPH snooze unavailable outside broker mode.";
|
|
414
|
+
}
|
|
415
|
+
if (!status.active) {
|
|
416
|
+
return `RALPH snooze off (${status.emptyCycleCount} empty cycle${status.emptyCycleCount === 1 ? "" : "s"}).`;
|
|
417
|
+
}
|
|
418
|
+
return `RALPH snoozed until ${status.until ?? "unknown"}${status.reason ? ` (${status.reason})` : ""}.`;
|
|
419
|
+
}
|
|
420
|
+
function runPinetSnoozeAction(params, deps, toolName) {
|
|
421
|
+
return (async () => {
|
|
422
|
+
const op = typeof params.op === "string" ? params.op.trim().toLowerCase() : "status";
|
|
423
|
+
const duration = typeof params.duration === "string" ? params.duration.trim() : "";
|
|
424
|
+
const reason = typeof params.reason === "string" ? params.reason.trim() : "";
|
|
425
|
+
deps.requireToolPolicy(toolName, undefined, `op=${op} | duration=${duration} | reason=${reason}`);
|
|
426
|
+
if (deps.brokerRole() !== "broker") {
|
|
427
|
+
throw new Error("RALPH snooze is only available while running as the Pinet broker.");
|
|
428
|
+
}
|
|
429
|
+
if (op === "status") {
|
|
430
|
+
const status = deps.ralphSnoozeStatus?.() ?? null;
|
|
431
|
+
return {
|
|
432
|
+
content: [{ type: "text", text: formatRalphSnoozeResult(status) }],
|
|
433
|
+
details: status,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
if (["clear", "off", "wake", "resume", "cancel"].includes(op)) {
|
|
437
|
+
const status = deps.clearRalphSnooze?.();
|
|
438
|
+
if (!status)
|
|
439
|
+
throw new Error("RALPH snooze is unavailable in this runtime.");
|
|
440
|
+
return {
|
|
441
|
+
content: [{ type: "text", text: formatRalphSnoozeResult(status) }],
|
|
442
|
+
details: status,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
if (!["set", "start", "on", "snooze"].includes(op)) {
|
|
446
|
+
throw new Error("op must be status, set, or clear");
|
|
447
|
+
}
|
|
448
|
+
const durationMs = parseScheduledWakeupDelay(duration);
|
|
449
|
+
if (durationMs == null) {
|
|
450
|
+
throw new Error("duration is required for op=set; use values like 5m, 30s, 1h30m, or 1d");
|
|
451
|
+
}
|
|
452
|
+
const status = deps.snoozeRalphLoop?.({ durationMs, reason: reason || "tool action" });
|
|
453
|
+
if (!status)
|
|
454
|
+
throw new Error("RALPH snooze is unavailable in this runtime.");
|
|
455
|
+
return { content: [{ type: "text", text: formatRalphSnoozeResult(status) }], details: status };
|
|
456
|
+
})();
|
|
457
|
+
}
|
|
458
|
+
function runPinetRemoteControlAction(params, deps, toolName, command) {
|
|
459
|
+
return (async () => {
|
|
460
|
+
const target = typeof params.target === "string" ? params.target.trim() : "";
|
|
461
|
+
if (!target) {
|
|
462
|
+
throw new Error("target is required");
|
|
463
|
+
}
|
|
464
|
+
deps.requireToolPolicy(toolName, undefined, `target=${target}`);
|
|
465
|
+
const result = await deps.sendPinetAgentMessage(target, command);
|
|
466
|
+
return {
|
|
467
|
+
content: [
|
|
468
|
+
{
|
|
469
|
+
type: "text",
|
|
470
|
+
text: `Sent ${command} to ${result.target}.`,
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
details: { messageId: result.messageId, target: result.target, command },
|
|
474
|
+
};
|
|
475
|
+
})();
|
|
476
|
+
}
|
|
477
|
+
function runPinetScheduleAction(params, deps, toolName, output) {
|
|
478
|
+
return (async () => {
|
|
479
|
+
const delay = typeof params.delay === "string" ? params.delay : undefined;
|
|
480
|
+
const at = typeof params.at === "string" ? params.at : undefined;
|
|
481
|
+
const message = typeof params.message === "string" ? params.message.trim() : "";
|
|
482
|
+
deps.requireToolPolicy(toolName, undefined, `delay=${delay ?? ""} | at=${at ?? ""} | message=${message}`);
|
|
483
|
+
if (!deps.pinetEnabled()) {
|
|
484
|
+
throw new Error("Pinet is not running. Use /pinet start or /pinet follow first.");
|
|
485
|
+
}
|
|
486
|
+
if (!message) {
|
|
487
|
+
throw new Error("message is required");
|
|
488
|
+
}
|
|
489
|
+
const fireAt = resolveScheduledWakeupFireAt({ delay, at });
|
|
490
|
+
if (deps.brokerRole() === "broker") {
|
|
491
|
+
const wakeup = await deps.scheduleBrokerWakeup(fireAt, message);
|
|
492
|
+
return {
|
|
493
|
+
content: [
|
|
494
|
+
{
|
|
495
|
+
type: "text",
|
|
496
|
+
text: output.full
|
|
497
|
+
? `Wake-up scheduled for ${wakeup.fireAt} (id: ${wakeup.id}).`
|
|
498
|
+
: `Pinet wake-up scheduled for ${wakeup.fireAt}.`,
|
|
499
|
+
},
|
|
500
|
+
],
|
|
501
|
+
details: wakeup,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
if (deps.brokerRole() === "follower") {
|
|
505
|
+
const wakeup = await deps.scheduleFollowerWakeup(fireAt, message);
|
|
506
|
+
return {
|
|
507
|
+
content: [
|
|
508
|
+
{
|
|
509
|
+
type: "text",
|
|
510
|
+
text: output.full
|
|
511
|
+
? `Wake-up scheduled for ${wakeup.fireAt} (id: ${wakeup.id}).`
|
|
512
|
+
: `Pinet wake-up scheduled for ${wakeup.fireAt}.`,
|
|
513
|
+
},
|
|
514
|
+
],
|
|
515
|
+
details: wakeup,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
throw new Error("Pinet is in an unexpected state.");
|
|
519
|
+
})();
|
|
520
|
+
}
|
|
521
|
+
function getAgentRepo(agent) {
|
|
522
|
+
return getRecordString(agent.metadata, "repo");
|
|
523
|
+
}
|
|
524
|
+
function getAgentBranch(agent) {
|
|
525
|
+
return getRecordString(agent.metadata, "branch");
|
|
526
|
+
}
|
|
527
|
+
function getAgentRole(agent) {
|
|
528
|
+
return getRecordString(agent.metadata, "role");
|
|
529
|
+
}
|
|
530
|
+
function buildCompactAgentDetails(agents, hint) {
|
|
531
|
+
return {
|
|
532
|
+
count: agents.length,
|
|
533
|
+
hint,
|
|
534
|
+
agents: agents.map((agent) => {
|
|
535
|
+
const capabilityTags = agent.capabilityTags ?? [];
|
|
536
|
+
return {
|
|
537
|
+
id: agent.id,
|
|
538
|
+
name: agent.name,
|
|
539
|
+
emoji: agent.emoji,
|
|
540
|
+
status: agent.status,
|
|
541
|
+
health: agent.health ?? null,
|
|
542
|
+
repo: getAgentRepo(agent) ?? null,
|
|
543
|
+
branch: getAgentBranch(agent) ?? null,
|
|
544
|
+
role: getAgentRole(agent) ?? null,
|
|
545
|
+
brokerManaged: agent.metadata?.brokerManaged === true,
|
|
546
|
+
launchSource: agent.metadata?.launchSource ?? null,
|
|
547
|
+
tmuxSession: agent.metadata?.tmuxSession ?? null,
|
|
548
|
+
routingScore: agent.routingScore ?? null,
|
|
549
|
+
...(agent.pendingInboxCount != null && agent.pendingInboxCount > 0
|
|
550
|
+
? { pendingInboxCount: agent.pendingInboxCount }
|
|
551
|
+
: {}),
|
|
552
|
+
capabilityTags: capabilityTags.slice(0, 4),
|
|
553
|
+
capabilityCount: capabilityTags.length,
|
|
554
|
+
};
|
|
555
|
+
}),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function formatCompactAgentList(agents, hint) {
|
|
559
|
+
const hintParts = [
|
|
560
|
+
hint.repo ? `repo=${hint.repo}` : null,
|
|
561
|
+
hint.branch ? `branch=${hint.branch}` : null,
|
|
562
|
+
hint.role ? `role=${hint.role}` : null,
|
|
563
|
+
hint.requiredTools && hint.requiredTools.length > 0
|
|
564
|
+
? `tools=${hint.requiredTools.join(",")}`
|
|
565
|
+
: null,
|
|
566
|
+
].filter((item) => Boolean(item));
|
|
567
|
+
const hintSuffix = hintParts.length > 0 ? `; hints ${hintParts.join(" · ")}` : "";
|
|
568
|
+
return `Pinet agents: ${agents.length} visible${hintSuffix}.`;
|
|
569
|
+
}
|
|
570
|
+
function formatPinetLaneSummary(lane) {
|
|
571
|
+
const refs = [
|
|
572
|
+
lane.issueNumber != null ? `#${lane.issueNumber}` : null,
|
|
573
|
+
lane.prNumber != null ? `PR #${lane.prNumber}` : null,
|
|
574
|
+
lane.pmMode ? "PM" : null,
|
|
575
|
+
].filter((item) => Boolean(item));
|
|
576
|
+
const participants = lane.participants
|
|
577
|
+
.slice(0, 4)
|
|
578
|
+
.map((participant) => `${participant.agentId}:${participant.role}`)
|
|
579
|
+
.join(", ");
|
|
580
|
+
return [
|
|
581
|
+
`- ${lane.laneId} [${lane.state}]${refs.length > 0 ? ` (${refs.join(" · ")})` : ""}`,
|
|
582
|
+
lane.name ? ` — ${lane.name}` : "",
|
|
583
|
+
lane.ownerAgentId ? ` owner=${lane.ownerAgentId}` : "",
|
|
584
|
+
lane.implementationLeadAgentId ? ` lead=${lane.implementationLeadAgentId}` : "",
|
|
585
|
+
participants ? ` participants=${participants}` : "",
|
|
586
|
+
lane.summary ? ` — ${lane.summary}` : "",
|
|
587
|
+
].join("");
|
|
588
|
+
}
|
|
589
|
+
function formatPinetLanes(lanes, full) {
|
|
590
|
+
if (lanes.length === 0)
|
|
591
|
+
return "Pinet lanes: none tracked.";
|
|
592
|
+
if (!full)
|
|
593
|
+
return `Pinet lanes: ${lanes.length} tracked.`;
|
|
594
|
+
return ["Pinet lanes:", ...lanes.map(formatPinetLaneSummary)].join("\n");
|
|
595
|
+
}
|
|
596
|
+
function getMaybeString(params, key) {
|
|
597
|
+
const value = params[key];
|
|
598
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
599
|
+
}
|
|
600
|
+
function getMaybeBoolean(params, key) {
|
|
601
|
+
const value = params[key];
|
|
602
|
+
return typeof value === "boolean" ? value : undefined;
|
|
603
|
+
}
|
|
604
|
+
function getMaybeNumber(params, key) {
|
|
605
|
+
const value = params[key];
|
|
606
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
607
|
+
}
|
|
608
|
+
function requireNumber(params, key) {
|
|
609
|
+
const value = getMaybeNumber(params, key);
|
|
610
|
+
if (value === undefined) {
|
|
611
|
+
throw new Error(`${key} must be a finite number`);
|
|
612
|
+
}
|
|
613
|
+
return value;
|
|
614
|
+
}
|
|
615
|
+
function hasParam(params, key) {
|
|
616
|
+
return Object.prototype.hasOwnProperty.call(params, key);
|
|
617
|
+
}
|
|
618
|
+
function getNullableStringUpdate(params, key) {
|
|
619
|
+
if (!hasParam(params, key))
|
|
620
|
+
return undefined;
|
|
621
|
+
const value = params[key];
|
|
622
|
+
if (value === null)
|
|
623
|
+
return null;
|
|
624
|
+
return typeof value === "string" ? value : undefined;
|
|
625
|
+
}
|
|
626
|
+
function getNullableNumberUpdate(params, key) {
|
|
627
|
+
if (!hasParam(params, key))
|
|
628
|
+
return undefined;
|
|
629
|
+
const value = params[key];
|
|
630
|
+
if (value === null)
|
|
631
|
+
return null;
|
|
632
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
633
|
+
}
|
|
634
|
+
function getMaybeMetadata(params) {
|
|
635
|
+
if (!hasParam(params, "metadata"))
|
|
636
|
+
return undefined;
|
|
637
|
+
const value = params.metadata;
|
|
638
|
+
if (value === null)
|
|
639
|
+
return null;
|
|
640
|
+
return isRecord(value) ? value : undefined;
|
|
641
|
+
}
|
|
642
|
+
function formatPortLeaseSummary(lease) {
|
|
643
|
+
return `${lease.host}:${lease.port} ${lease.status} lease=${lease.id} purpose=${lease.purpose} owner=${lease.ownerAgentId ?? "none"} expires=${lease.expiresAt}`;
|
|
644
|
+
}
|
|
645
|
+
function formatPortLeases(leases, full) {
|
|
646
|
+
if (leases.length === 0)
|
|
647
|
+
return "Pinet port leases: none.";
|
|
648
|
+
if (!full)
|
|
649
|
+
return `Pinet port leases: ${leases.length}.`;
|
|
650
|
+
return [
|
|
651
|
+
"Pinet port leases:",
|
|
652
|
+
...leases.map((lease) => `- ${formatPortLeaseSummary(lease)}`),
|
|
653
|
+
].join("\n");
|
|
654
|
+
}
|
|
655
|
+
function runPinetPortsAction(params, deps, toolName, output) {
|
|
656
|
+
return (async () => {
|
|
657
|
+
const op = getMaybeString(params, "op")?.toLowerCase() ?? "list";
|
|
658
|
+
deps.requireToolPolicy(toolName, undefined, `op=${op} | format=${output.format}`);
|
|
659
|
+
if (!deps.pinetEnabled()) {
|
|
660
|
+
throw new Error("Pinet is not running. Use /pinet start or /pinet follow first.");
|
|
661
|
+
}
|
|
662
|
+
if (op === "acquire") {
|
|
663
|
+
const purpose = getMaybeString(params, "purpose");
|
|
664
|
+
if (!purpose)
|
|
665
|
+
throw new Error("ports op=acquire requires purpose");
|
|
666
|
+
const ttlMs = requireNumber(params, "ttl_ms");
|
|
667
|
+
const lease = await deps.acquirePortLease({
|
|
668
|
+
purpose,
|
|
669
|
+
ttlMs,
|
|
670
|
+
...(getMaybeString(params, "host") ? { host: getMaybeString(params, "host") } : {}),
|
|
671
|
+
...(getMaybeNumber(params, "port") !== undefined
|
|
672
|
+
? { port: getMaybeNumber(params, "port") }
|
|
673
|
+
: {}),
|
|
674
|
+
...(getMaybeNumber(params, "min_port") !== undefined
|
|
675
|
+
? { minPort: getMaybeNumber(params, "min_port") }
|
|
676
|
+
: {}),
|
|
677
|
+
...(getMaybeNumber(params, "max_port") !== undefined
|
|
678
|
+
? { maxPort: getMaybeNumber(params, "max_port") }
|
|
679
|
+
: {}),
|
|
680
|
+
...(getMaybeNumber(params, "pid") !== undefined
|
|
681
|
+
? { pid: getMaybeNumber(params, "pid") }
|
|
682
|
+
: {}),
|
|
683
|
+
...(getMaybeMetadata(params) !== undefined ? { metadata: getMaybeMetadata(params) } : {}),
|
|
684
|
+
});
|
|
685
|
+
return {
|
|
686
|
+
content: [
|
|
687
|
+
{ type: "text", text: `Pinet port lease acquired: ${formatPortLeaseSummary(lease)}.` },
|
|
688
|
+
],
|
|
689
|
+
details: { lease },
|
|
690
|
+
compactDetails: {
|
|
691
|
+
leaseId: lease.id,
|
|
692
|
+
host: lease.host,
|
|
693
|
+
port: lease.port,
|
|
694
|
+
expiresAt: lease.expiresAt,
|
|
695
|
+
},
|
|
696
|
+
fullDetails: { lease },
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
if (op === "renew") {
|
|
700
|
+
const leaseId = getMaybeString(params, "lease_id");
|
|
701
|
+
if (!leaseId)
|
|
702
|
+
throw new Error("ports op=renew requires lease_id");
|
|
703
|
+
const lease = await deps.renewPortLease({
|
|
704
|
+
leaseId,
|
|
705
|
+
ttlMs: requireNumber(params, "ttl_ms"),
|
|
706
|
+
});
|
|
707
|
+
return {
|
|
708
|
+
content: [
|
|
709
|
+
{ type: "text", text: `Pinet port lease renewed: ${formatPortLeaseSummary(lease)}.` },
|
|
710
|
+
],
|
|
711
|
+
details: { lease },
|
|
712
|
+
compactDetails: { leaseId: lease.id, expiresAt: lease.expiresAt },
|
|
713
|
+
fullDetails: { lease },
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
if (op === "release") {
|
|
717
|
+
const leaseId = getMaybeString(params, "lease_id");
|
|
718
|
+
if (!leaseId)
|
|
719
|
+
throw new Error("ports op=release requires lease_id");
|
|
720
|
+
const lease = await deps.releasePortLease({
|
|
721
|
+
leaseId,
|
|
722
|
+
});
|
|
723
|
+
return {
|
|
724
|
+
content: [
|
|
725
|
+
{ type: "text", text: `Pinet port lease released: ${formatPortLeaseSummary(lease)}.` },
|
|
726
|
+
],
|
|
727
|
+
details: { lease },
|
|
728
|
+
compactDetails: { leaseId: lease.id, status: lease.status },
|
|
729
|
+
fullDetails: { lease },
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
if (op === "status") {
|
|
733
|
+
const leaseId = getMaybeString(params, "lease_id");
|
|
734
|
+
if (!leaseId)
|
|
735
|
+
throw new Error("ports op=status requires lease_id");
|
|
736
|
+
const lease = await deps.getPortLease(leaseId);
|
|
737
|
+
return {
|
|
738
|
+
content: [
|
|
739
|
+
{
|
|
740
|
+
type: "text",
|
|
741
|
+
text: lease
|
|
742
|
+
? `Pinet port lease: ${formatPortLeaseSummary(lease)}.`
|
|
743
|
+
: "Pinet port lease: not found.",
|
|
744
|
+
},
|
|
745
|
+
],
|
|
746
|
+
details: { lease },
|
|
747
|
+
compactDetails: lease ? { leaseId: lease.id, status: lease.status } : { lease: null },
|
|
748
|
+
fullDetails: { lease },
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
if (op === "expire") {
|
|
752
|
+
const leases = await deps.expirePortLeases();
|
|
753
|
+
return {
|
|
754
|
+
content: [{ type: "text", text: `Pinet port leases expired: ${leases.length}.` }],
|
|
755
|
+
details: { leases },
|
|
756
|
+
compactDetails: { leaseCount: leases.length, leases: leases.slice(0, 10) },
|
|
757
|
+
fullDetails: { leases },
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
if (op === "list") {
|
|
761
|
+
const leases = await deps.listPortLeases({
|
|
762
|
+
...(getMaybeBoolean(params, "include_inactive") !== undefined
|
|
763
|
+
? { includeInactive: getMaybeBoolean(params, "include_inactive") }
|
|
764
|
+
: {}),
|
|
765
|
+
...(getMaybeBoolean(params, "expired_only") !== undefined
|
|
766
|
+
? { expiredOnly: getMaybeBoolean(params, "expired_only") }
|
|
767
|
+
: {}),
|
|
768
|
+
...(getMaybeString(params, "purpose")
|
|
769
|
+
? { purpose: getMaybeString(params, "purpose") }
|
|
770
|
+
: {}),
|
|
771
|
+
...(getMaybeString(params, "host") ? { host: getMaybeString(params, "host") } : {}),
|
|
772
|
+
});
|
|
773
|
+
return {
|
|
774
|
+
content: [{ type: "text", text: formatPortLeases(leases, output.full) }],
|
|
775
|
+
details: { leases },
|
|
776
|
+
compactDetails: { leaseCount: leases.length, leases: leases.slice(0, 10) },
|
|
777
|
+
fullDetails: { leases },
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
throw new Error("ports op must be acquire, renew, release, status, list, or expire");
|
|
781
|
+
})();
|
|
782
|
+
}
|
|
783
|
+
function runPinetLanesAction(params, deps, toolName, output) {
|
|
784
|
+
return (async () => {
|
|
785
|
+
const op = getMaybeString(params, "op")?.toLowerCase() ?? "list";
|
|
786
|
+
deps.requireToolPolicy(toolName, undefined, `op=${op} | format=${output.format}`);
|
|
787
|
+
if (!deps.pinetEnabled()) {
|
|
788
|
+
throw new Error("Pinet is not running. Use /pinet start or /pinet follow first.");
|
|
789
|
+
}
|
|
790
|
+
if (op === "list") {
|
|
791
|
+
const options = {
|
|
792
|
+
...(getMaybeString(params, "state")
|
|
793
|
+
? { state: getMaybeString(params, "state") }
|
|
794
|
+
: {}),
|
|
795
|
+
...(getMaybeString(params, "owner_agent")
|
|
796
|
+
? { ownerAgentId: getMaybeString(params, "owner_agent") }
|
|
797
|
+
: {}),
|
|
798
|
+
...(getMaybeBoolean(params, "include_done") !== undefined
|
|
799
|
+
? { includeDone: getMaybeBoolean(params, "include_done") }
|
|
800
|
+
: {}),
|
|
801
|
+
};
|
|
802
|
+
const lanes = await deps.listPinetLanes(options);
|
|
803
|
+
return {
|
|
804
|
+
content: [{ type: "text", text: formatPinetLanes(lanes, output.full) }],
|
|
805
|
+
details: { lanes },
|
|
806
|
+
compactDetails: { laneCount: lanes.length, lanes: lanes.slice(0, 10) },
|
|
807
|
+
fullDetails: { lanes },
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
if (op === "upsert") {
|
|
811
|
+
const laneId = getMaybeString(params, "lane_id");
|
|
812
|
+
if (!laneId)
|
|
813
|
+
throw new Error("lanes op=upsert requires lane_id");
|
|
814
|
+
const lane = await deps.upsertPinetLane({
|
|
815
|
+
laneId,
|
|
816
|
+
...(getNullableStringUpdate(params, "name") !== undefined
|
|
817
|
+
? { name: getNullableStringUpdate(params, "name") }
|
|
818
|
+
: {}),
|
|
819
|
+
...(getNullableStringUpdate(params, "task") !== undefined
|
|
820
|
+
? { task: getNullableStringUpdate(params, "task") }
|
|
821
|
+
: {}),
|
|
822
|
+
...(getNullableNumberUpdate(params, "issue_number") !== undefined
|
|
823
|
+
? { issueNumber: getNullableNumberUpdate(params, "issue_number") }
|
|
824
|
+
: {}),
|
|
825
|
+
...(getNullableNumberUpdate(params, "pr_number") !== undefined
|
|
826
|
+
? { prNumber: getNullableNumberUpdate(params, "pr_number") }
|
|
827
|
+
: {}),
|
|
828
|
+
...(getNullableStringUpdate(params, "thread_id") !== undefined
|
|
829
|
+
? { threadId: getNullableStringUpdate(params, "thread_id") }
|
|
830
|
+
: {}),
|
|
831
|
+
...(getNullableStringUpdate(params, "owner_agent") !== undefined
|
|
832
|
+
? { ownerAgentId: getNullableStringUpdate(params, "owner_agent") }
|
|
833
|
+
: {}),
|
|
834
|
+
...(getNullableStringUpdate(params, "implementation_lead") !== undefined
|
|
835
|
+
? { implementationLeadAgentId: getNullableStringUpdate(params, "implementation_lead") }
|
|
836
|
+
: {}),
|
|
837
|
+
...(getMaybeBoolean(params, "pm_mode") !== undefined
|
|
838
|
+
? { pmMode: getMaybeBoolean(params, "pm_mode") }
|
|
839
|
+
: {}),
|
|
840
|
+
...(getMaybeString(params, "state")
|
|
841
|
+
? { state: getMaybeString(params, "state") }
|
|
842
|
+
: {}),
|
|
843
|
+
...(getNullableStringUpdate(params, "summary") !== undefined
|
|
844
|
+
? { summary: getNullableStringUpdate(params, "summary") }
|
|
845
|
+
: {}),
|
|
846
|
+
...(getMaybeMetadata(params) !== undefined ? { metadata: getMaybeMetadata(params) } : {}),
|
|
847
|
+
});
|
|
848
|
+
return {
|
|
849
|
+
content: [{ type: "text", text: `Pinet lane ${lane.laneId} saved (${lane.state}).` }],
|
|
850
|
+
details: { lane },
|
|
851
|
+
compactDetails: { laneId: lane.laneId, state: lane.state, pmMode: lane.pmMode },
|
|
852
|
+
fullDetails: { lane },
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
if (op === "participant") {
|
|
856
|
+
const laneId = getMaybeString(params, "lane_id");
|
|
857
|
+
if (!laneId)
|
|
858
|
+
throw new Error("lanes op=participant requires lane_id");
|
|
859
|
+
const agentId = getMaybeString(params, "agent_id") ?? "";
|
|
860
|
+
const role = (getMaybeString(params, "lane_role") ??
|
|
861
|
+
getMaybeString(params, "role") ??
|
|
862
|
+
"observer");
|
|
863
|
+
const participant = await deps.setPinetLaneParticipant({
|
|
864
|
+
laneId,
|
|
865
|
+
agentId,
|
|
866
|
+
role,
|
|
867
|
+
...(getNullableStringUpdate(params, "status") !== undefined
|
|
868
|
+
? { status: getNullableStringUpdate(params, "status") }
|
|
869
|
+
: {}),
|
|
870
|
+
...(getNullableStringUpdate(params, "summary") !== undefined
|
|
871
|
+
? { summary: getNullableStringUpdate(params, "summary") }
|
|
872
|
+
: {}),
|
|
873
|
+
...(getMaybeMetadata(params) !== undefined ? { metadata: getMaybeMetadata(params) } : {}),
|
|
874
|
+
});
|
|
875
|
+
return {
|
|
876
|
+
content: [
|
|
877
|
+
{
|
|
878
|
+
type: "text",
|
|
879
|
+
text: `Pinet lane ${participant.laneId} participant ${participant.agentId} saved as ${participant.role}.`,
|
|
880
|
+
},
|
|
881
|
+
],
|
|
882
|
+
details: { participant },
|
|
883
|
+
compactDetails: {
|
|
884
|
+
laneId: participant.laneId,
|
|
885
|
+
agentId: participant.agentId,
|
|
886
|
+
role: participant.role,
|
|
887
|
+
},
|
|
888
|
+
fullDetails: { participant },
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
throw new Error("lanes op must be list, upsert, or participant");
|
|
892
|
+
})();
|
|
893
|
+
}
|
|
894
|
+
function runPinetAgentsAction(params, deps, toolName, output) {
|
|
895
|
+
return (async () => {
|
|
896
|
+
const hint = {
|
|
897
|
+
repo: typeof params.repo === "string" ? params.repo : undefined,
|
|
898
|
+
branch: typeof params.branch === "string" ? params.branch : undefined,
|
|
899
|
+
role: typeof params.role === "string" ? params.role : undefined,
|
|
900
|
+
requiredTools: typeof params.required_tools === "string"
|
|
901
|
+
? params.required_tools
|
|
902
|
+
.split(",")
|
|
903
|
+
.map((tool) => tool.trim())
|
|
904
|
+
.filter(Boolean)
|
|
905
|
+
: undefined,
|
|
906
|
+
task: typeof params.task === "string" ? params.task : undefined,
|
|
907
|
+
};
|
|
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}`);
|
|
909
|
+
if (!deps.pinetEnabled()) {
|
|
910
|
+
throw new Error("Pinet is not running. Use /pinet start or /pinet follow first.");
|
|
911
|
+
}
|
|
912
|
+
const includeGhosts = params.include_ghosts === true;
|
|
913
|
+
const recentGhostWindowMs = DEFAULT_HEARTBEAT_TIMEOUT_MS * 2;
|
|
914
|
+
const nowMs = Date.now();
|
|
915
|
+
const hasHint = Boolean(hint.repo || hint.branch || hint.role || (hint.requiredTools?.length ?? 0) > 0 || hint.task);
|
|
916
|
+
const toDisplay = (agent) => buildAgentDisplayInfo(agent, {
|
|
917
|
+
now: nowMs,
|
|
918
|
+
heartbeatTimeoutMs: DEFAULT_HEARTBEAT_TIMEOUT_MS,
|
|
919
|
+
heartbeatIntervalMs: HEARTBEAT_INTERVAL_MS,
|
|
920
|
+
});
|
|
921
|
+
let rawAgents;
|
|
922
|
+
if (deps.brokerRole() === "broker") {
|
|
923
|
+
rawAgents = deps.listBrokerAgents();
|
|
924
|
+
}
|
|
925
|
+
else if (deps.brokerRole() === "follower") {
|
|
926
|
+
rawAgents = await deps.listFollowerAgents(includeGhosts);
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
throw new Error("Pinet is in an unexpected state.");
|
|
930
|
+
}
|
|
931
|
+
const visibleAgents = filterAgentsForMeshVisibility(rawAgents, {
|
|
932
|
+
now: nowMs,
|
|
933
|
+
includeGhosts,
|
|
934
|
+
recentDisconnectWindowMs: recentGhostWindowMs,
|
|
935
|
+
}).map(toDisplay);
|
|
936
|
+
const agents = rankAgentsForRouting(visibleAgents, hint);
|
|
937
|
+
const header = hasHint && output.full ? `${buildPinetAgentsHintText(hint)}\n\n` : "";
|
|
938
|
+
const text = `${header}${output.full ? formatAgentList(agents, os.homedir()) : formatCompactAgentList(agents, hint)}`;
|
|
939
|
+
return {
|
|
940
|
+
content: [{ type: "text", text }],
|
|
941
|
+
details: { agents, hint },
|
|
942
|
+
compactDetails: buildCompactAgentDetails(agents, hint),
|
|
943
|
+
fullDetails: { agents, hint },
|
|
944
|
+
};
|
|
945
|
+
})();
|
|
946
|
+
}
|
|
947
|
+
export function registerPinetTools(pi, deps) {
|
|
948
|
+
const actionDefinitions = new Map();
|
|
949
|
+
function registerAction(definition) {
|
|
950
|
+
actionDefinitions.set(definition.name, definition);
|
|
951
|
+
}
|
|
952
|
+
registerAction({
|
|
953
|
+
name: "send",
|
|
954
|
+
description: "Send a message to a connected Pinet agent or broker-only broadcast channel.",
|
|
955
|
+
parameters: Type.Object({
|
|
956
|
+
to: Type.String({
|
|
957
|
+
description: "Target agent name/ID, or a broker-only broadcast channel like #extensions. Avoid #all for repo-specific issue/policy announcements.",
|
|
958
|
+
}),
|
|
959
|
+
message: Type.String({ description: "Message body" }),
|
|
960
|
+
transfer_thread_id: Type.Optional(Type.String({
|
|
961
|
+
description: "Broker-only: transfer ownership of this existing Slack thread to the direct recipient after delivery (for example a Slack thread_ts).",
|
|
962
|
+
})),
|
|
963
|
+
...PINET_OUTPUT_OPTION_PARAMETERS,
|
|
964
|
+
}),
|
|
965
|
+
execute: (_id, params, output) => runPinetSendAction(params, deps, "pinet:send", output),
|
|
966
|
+
});
|
|
967
|
+
registerAction({
|
|
968
|
+
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.",
|
|
970
|
+
parameters: Type.Object({
|
|
971
|
+
thread_id: Type.Optional(Type.String({
|
|
972
|
+
description: "Optional Pinet/Slack broker thread ID to filter this agent's own inbox rows; it does not grant cross-agent thread access.",
|
|
973
|
+
})),
|
|
974
|
+
limit: Type.Optional(Type.Number({ description: "Maximum messages to return (default 20, max 100)" })),
|
|
975
|
+
unread_only: Type.Optional(Type.Boolean({ description: "Only return unread rows (default true)" })),
|
|
976
|
+
mark_read: Type.Optional(Type.Boolean({ description: "Mark returned unread rows as read (default true)" })),
|
|
977
|
+
...PINET_OUTPUT_OPTION_PARAMETERS,
|
|
978
|
+
}),
|
|
979
|
+
execute: (_id, params, output) => runPinetReadAction(params, deps, "pinet:read", output),
|
|
980
|
+
});
|
|
981
|
+
registerAction({
|
|
982
|
+
name: "free",
|
|
983
|
+
description: "Mark this Pinet agent idle/free for new work.",
|
|
984
|
+
parameters: Type.Object({
|
|
985
|
+
note: Type.Optional(Type.String({ description: "Optional short note about what you just finished" })),
|
|
986
|
+
...PINET_OUTPUT_OPTION_PARAMETERS,
|
|
987
|
+
}),
|
|
988
|
+
execute: (_id, params, output) => runPinetFreeAction(params, deps, "pinet:free", output),
|
|
989
|
+
});
|
|
990
|
+
registerAction({
|
|
991
|
+
name: "snooze",
|
|
992
|
+
description: "Broker-only: inspect, set, or clear RALPH snooze so empty maintenance cycles stay quiet while urgent work still routes normally.",
|
|
993
|
+
parameters: Type.Object({
|
|
994
|
+
op: Type.Optional(Type.String({ description: "Operation: status, set, or clear" })),
|
|
995
|
+
duration: Type.Optional(Type.String({ description: "For op=set, relative duration like 30m" })),
|
|
996
|
+
reason: Type.Optional(Type.String({ description: "Optional snooze reason" })),
|
|
997
|
+
...PINET_OUTPUT_OPTION_PARAMETERS,
|
|
998
|
+
}),
|
|
999
|
+
execute: (_id, params, _output) => runPinetSnoozeAction(params, deps, "pinet:snooze"),
|
|
1000
|
+
});
|
|
1001
|
+
registerAction({
|
|
1002
|
+
name: "reload",
|
|
1003
|
+
description: "Ask another connected Pinet agent to reload itself.",
|
|
1004
|
+
parameters: Type.Object({
|
|
1005
|
+
target: Type.String({ description: "Target agent name or ID" }),
|
|
1006
|
+
...PINET_OUTPUT_OPTION_PARAMETERS,
|
|
1007
|
+
}),
|
|
1008
|
+
execute: (_id, params, _output) => runPinetRemoteControlAction(params, deps, "pinet:reload", "/reload"),
|
|
1009
|
+
});
|
|
1010
|
+
registerAction({
|
|
1011
|
+
name: "exit",
|
|
1012
|
+
description: "Ask another connected Pinet agent to exit gracefully.",
|
|
1013
|
+
parameters: Type.Object({
|
|
1014
|
+
target: Type.String({ description: "Target agent name or ID" }),
|
|
1015
|
+
...PINET_OUTPUT_OPTION_PARAMETERS,
|
|
1016
|
+
}),
|
|
1017
|
+
execute: (_id, params, _output) => runPinetRemoteControlAction(params, deps, "pinet:exit", "/exit"),
|
|
1018
|
+
});
|
|
1019
|
+
registerAction({
|
|
1020
|
+
name: "schedule",
|
|
1021
|
+
description: "Schedule a future wake-up for this Pinet agent.",
|
|
1022
|
+
parameters: Type.Object({
|
|
1023
|
+
delay: Type.Optional(Type.String({ description: "Relative delay like 5m, 30s, 1h30m, or 1d" })),
|
|
1024
|
+
at: Type.Optional(Type.String({ description: "Absolute ISO-8601 UTC time, e.g. 2026-04-02T14:30:00Z" })),
|
|
1025
|
+
message: Type.String({ description: "Reminder or wake-up message to deliver later" }),
|
|
1026
|
+
...PINET_OUTPUT_OPTION_PARAMETERS,
|
|
1027
|
+
}),
|
|
1028
|
+
execute: (_id, params, output) => runPinetScheduleAction(params, deps, "pinet:schedule", output),
|
|
1029
|
+
});
|
|
1030
|
+
registerAction({
|
|
1031
|
+
name: "agents",
|
|
1032
|
+
description: "List connected Pinet agents with status and capabilities.",
|
|
1033
|
+
parameters: Type.Object({
|
|
1034
|
+
repo: Type.Optional(Type.String({ description: "Preferred repo name for routing" })),
|
|
1035
|
+
branch: Type.Optional(Type.String({ description: "Preferred branch for routing" })),
|
|
1036
|
+
role: Type.Optional(Type.String({ description: "Preferred agent role, e.g. broker or worker" })),
|
|
1037
|
+
required_tools: Type.Optional(Type.String({ description: "Comma-separated required capability/tool tags" })),
|
|
1038
|
+
task: Type.Optional(Type.String({ description: "Optional natural-language task hint" })),
|
|
1039
|
+
include_ghosts: Type.Optional(Type.Boolean({
|
|
1040
|
+
description: "Include recently disconnected/resumable agents. Defaults false so graceful exits do not look like actionable ghosts.",
|
|
1041
|
+
})),
|
|
1042
|
+
...PINET_OUTPUT_OPTION_PARAMETERS,
|
|
1043
|
+
}),
|
|
1044
|
+
execute: (_id, params, output) => runPinetAgentsAction(params, deps, "pinet:agents", output),
|
|
1045
|
+
});
|
|
1046
|
+
registerAction({
|
|
1047
|
+
name: "ports",
|
|
1048
|
+
description: "Acquire, renew, release, inspect, list, or expire durable Pinet local port leases.",
|
|
1049
|
+
parameters: Type.Object({
|
|
1050
|
+
op: Type.Optional(Type.String({ description: "Operation: acquire, renew, release, status, list, or expire" })),
|
|
1051
|
+
purpose: Type.Optional(Type.String({ description: "Lease purpose for op=acquire" })),
|
|
1052
|
+
ttl_ms: Type.Optional(Type.Number({ description: "Lease TTL in milliseconds" })),
|
|
1053
|
+
lease_id: Type.Optional(Type.String({ description: "Lease id for renew/release/status" })),
|
|
1054
|
+
host: Type.Optional(Type.String({ description: "Host binding; default 127.0.0.1" })),
|
|
1055
|
+
port: Type.Optional(Type.Number({ description: "Requested port for op=acquire" })),
|
|
1056
|
+
min_port: Type.Optional(Type.Number({ description: "Minimum allocation port" })),
|
|
1057
|
+
max_port: Type.Optional(Type.Number({ description: "Maximum allocation port" })),
|
|
1058
|
+
pid: Type.Optional(Type.Number({ description: "Optional process id associated with the lease" })),
|
|
1059
|
+
include_inactive: Type.Optional(Type.Boolean({ description: "Include released/expired rows" })),
|
|
1060
|
+
expired_only: Type.Optional(Type.Boolean({ description: "List only expired rows" })),
|
|
1061
|
+
metadata: Type.Optional(Type.Union([Type.Record(Type.String(), Type.Unknown()), Type.Null()])),
|
|
1062
|
+
...PINET_OUTPUT_OPTION_PARAMETERS,
|
|
1063
|
+
}),
|
|
1064
|
+
execute: (_id, params, output) => runPinetPortsAction(params, deps, "pinet:ports", output),
|
|
1065
|
+
});
|
|
1066
|
+
registerAction({
|
|
1067
|
+
name: "lanes",
|
|
1068
|
+
description: "List or update durable Pinet lane metadata for PM-mode and complex coordination visibility.",
|
|
1069
|
+
parameters: Type.Object({
|
|
1070
|
+
op: Type.Optional(Type.String({ description: "Operation: list, upsert, or participant" })),
|
|
1071
|
+
lane_id: Type.Optional(Type.String({ description: "Stable lane id, e.g. issue-688" })),
|
|
1072
|
+
name: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "Human-readable lane name" })),
|
|
1073
|
+
task: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "Short lane task description" })),
|
|
1074
|
+
issue_number: Type.Optional(Type.Union([Type.Number(), Type.Null()], { description: "Linked GitHub issue number" })),
|
|
1075
|
+
pr_number: Type.Optional(Type.Union([Type.Number(), Type.Null()], { description: "Linked GitHub PR number" })),
|
|
1076
|
+
thread_id: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "Owning Pinet/Slack thread id" })),
|
|
1077
|
+
owner_agent: Type.Optional(Type.Union([Type.String(), Type.Null()], {
|
|
1078
|
+
description: "Accountable follower/PM agent id",
|
|
1079
|
+
})),
|
|
1080
|
+
implementation_lead: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "Implementation lead agent id" })),
|
|
1081
|
+
agent_id: Type.Optional(Type.String({ description: "Participant agent id for op=participant" })),
|
|
1082
|
+
lane_role: Type.Optional(Type.Union([
|
|
1083
|
+
Type.Literal("broker"),
|
|
1084
|
+
Type.Literal("coordinator"),
|
|
1085
|
+
Type.Literal("pm"),
|
|
1086
|
+
Type.Literal("lead"),
|
|
1087
|
+
Type.Literal("implementer"),
|
|
1088
|
+
Type.Literal("reviewer"),
|
|
1089
|
+
Type.Literal("second_pass_reviewer"),
|
|
1090
|
+
Type.Literal("observer"),
|
|
1091
|
+
], { description: "Participant role" })),
|
|
1092
|
+
pm_mode: Type.Optional(Type.Boolean({ description: "Whether PM mode is enabled" })),
|
|
1093
|
+
state: Type.Optional(Type.Union([
|
|
1094
|
+
Type.Literal("planned"),
|
|
1095
|
+
Type.Literal("active"),
|
|
1096
|
+
Type.Literal("blocked"),
|
|
1097
|
+
Type.Literal("review"),
|
|
1098
|
+
Type.Literal("ready"),
|
|
1099
|
+
Type.Literal("done"),
|
|
1100
|
+
Type.Literal("cancelled"),
|
|
1101
|
+
Type.Literal("detached"),
|
|
1102
|
+
], { description: "Lane state" })),
|
|
1103
|
+
status: Type.Optional(Type.Union([Type.String(), Type.Null()], { description: "Participant status" })),
|
|
1104
|
+
summary: Type.Optional(Type.Union([Type.String(), Type.Null()], {
|
|
1105
|
+
description: "Short lane or participant summary",
|
|
1106
|
+
})),
|
|
1107
|
+
metadata: Type.Optional(Type.Union([Type.Record(Type.String(), Type.Unknown()), Type.Null()])),
|
|
1108
|
+
include_done: Type.Optional(Type.Boolean({ description: "Include done/cancelled/detached lanes in list output" })),
|
|
1109
|
+
...PINET_OUTPUT_OPTION_PARAMETERS,
|
|
1110
|
+
}),
|
|
1111
|
+
execute: (_id, params, output) => runPinetLanesAction(params, deps, "pinet:lanes", output),
|
|
1112
|
+
});
|
|
1113
|
+
pi.registerTool({
|
|
1114
|
+
name: "pinet",
|
|
1115
|
+
label: "Pinet Dispatcher",
|
|
1116
|
+
description: "Dispatch Pinet operations by action with compact help and schema discovery.",
|
|
1117
|
+
promptSnippet: 'Use this compact dispatcher for Pinet actions: send, read, free, snooze, schedule, agents, lanes, ports, reload, exit, and help. Use /pinet start, /pinet follow, and /pinet unfollow for TUI lifecycle changes. Defaults to terse CLI text; pass args.format="json" or args.full=true for explicit detail, but avoid JSON/full unless needed because it can fill context quickly.',
|
|
1118
|
+
parameters: Type.Object({
|
|
1119
|
+
action: Type.String({
|
|
1120
|
+
description: "Action name: help, send, read, free, snooze, schedule, agents, lanes, ports, reload, or exit.",
|
|
1121
|
+
}),
|
|
1122
|
+
args: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
|
|
1123
|
+
description: 'Action arguments. Add format="cli"|"json" (or f/"-f") and full=true (or "--full": true) for explicit presentation control. Default cli keeps data.details compact; format="json" or full=true exposes full structured details and can fill context quickly, so use it only when needed.',
|
|
1124
|
+
})),
|
|
1125
|
+
}),
|
|
1126
|
+
async execute(toolCallId, params) {
|
|
1127
|
+
let normalizedAction;
|
|
1128
|
+
try {
|
|
1129
|
+
normalizedAction = normalizeDispatcherAction(params.action);
|
|
1130
|
+
}
|
|
1131
|
+
catch (error) {
|
|
1132
|
+
return wrapDispatcherEnvelope(buildPinetDispatcherEnvelope("failed", null, [
|
|
1133
|
+
{
|
|
1134
|
+
class: "input",
|
|
1135
|
+
message: getErrorMessage(error),
|
|
1136
|
+
retryable: false,
|
|
1137
|
+
hint: 'Use action="help" to inspect supported actions.',
|
|
1138
|
+
},
|
|
1139
|
+
]), getOptionalPinetOutputOptions(params.args));
|
|
1140
|
+
}
|
|
1141
|
+
if (normalizedAction === "help") {
|
|
1142
|
+
const args = isRecord(params.args) ? params.args : {};
|
|
1143
|
+
let output;
|
|
1144
|
+
try {
|
|
1145
|
+
output = normalizePinetOutputOptions(args);
|
|
1146
|
+
}
|
|
1147
|
+
catch (error) {
|
|
1148
|
+
return wrapDispatcherEnvelope(buildPinetDispatcherEnvelope("failed", null, [
|
|
1149
|
+
{
|
|
1150
|
+
class: "input",
|
|
1151
|
+
message: getErrorMessage(error),
|
|
1152
|
+
retryable: false,
|
|
1153
|
+
hint: 'Use format="cli" or format="json" and full as a boolean.',
|
|
1154
|
+
},
|
|
1155
|
+
]), getTolerantPinetOutputOptions(args));
|
|
1156
|
+
}
|
|
1157
|
+
return wrapDispatcherEnvelope(buildPinetDispatcherHelpEnvelope(args, actionDefinitions), output);
|
|
1158
|
+
}
|
|
1159
|
+
const definition = actionDefinitions.get(normalizedAction);
|
|
1160
|
+
if (!definition) {
|
|
1161
|
+
return wrapDispatcherEnvelope(buildPinetDispatcherEnvelope("failed", null, [
|
|
1162
|
+
{
|
|
1163
|
+
class: "input",
|
|
1164
|
+
message: `Unknown Pinet action: ${normalizedAction}`,
|
|
1165
|
+
retryable: false,
|
|
1166
|
+
hint: 'Use action="help" to inspect supported actions.',
|
|
1167
|
+
},
|
|
1168
|
+
]), getOptionalPinetOutputOptions(params.args));
|
|
1169
|
+
}
|
|
1170
|
+
if (!isRecord(params.args)) {
|
|
1171
|
+
return wrapDispatcherEnvelope(buildPinetDispatcherEnvelope("failed", null, [
|
|
1172
|
+
{
|
|
1173
|
+
class: "input",
|
|
1174
|
+
message: "args must be an object for Pinet action execution.",
|
|
1175
|
+
retryable: false,
|
|
1176
|
+
hint: "Pass a JSON object as args.",
|
|
1177
|
+
},
|
|
1178
|
+
]), getOptionalPinetOutputOptions(params.args));
|
|
1179
|
+
}
|
|
1180
|
+
let output;
|
|
1181
|
+
try {
|
|
1182
|
+
output = normalizePinetOutputOptions(params.args);
|
|
1183
|
+
}
|
|
1184
|
+
catch (error) {
|
|
1185
|
+
return wrapDispatcherEnvelope(buildPinetDispatcherEnvelope("failed", null, [
|
|
1186
|
+
{
|
|
1187
|
+
class: "input",
|
|
1188
|
+
message: getErrorMessage(error),
|
|
1189
|
+
retryable: false,
|
|
1190
|
+
hint: 'Use format="cli" or format="json" and full as a boolean.',
|
|
1191
|
+
},
|
|
1192
|
+
]), getTolerantPinetOutputOptions(params.args));
|
|
1193
|
+
}
|
|
1194
|
+
try {
|
|
1195
|
+
const result = await definition.execute(toolCallId, params.args, output);
|
|
1196
|
+
return wrapDispatcherEnvelope(buildPinetDispatcherEnvelope("succeeded", {
|
|
1197
|
+
action: definition.name,
|
|
1198
|
+
text: result.content[0]?.text ?? "",
|
|
1199
|
+
details: selectPinetResultDetails(result, output),
|
|
1200
|
+
}), output);
|
|
1201
|
+
}
|
|
1202
|
+
catch (error) {
|
|
1203
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
1204
|
+
throw error;
|
|
1205
|
+
}
|
|
1206
|
+
const message = getErrorMessage(error);
|
|
1207
|
+
return wrapDispatcherEnvelope(buildPinetDispatcherEnvelope("failed", null, [classifyPinetError(message)]), output);
|
|
1208
|
+
}
|
|
1209
|
+
},
|
|
1210
|
+
});
|
|
1211
|
+
}
|