@gugu910/pi-slack-bridge 0.1.3
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 +299 -0
- package/dist/activity-log.js +304 -0
- package/dist/broker/adapters/slack.js +645 -0
- package/dist/broker/adapters/types.js +3 -0
- package/dist/broker/agent-messaging.js +154 -0
- package/dist/broker/auth.js +97 -0
- package/dist/broker/client.js +495 -0
- package/dist/broker/control-plane-canvas.js +357 -0
- package/dist/broker/index.js +125 -0
- package/dist/broker/leader.js +133 -0
- package/dist/broker/maintenance.js +135 -0
- package/dist/broker/paths.js +69 -0
- package/dist/broker/router.js +287 -0
- package/dist/broker/schema.js +1492 -0
- package/dist/broker/socket-server.js +665 -0
- package/dist/broker/types.js +12 -0
- package/dist/broker-delivery.js +34 -0
- package/dist/canvases.js +175 -0
- package/dist/deploy-manifest.js +238 -0
- package/dist/follower-delivery.js +83 -0
- package/dist/git-metadata.js +95 -0
- package/dist/guardrails.js +197 -0
- package/dist/helpers.js +2128 -0
- package/dist/home-tab.js +240 -0
- package/dist/index.js +3086 -0
- package/dist/pinet-commands.js +244 -0
- package/dist/ralph-loop.js +385 -0
- package/dist/reaction-triggers.js +160 -0
- package/dist/scheduled-wakeups.js +71 -0
- package/dist/slack-api.js +5 -0
- package/dist/slack-block-kit.js +425 -0
- package/dist/slack-export.js +214 -0
- package/dist/slack-modals.js +269 -0
- package/dist/slack-presence.js +98 -0
- package/dist/slack-socket-dedup.js +143 -0
- package/dist/slack-tools.js +1715 -0
- package/dist/slack-upload.js +147 -0
- package/dist/task-assignments.js +403 -0
- package/dist/ttl-cache.js +110 -0
- package/manifest.yaml +57 -0
- package/package.json +45 -0
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,2128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEFAULT_CONFIRMATION_REQUEST_TTL_MS = exports.DEFAULT_PINET_SKIN_THEME = exports.DEFAULT_RALPH_LOOP_STUCK_WORKING_THRESHOLD_MS = exports.DEFAULT_RALPH_LOOP_FOLLOW_UP_COOLDOWN_MS = exports.DEFAULT_RALPH_LOOP_NUDGE_COOLDOWN_MS = exports.DEFAULT_RALPH_LOOP_IDLE_WITH_WORK_THRESHOLD_MS = exports.DEFAULT_RALPH_LOOP_INTERVAL_MS = exports.FORM_METHODS = void 0;
|
|
37
|
+
exports.resolvePinetMeshAuth = resolvePinetMeshAuth;
|
|
38
|
+
exports.loadSettings = loadSettings;
|
|
39
|
+
exports.buildAllowlist = buildAllowlist;
|
|
40
|
+
exports.isUserAllowed = isUserAllowed;
|
|
41
|
+
exports.getSqliteJournalMode = getSqliteJournalMode;
|
|
42
|
+
exports.isSqliteWalEnabled = isSqliteWalEnabled;
|
|
43
|
+
exports.buildSqliteWalFallbackWarning = buildSqliteWalFallbackWarning;
|
|
44
|
+
exports.formatInboxMessages = formatInboxMessages;
|
|
45
|
+
exports.isTerminalPinetStandDownMessage = isTerminalPinetStandDownMessage;
|
|
46
|
+
exports.formatPinetInboxMessages = formatPinetInboxMessages;
|
|
47
|
+
exports.parsePinetControlCommand = parsePinetControlCommand;
|
|
48
|
+
exports.queuePinetRemoteControl = queuePinetRemoteControl;
|
|
49
|
+
exports.finishPinetRemoteControl = finishPinetRemoteControl;
|
|
50
|
+
exports.reloadPinetRuntimeSafely = reloadPinetRuntimeSafely;
|
|
51
|
+
exports.getPinetControlCommandFromText = getPinetControlCommandFromText;
|
|
52
|
+
exports.buildPinetControlMetadata = buildPinetControlMetadata;
|
|
53
|
+
exports.buildPinetControlMessage = buildPinetControlMessage;
|
|
54
|
+
exports.normalizeOutgoingPinetControlMessage = normalizeOutgoingPinetControlMessage;
|
|
55
|
+
exports.buildPinetSkinMetadata = buildPinetSkinMetadata;
|
|
56
|
+
exports.extractPinetSkinUpdate = extractPinetSkinUpdate;
|
|
57
|
+
exports.extractPinetControlCommand = extractPinetControlCommand;
|
|
58
|
+
exports.buildSlackRequest = buildSlackRequest;
|
|
59
|
+
exports.createAbortError = createAbortError;
|
|
60
|
+
exports.isAbortError = isAbortError;
|
|
61
|
+
exports.abortableDelay = abortableDelay;
|
|
62
|
+
exports.createAbortableOperationTracker = createAbortableOperationTracker;
|
|
63
|
+
exports.stripBotMention = stripBotMention;
|
|
64
|
+
exports.isChannelId = isChannelId;
|
|
65
|
+
exports.shortenPath = shortenPath;
|
|
66
|
+
exports.extractAgentCapabilities = extractAgentCapabilities;
|
|
67
|
+
exports.buildAgentCapabilityTags = buildAgentCapabilityTags;
|
|
68
|
+
exports.buildAgentDisplayInfo = buildAgentDisplayInfo;
|
|
69
|
+
exports.rankAgentsForRouting = rankAgentsForRouting;
|
|
70
|
+
exports.evaluateRalphLoopCycle = evaluateRalphLoopCycle;
|
|
71
|
+
exports.rewriteRalphLoopGhostAnomalies = rewriteRalphLoopGhostAnomalies;
|
|
72
|
+
exports.buildRalphLoopNudgeMessage = buildRalphLoopNudgeMessage;
|
|
73
|
+
exports.buildRalphLoopAnomalySignature = buildRalphLoopAnomalySignature;
|
|
74
|
+
exports.buildRalphLoopStatusMessage = buildRalphLoopStatusMessage;
|
|
75
|
+
exports.shouldDeliverRalphLoopFollowUp = shouldDeliverRalphLoopFollowUp;
|
|
76
|
+
exports.buildRalphLoopFollowUpMessage = buildRalphLoopFollowUpMessage;
|
|
77
|
+
exports.buildRalphLoopCycleNotifications = buildRalphLoopCycleNotifications;
|
|
78
|
+
exports.buildBrokerPromptGuidelines = buildBrokerPromptGuidelines;
|
|
79
|
+
exports.buildWorkerPromptGuidelines = buildWorkerPromptGuidelines;
|
|
80
|
+
exports.buildPinetSkinPromptGuideline = buildPinetSkinPromptGuideline;
|
|
81
|
+
exports.buildIdentityReplyGuidelines = buildIdentityReplyGuidelines;
|
|
82
|
+
exports.resolveAgentPersonality = resolveAgentPersonality;
|
|
83
|
+
exports.buildAgentPersonalityGuidelines = buildAgentPersonalityGuidelines;
|
|
84
|
+
exports.resolvePersistedAgentIdentity = resolvePersistedAgentIdentity;
|
|
85
|
+
exports.buildAgentStableId = buildAgentStableId;
|
|
86
|
+
exports.resolveAgentStableId = resolveAgentStableId;
|
|
87
|
+
exports.isLikelyLocalSubagentContext = isLikelyLocalSubagentContext;
|
|
88
|
+
exports.isDirectMessageChannel = isDirectMessageChannel;
|
|
89
|
+
exports.syncFollowerInboxEntries = syncFollowerInboxEntries;
|
|
90
|
+
exports.syncBrokerInboxEntries = syncBrokerInboxEntries;
|
|
91
|
+
exports.resolveFollowerThreadChannel = resolveFollowerThreadChannel;
|
|
92
|
+
exports.getFollowerReconnectUiUpdate = getFollowerReconnectUiUpdate;
|
|
93
|
+
exports.agentOwnsThread = agentOwnsThread;
|
|
94
|
+
exports.normalizeOwnedThreads = normalizeOwnedThreads;
|
|
95
|
+
exports.getFollowerOwnedThreadClaims = getFollowerOwnedThreadClaims;
|
|
96
|
+
exports.trackBrokerInboundThread = trackBrokerInboundThread;
|
|
97
|
+
exports.formatAgentList = formatAgentList;
|
|
98
|
+
exports.buildPinetOwnerToken = buildPinetOwnerToken;
|
|
99
|
+
exports.generateAgentName = generateAgentName;
|
|
100
|
+
exports.normalizePinetSkinTheme = normalizePinetSkinTheme;
|
|
101
|
+
exports.buildPinetSkinAssignment = buildPinetSkinAssignment;
|
|
102
|
+
exports.resolveAgentIdentity = resolveAgentIdentity;
|
|
103
|
+
exports.alignAgentIdentityToRole = alignAgentIdentityToRole;
|
|
104
|
+
exports.resolveRuntimeAgentIdentity = resolveRuntimeAgentIdentity;
|
|
105
|
+
exports.normalizeThreadConfirmationState = normalizeThreadConfirmationState;
|
|
106
|
+
exports.isThreadConfirmationStateEmpty = isThreadConfirmationStateEmpty;
|
|
107
|
+
exports.confirmationRequestMatches = confirmationRequestMatches;
|
|
108
|
+
exports.consumeMatchingConfirmationRequest = consumeMatchingConfirmationRequest;
|
|
109
|
+
exports.registerThreadConfirmationRequest = registerThreadConfirmationRequest;
|
|
110
|
+
exports.callSlackAPI = callSlackAPI;
|
|
111
|
+
exports.isRalphNudgeEntry = isRalphNudgeEntry;
|
|
112
|
+
exports.isAgentToAgentEntry = isAgentToAgentEntry;
|
|
113
|
+
exports.partitionFollowerInboxEntries = partitionFollowerInboxEntries;
|
|
114
|
+
const fs = __importStar(require("node:fs"));
|
|
115
|
+
const os = __importStar(require("node:os"));
|
|
116
|
+
const path = __importStar(require("node:path"));
|
|
117
|
+
const guardrails_js_1 = require("./guardrails.js");
|
|
118
|
+
function normalizeOptionalSetting(value) {
|
|
119
|
+
const trimmed = value?.trim();
|
|
120
|
+
return trimmed && trimmed.length > 0 ? trimmed : null;
|
|
121
|
+
}
|
|
122
|
+
function resolvePinetMeshAuth(settings, env = process.env) {
|
|
123
|
+
const settingsMeshSecret = normalizeOptionalSetting(settings.meshSecret);
|
|
124
|
+
const settingsMeshSecretPath = normalizeOptionalSetting(settings.meshSecretPath);
|
|
125
|
+
if (settingsMeshSecret || settingsMeshSecretPath) {
|
|
126
|
+
return {
|
|
127
|
+
meshSecret: settingsMeshSecret,
|
|
128
|
+
meshSecretPath: settingsMeshSecret ? null : settingsMeshSecretPath,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const envMeshSecret = normalizeOptionalSetting(env.PINET_MESH_SECRET);
|
|
132
|
+
const envMeshSecretPath = normalizeOptionalSetting(env.PINET_MESH_SECRET_PATH);
|
|
133
|
+
return {
|
|
134
|
+
meshSecret: envMeshSecret,
|
|
135
|
+
meshSecretPath: envMeshSecret ? null : envMeshSecretPath,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function loadSettings(settingsPath) {
|
|
139
|
+
const p = settingsPath ?? path.join(os.homedir(), ".pi", "agent", "settings.json");
|
|
140
|
+
try {
|
|
141
|
+
const content = fs.readFileSync(p, "utf-8");
|
|
142
|
+
const parsed = JSON.parse(content);
|
|
143
|
+
return parsed["slack-bridge"] ?? {};
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return {};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// ─── Allowlist ───────────────────────────────────────────
|
|
150
|
+
function buildAllowlist(settings, envVar) {
|
|
151
|
+
if (settings.allowedUsers && settings.allowedUsers.length > 0) {
|
|
152
|
+
return new Set(settings.allowedUsers);
|
|
153
|
+
}
|
|
154
|
+
if (envVar) {
|
|
155
|
+
return new Set(envVar
|
|
156
|
+
.split(",")
|
|
157
|
+
.map((id) => id.trim())
|
|
158
|
+
.filter(Boolean));
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
function isUserAllowed(allowlist, userId) {
|
|
163
|
+
return allowlist === null || allowlist.has(userId);
|
|
164
|
+
}
|
|
165
|
+
function getSqliteJournalMode(result) {
|
|
166
|
+
const mode = result?.journal_mode?.trim().toLowerCase();
|
|
167
|
+
return mode && mode.length > 0 ? mode : "unknown";
|
|
168
|
+
}
|
|
169
|
+
function isSqliteWalEnabled(result) {
|
|
170
|
+
return getSqliteJournalMode(result) === "wal";
|
|
171
|
+
}
|
|
172
|
+
function buildSqliteWalFallbackWarning(component, result) {
|
|
173
|
+
return `[${component}] SQLite WAL mode not available, using ${getSqliteJournalMode(result)} journal mode fallback`;
|
|
174
|
+
}
|
|
175
|
+
function formatInboxMetadata(metadata) {
|
|
176
|
+
if (!metadata || Object.keys(metadata).length === 0)
|
|
177
|
+
return "";
|
|
178
|
+
if (metadata.kind === "slack_block_action") {
|
|
179
|
+
return ` | metadata=${JSON.stringify({
|
|
180
|
+
kind: metadata.kind,
|
|
181
|
+
actionId: metadata.actionId ?? null,
|
|
182
|
+
blockId: metadata.blockId ?? null,
|
|
183
|
+
value: metadata.value ?? null,
|
|
184
|
+
parsedValue: metadata.parsedValue ?? null,
|
|
185
|
+
})}`;
|
|
186
|
+
}
|
|
187
|
+
return "";
|
|
188
|
+
}
|
|
189
|
+
function formatInboxMessages(messages, userNames) {
|
|
190
|
+
const lines = messages.map((m) => {
|
|
191
|
+
const n = userNames.get(m.userId) ?? m.userId;
|
|
192
|
+
const metadataSuffix = formatInboxMetadata(m.metadata);
|
|
193
|
+
if (m.isChannelMention) {
|
|
194
|
+
return `[thread ${m.threadTs}] (channel mention in <#${m.channel}>) ${n}: ${m.text}${metadataSuffix}`;
|
|
195
|
+
}
|
|
196
|
+
return `[thread ${m.threadTs}] ${n}: ${m.text}${metadataSuffix}`;
|
|
197
|
+
});
|
|
198
|
+
return `New Slack messages:\n${lines.join("\n")}\n\nACK briefly, do the work, report blockers immediately, report the outcome when done.`;
|
|
199
|
+
}
|
|
200
|
+
function getPinetSenderLabel(message) {
|
|
201
|
+
const senderId = message.sender?.trim() ?? "";
|
|
202
|
+
const senderAgent = typeof message.metadata?.senderAgent === "string" ? message.metadata.senderAgent.trim() : "";
|
|
203
|
+
if (senderId && senderAgent && senderAgent !== senderId) {
|
|
204
|
+
return `${senderId} (${senderAgent})`;
|
|
205
|
+
}
|
|
206
|
+
return senderId || senderAgent || "unknown-agent";
|
|
207
|
+
}
|
|
208
|
+
const PINET_TERMINAL_STAND_DOWN_PATTERNS = [
|
|
209
|
+
/\bno further repl(?:y|ies) (?:are|is) needed\b/i,
|
|
210
|
+
/\bno further acknowledg(?:ement|ements) (?:are|is) needed\b/i,
|
|
211
|
+
/\bno reply is needed\b/i,
|
|
212
|
+
/\bhard stop on this [^.\n]*thread\b/i,
|
|
213
|
+
/\bno more work is needed\b/i,
|
|
214
|
+
/\bstand down\b/i,
|
|
215
|
+
/\bstay free(?:\/| and )quiet\b/i,
|
|
216
|
+
/\bstay quiet(?:\/| and )free\b/i,
|
|
217
|
+
/\bthread is already satisfied\b/i,
|
|
218
|
+
/\bunless I (?:assign|ask for) (?:a )?(?:genuinely )?new task\b/i,
|
|
219
|
+
];
|
|
220
|
+
const PINET_ACTIONABLE_TASK_PATTERNS = [
|
|
221
|
+
/\bnew [a-z-]+ lane\b/i,
|
|
222
|
+
/\bissue:\b/i,
|
|
223
|
+
/\btask:\b/i,
|
|
224
|
+
/\bworktree setup:\b/i,
|
|
225
|
+
/\bplease ACK\/work\/ask\/report\b/i,
|
|
226
|
+
];
|
|
227
|
+
function isTerminalPinetStandDownMessage(body) {
|
|
228
|
+
const normalized = body?.replace(/\s+/g, " ").trim() ?? "";
|
|
229
|
+
if (!normalized) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
const hasTerminalCue = PINET_TERMINAL_STAND_DOWN_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
233
|
+
if (!hasTerminalCue) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
return !PINET_ACTIONABLE_TASK_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
237
|
+
}
|
|
238
|
+
function formatPinetInboxMessages(entries) {
|
|
239
|
+
const annotatedEntries = entries.map((entry) => ({
|
|
240
|
+
entry,
|
|
241
|
+
terminalStandDown: isTerminalPinetStandDownMessage(entry.message.body),
|
|
242
|
+
}));
|
|
243
|
+
const lines = annotatedEntries.map(({ entry, terminalStandDown }) => {
|
|
244
|
+
const threadTs = entry.message.threadId ?? "";
|
|
245
|
+
const sender = getPinetSenderLabel(entry.message);
|
|
246
|
+
const standDownSuffix = terminalStandDown ? " [terminal stand-down]" : "";
|
|
247
|
+
return `[thread ${threadTs}] ${sender}${standDownSuffix}: ${entry.message.body ?? ""}`;
|
|
248
|
+
});
|
|
249
|
+
const hasTerminalStandDown = annotatedEntries.some((entry) => entry.terminalStandDown);
|
|
250
|
+
const hasActionableWork = annotatedEntries.some((entry) => !entry.terminalStandDown);
|
|
251
|
+
const guidance = hasTerminalStandDown
|
|
252
|
+
? hasActionableWork
|
|
253
|
+
? "Reply via pinet_message for actionable work only. For messages marked [terminal stand-down], do NOT acknowledge or reply unless you have a real blocker or materially new finding. For new tasks, ACK briefly, do the work, report blockers immediately, report the outcome when done."
|
|
254
|
+
: "Reply via pinet_message only if you have a real blocker or materially new finding. Treat messages marked [terminal stand-down] as closed; do NOT send another acknowledgement."
|
|
255
|
+
: "Reply via pinet_message. ACK briefly, do the work, report blockers immediately, report the outcome when done.";
|
|
256
|
+
return `New Pinet messages:\n${lines.join("\n")}\n\n${guidance}`;
|
|
257
|
+
}
|
|
258
|
+
function parsePinetControlCommand(value) {
|
|
259
|
+
return value === "reload" || value === "exit" ? value : null;
|
|
260
|
+
}
|
|
261
|
+
function queuePinetRemoteControl(state, command) {
|
|
262
|
+
if (!state.currentCommand) {
|
|
263
|
+
return {
|
|
264
|
+
currentCommand: command,
|
|
265
|
+
queuedCommand: state.queuedCommand,
|
|
266
|
+
accepted: true,
|
|
267
|
+
shouldStartNow: true,
|
|
268
|
+
status: "start",
|
|
269
|
+
scheduledCommand: command,
|
|
270
|
+
ackDisposition: "immediate",
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if (state.currentCommand === "exit") {
|
|
274
|
+
return {
|
|
275
|
+
currentCommand: state.currentCommand,
|
|
276
|
+
queuedCommand: state.queuedCommand,
|
|
277
|
+
accepted: true,
|
|
278
|
+
shouldStartNow: false,
|
|
279
|
+
status: "covered",
|
|
280
|
+
scheduledCommand: state.currentCommand,
|
|
281
|
+
ackDisposition: "immediate",
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const queuedCommand = state.queuedCommand === "exit" || command === "exit"
|
|
285
|
+
? "exit"
|
|
286
|
+
: (state.queuedCommand ?? command);
|
|
287
|
+
const status = queuedCommand === state.queuedCommand ? "covered" : "queued";
|
|
288
|
+
return {
|
|
289
|
+
currentCommand: state.currentCommand,
|
|
290
|
+
queuedCommand,
|
|
291
|
+
accepted: true,
|
|
292
|
+
shouldStartNow: false,
|
|
293
|
+
status,
|
|
294
|
+
scheduledCommand: queuedCommand,
|
|
295
|
+
ackDisposition: "on_start",
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function finishPinetRemoteControl(state) {
|
|
299
|
+
return {
|
|
300
|
+
currentCommand: state.queuedCommand,
|
|
301
|
+
queuedCommand: null,
|
|
302
|
+
nextCommand: state.queuedCommand,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
async function reloadPinetRuntimeSafely(reloader) {
|
|
306
|
+
const role = reloader.getCurrentRole();
|
|
307
|
+
if (!role) {
|
|
308
|
+
throw new Error("Pinet is not running.");
|
|
309
|
+
}
|
|
310
|
+
const snapshot = reloader.snapshotState();
|
|
311
|
+
try {
|
|
312
|
+
reloader.refreshState();
|
|
313
|
+
await reloader.validateRefreshedState();
|
|
314
|
+
}
|
|
315
|
+
catch (validationErr) {
|
|
316
|
+
reloader.restoreState(snapshot);
|
|
317
|
+
throw validationErr;
|
|
318
|
+
}
|
|
319
|
+
await reloader.stopRuntime();
|
|
320
|
+
try {
|
|
321
|
+
await reloader.startRuntime(role);
|
|
322
|
+
}
|
|
323
|
+
catch (reloadErr) {
|
|
324
|
+
reloader.restoreState(snapshot);
|
|
325
|
+
try {
|
|
326
|
+
await reloader.startRuntime(role);
|
|
327
|
+
}
|
|
328
|
+
catch (rollbackErr) {
|
|
329
|
+
const reloadMessage = reloadErr instanceof Error ? reloadErr.message : String(reloadErr);
|
|
330
|
+
const rollbackMessage = rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr);
|
|
331
|
+
throw new Error(`Reload failed: ${reloadMessage}. Rollback to the previous runtime also failed: ${rollbackMessage}`);
|
|
332
|
+
}
|
|
333
|
+
const reloadMessage = reloadErr instanceof Error ? reloadErr.message : String(reloadErr);
|
|
334
|
+
throw new Error(`Reload failed: ${reloadMessage}. Restored the previous runtime.`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function parsePinetControlEnvelope(value) {
|
|
338
|
+
if (typeof value !== "object" || value === null)
|
|
339
|
+
return null;
|
|
340
|
+
const record = value;
|
|
341
|
+
if (record.type !== "pinet:control")
|
|
342
|
+
return null;
|
|
343
|
+
return parsePinetControlCommand(record.action);
|
|
344
|
+
}
|
|
345
|
+
function parseStructuredPinetControlCommandFromText(text) {
|
|
346
|
+
const trimmed = text?.trim();
|
|
347
|
+
if (!trimmed)
|
|
348
|
+
return null;
|
|
349
|
+
try {
|
|
350
|
+
return parsePinetControlEnvelope(JSON.parse(trimmed));
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function parseLegacyPinetControlCommandFromText(text) {
|
|
357
|
+
const trimmed = text?.trim();
|
|
358
|
+
if (trimmed === "/reload")
|
|
359
|
+
return "reload";
|
|
360
|
+
if (trimmed === "/exit")
|
|
361
|
+
return "exit";
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
function getPinetControlCommandFromText(text) {
|
|
365
|
+
return (parseStructuredPinetControlCommandFromText(text) ?? parseLegacyPinetControlCommandFromText(text));
|
|
366
|
+
}
|
|
367
|
+
function buildPinetControlMetadata(command) {
|
|
368
|
+
return { type: "pinet:control", action: command };
|
|
369
|
+
}
|
|
370
|
+
function buildPinetControlMessage(command) {
|
|
371
|
+
return JSON.stringify(buildPinetControlMetadata(command));
|
|
372
|
+
}
|
|
373
|
+
function normalizeOutgoingPinetControlMessage(body, metadata) {
|
|
374
|
+
const command = getPinetControlCommandFromText(body);
|
|
375
|
+
if (!command)
|
|
376
|
+
return null;
|
|
377
|
+
return {
|
|
378
|
+
body: buildPinetControlMessage(command),
|
|
379
|
+
metadata: {
|
|
380
|
+
...(metadata ?? {}),
|
|
381
|
+
...buildPinetControlMetadata(command),
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function buildPinetSkinMetadata(update) {
|
|
386
|
+
return {
|
|
387
|
+
kind: "pinet_skin",
|
|
388
|
+
theme: update.theme,
|
|
389
|
+
name: update.name,
|
|
390
|
+
emoji: update.emoji,
|
|
391
|
+
personality: update.personality,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function extractPinetSkinUpdate(message) {
|
|
395
|
+
const metadata = message.metadata ?? {};
|
|
396
|
+
const isAgentToAgent = metadata.a2a === true ||
|
|
397
|
+
(typeof message.threadId === "string" && message.threadId.startsWith("a2a:"));
|
|
398
|
+
if (!isAgentToAgent || metadata.kind !== "pinet_skin")
|
|
399
|
+
return null;
|
|
400
|
+
const theme = typeof metadata.theme === "string" ? metadata.theme.trim() : "";
|
|
401
|
+
const name = typeof metadata.name === "string" ? metadata.name.trim() : "";
|
|
402
|
+
const emoji = typeof metadata.emoji === "string" ? metadata.emoji.trim() : "";
|
|
403
|
+
const personality = typeof metadata.personality === "string" ? metadata.personality.trim() : "";
|
|
404
|
+
if (!theme || !name || !emoji || !personality)
|
|
405
|
+
return null;
|
|
406
|
+
return { theme, name, emoji, personality };
|
|
407
|
+
}
|
|
408
|
+
function extractPinetControlCommand(message) {
|
|
409
|
+
const metadata = message.metadata ?? {};
|
|
410
|
+
const isAgentToAgent = metadata.a2a === true ||
|
|
411
|
+
(typeof message.threadId === "string" && message.threadId.startsWith("a2a:"));
|
|
412
|
+
if (!isAgentToAgent)
|
|
413
|
+
return null;
|
|
414
|
+
const metadataCommand = parsePinetControlEnvelope(metadata) ??
|
|
415
|
+
(metadata.kind === "pinet_control" ? parsePinetControlCommand(metadata.command) : null);
|
|
416
|
+
if (metadataCommand)
|
|
417
|
+
return metadataCommand;
|
|
418
|
+
// Backward-compatible fallback for structured JSON or exact slash commands sent over a2a flows.
|
|
419
|
+
return getPinetControlCommandFromText(message.body);
|
|
420
|
+
}
|
|
421
|
+
// ─── Slack API encoding ──────────────────────────────────
|
|
422
|
+
exports.FORM_METHODS = new Set([
|
|
423
|
+
"auth.test",
|
|
424
|
+
"users.info",
|
|
425
|
+
"conversations.list",
|
|
426
|
+
"conversations.history",
|
|
427
|
+
"conversations.replies",
|
|
428
|
+
"conversations.info",
|
|
429
|
+
"apps.connections.open",
|
|
430
|
+
]);
|
|
431
|
+
function buildSlackRequest(method, token, body) {
|
|
432
|
+
const headers = {
|
|
433
|
+
Authorization: `Bearer ${token}`,
|
|
434
|
+
};
|
|
435
|
+
let serialized;
|
|
436
|
+
const needsJson = !exports.FORM_METHODS.has(method);
|
|
437
|
+
if (body) {
|
|
438
|
+
if (needsJson) {
|
|
439
|
+
headers["Content-Type"] = "application/json; charset=utf-8";
|
|
440
|
+
serialized = JSON.stringify(body);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
444
|
+
serialized = new URLSearchParams(Object.entries(body).map(([k, v]) => [k, String(v)])).toString();
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
url: `https://slack.com/api/${method}`,
|
|
449
|
+
init: {
|
|
450
|
+
method: "POST",
|
|
451
|
+
headers,
|
|
452
|
+
...(serialized ? { body: serialized } : {}),
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
function createAbortError(message = "Operation aborted") {
|
|
457
|
+
const error = new Error(message);
|
|
458
|
+
error.name = "AbortError";
|
|
459
|
+
return error;
|
|
460
|
+
}
|
|
461
|
+
function isAbortError(error) {
|
|
462
|
+
return error instanceof Error && error.name === "AbortError";
|
|
463
|
+
}
|
|
464
|
+
function abortableDelay(ms, signal) {
|
|
465
|
+
if (signal.aborted) {
|
|
466
|
+
return Promise.reject(createAbortError());
|
|
467
|
+
}
|
|
468
|
+
return new Promise((resolve, reject) => {
|
|
469
|
+
const timer = setTimeout(() => {
|
|
470
|
+
signal.removeEventListener("abort", onAbort);
|
|
471
|
+
resolve();
|
|
472
|
+
}, ms);
|
|
473
|
+
function onAbort() {
|
|
474
|
+
clearTimeout(timer);
|
|
475
|
+
signal.removeEventListener("abort", onAbort);
|
|
476
|
+
reject(createAbortError());
|
|
477
|
+
}
|
|
478
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
function createAbortableOperationTracker() {
|
|
482
|
+
let aborting = false;
|
|
483
|
+
const controllers = new Set();
|
|
484
|
+
const operations = new Set();
|
|
485
|
+
return {
|
|
486
|
+
async run(operation) {
|
|
487
|
+
if (aborting) {
|
|
488
|
+
throw createAbortError("Operation rejected: shutdown in progress");
|
|
489
|
+
}
|
|
490
|
+
const controller = new AbortController();
|
|
491
|
+
const tracked = Promise.resolve().then(() => operation(controller.signal));
|
|
492
|
+
controllers.add(controller);
|
|
493
|
+
operations.add(tracked);
|
|
494
|
+
try {
|
|
495
|
+
return await tracked;
|
|
496
|
+
}
|
|
497
|
+
finally {
|
|
498
|
+
controllers.delete(controller);
|
|
499
|
+
operations.delete(tracked);
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
async abortAndWait() {
|
|
503
|
+
aborting = true;
|
|
504
|
+
for (const controller of controllers) {
|
|
505
|
+
controller.abort();
|
|
506
|
+
}
|
|
507
|
+
if (operations.size === 0)
|
|
508
|
+
return;
|
|
509
|
+
await Promise.allSettled(Array.from(operations));
|
|
510
|
+
},
|
|
511
|
+
isAborting() {
|
|
512
|
+
return aborting;
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
// ─── Mention stripping ───────────────────────────────────
|
|
517
|
+
function stripBotMention(text, botUserId) {
|
|
518
|
+
return text.replace(new RegExp(`<@${botUserId}>\\s*`, "g"), "").trim();
|
|
519
|
+
}
|
|
520
|
+
// ─── Channel ID detection ────────────────────────────────
|
|
521
|
+
function isChannelId(nameOrId) {
|
|
522
|
+
return /^[CGD][A-Z0-9]+$/.test(nameOrId);
|
|
523
|
+
}
|
|
524
|
+
const DEFAULT_AGENT_HEARTBEAT_TIMEOUT_MS = 15_000;
|
|
525
|
+
const DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS = 5_000;
|
|
526
|
+
function asRecord(value) {
|
|
527
|
+
return typeof value === "object" && value !== null ? value : null;
|
|
528
|
+
}
|
|
529
|
+
function asString(value) {
|
|
530
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
531
|
+
}
|
|
532
|
+
function asStringArray(value) {
|
|
533
|
+
if (!Array.isArray(value))
|
|
534
|
+
return undefined;
|
|
535
|
+
const strings = value.map(asString).filter((item) => Boolean(item));
|
|
536
|
+
return strings.length > 0 ? strings : undefined;
|
|
537
|
+
}
|
|
538
|
+
function parseIsoMs(value) {
|
|
539
|
+
if (!value)
|
|
540
|
+
return null;
|
|
541
|
+
const parsed = Date.parse(value);
|
|
542
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
543
|
+
}
|
|
544
|
+
function formatAge(ms) {
|
|
545
|
+
if (ms == null || !Number.isFinite(ms))
|
|
546
|
+
return null;
|
|
547
|
+
const seconds = Math.max(0, Math.round(ms / 1000));
|
|
548
|
+
if (seconds < 60)
|
|
549
|
+
return `${seconds}s ago`;
|
|
550
|
+
const minutes = Math.round(seconds / 60);
|
|
551
|
+
if (minutes < 60)
|
|
552
|
+
return `${minutes}m ago`;
|
|
553
|
+
const hours = Math.round(minutes / 60);
|
|
554
|
+
if (hours < 48)
|
|
555
|
+
return `${hours}h ago`;
|
|
556
|
+
const days = Math.round(hours / 24);
|
|
557
|
+
return `${days}d ago`;
|
|
558
|
+
}
|
|
559
|
+
function formatLease(expiresAt, nowMs) {
|
|
560
|
+
const expiresMs = parseIsoMs(expiresAt);
|
|
561
|
+
if (expiresMs == null)
|
|
562
|
+
return null;
|
|
563
|
+
const deltaMs = expiresMs - nowMs;
|
|
564
|
+
if (deltaMs >= 0) {
|
|
565
|
+
const seconds = Math.max(0, Math.round(deltaMs / 1000));
|
|
566
|
+
if (seconds < 60)
|
|
567
|
+
return `lease in ${seconds}s`;
|
|
568
|
+
const minutes = Math.round(seconds / 60);
|
|
569
|
+
if (minutes < 60)
|
|
570
|
+
return `lease in ${minutes}m`;
|
|
571
|
+
const hours = Math.round(minutes / 60);
|
|
572
|
+
return `lease in ${hours}h`;
|
|
573
|
+
}
|
|
574
|
+
const elapsedSeconds = Math.round(Math.abs(deltaMs) / 1000);
|
|
575
|
+
if (elapsedSeconds < 60)
|
|
576
|
+
return `lease expired ${elapsedSeconds}s ago`;
|
|
577
|
+
const minutes = Math.round(elapsedSeconds / 60);
|
|
578
|
+
if (minutes < 60)
|
|
579
|
+
return `lease expired ${minutes}m ago`;
|
|
580
|
+
const hours = Math.round(minutes / 60);
|
|
581
|
+
return `lease expired ${hours}h ago`;
|
|
582
|
+
}
|
|
583
|
+
function normalizeTaskTokens(task) {
|
|
584
|
+
if (!task)
|
|
585
|
+
return [];
|
|
586
|
+
return task
|
|
587
|
+
.toLowerCase()
|
|
588
|
+
.split(/[^a-z0-9]+/)
|
|
589
|
+
.map((token) => token.trim())
|
|
590
|
+
.filter((token) => token.length >= 3);
|
|
591
|
+
}
|
|
592
|
+
function compareRouting(left, right) {
|
|
593
|
+
if ((left.routingScore ?? 0) !== (right.routingScore ?? 0)) {
|
|
594
|
+
return (right.routingScore ?? 0) - (left.routingScore ?? 0);
|
|
595
|
+
}
|
|
596
|
+
const leftGhost = left.ghost ? 1 : 0;
|
|
597
|
+
const rightGhost = right.ghost ? 1 : 0;
|
|
598
|
+
if (leftGhost !== rightGhost)
|
|
599
|
+
return leftGhost - rightGhost;
|
|
600
|
+
if (left.status !== right.status)
|
|
601
|
+
return left.status === "idle" ? -1 : 1;
|
|
602
|
+
return left.name.localeCompare(right.name);
|
|
603
|
+
}
|
|
604
|
+
function shortenPath(p, homedir) {
|
|
605
|
+
if (p === homedir)
|
|
606
|
+
return "~";
|
|
607
|
+
const prefix = homedir.endsWith("/") ? homedir : homedir + "/";
|
|
608
|
+
if (p.startsWith(prefix)) {
|
|
609
|
+
return "~/" + p.slice(prefix.length);
|
|
610
|
+
}
|
|
611
|
+
return p;
|
|
612
|
+
}
|
|
613
|
+
function extractAgentCapabilities(metadata) {
|
|
614
|
+
const record = asRecord(metadata);
|
|
615
|
+
const capabilitiesRecord = asRecord(record?.capabilities);
|
|
616
|
+
return {
|
|
617
|
+
repo: asString(capabilitiesRecord?.repo) ?? asString(record?.repo),
|
|
618
|
+
repoRoot: asString(capabilitiesRecord?.repoRoot) ?? asString(record?.repoRoot),
|
|
619
|
+
branch: asString(capabilitiesRecord?.branch) ?? asString(record?.branch),
|
|
620
|
+
role: asString(capabilitiesRecord?.role) ?? asString(record?.role),
|
|
621
|
+
tools: asStringArray(capabilitiesRecord?.tools),
|
|
622
|
+
tags: asStringArray(capabilitiesRecord?.tags),
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function buildAgentCapabilityTags(capabilities) {
|
|
626
|
+
const tags = new Set();
|
|
627
|
+
if (capabilities.role)
|
|
628
|
+
tags.add(`role:${capabilities.role}`);
|
|
629
|
+
if (capabilities.repo)
|
|
630
|
+
tags.add(`repo:${capabilities.repo}`);
|
|
631
|
+
if (capabilities.branch)
|
|
632
|
+
tags.add(`branch:${capabilities.branch}`);
|
|
633
|
+
for (const tool of capabilities.tools ?? []) {
|
|
634
|
+
tags.add(`tool:${tool}`);
|
|
635
|
+
}
|
|
636
|
+
for (const tag of capabilities.tags ?? []) {
|
|
637
|
+
tags.add(tag);
|
|
638
|
+
}
|
|
639
|
+
return [...tags];
|
|
640
|
+
}
|
|
641
|
+
function buildAgentDisplayInfo(agent, options = {}) {
|
|
642
|
+
const nowMs = options.now ?? Date.now();
|
|
643
|
+
const heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? DEFAULT_AGENT_HEARTBEAT_TIMEOUT_MS;
|
|
644
|
+
const heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS;
|
|
645
|
+
const heartbeatMs = parseIsoMs(agent.lastHeartbeat);
|
|
646
|
+
const heartbeatAgeMs = heartbeatMs == null ? null : Math.max(0, nowMs - heartbeatMs);
|
|
647
|
+
const computedLeaseExpiresAt = agent.resumableUntil ??
|
|
648
|
+
(heartbeatMs == null ? null : new Date(heartbeatMs + heartbeatTimeoutMs).toISOString());
|
|
649
|
+
const disconnectedAtMs = parseIsoMs(agent.disconnectedAt);
|
|
650
|
+
const resumableUntilMs = parseIsoMs(agent.resumableUntil);
|
|
651
|
+
const staleThresholdMs = Math.max(heartbeatIntervalMs * 2, heartbeatTimeoutMs - heartbeatIntervalMs);
|
|
652
|
+
let health = "healthy";
|
|
653
|
+
if (disconnectedAtMs != null && resumableUntilMs != null && resumableUntilMs > nowMs) {
|
|
654
|
+
health = "resumable";
|
|
655
|
+
}
|
|
656
|
+
else if (disconnectedAtMs != null ||
|
|
657
|
+
(heartbeatAgeMs != null && heartbeatAgeMs > heartbeatTimeoutMs)) {
|
|
658
|
+
health = "ghost";
|
|
659
|
+
}
|
|
660
|
+
else if (heartbeatAgeMs != null && heartbeatAgeMs > staleThresholdMs) {
|
|
661
|
+
health = "stale";
|
|
662
|
+
}
|
|
663
|
+
const metadata = asRecord(agent.metadata);
|
|
664
|
+
const capabilities = extractAgentCapabilities(metadata);
|
|
665
|
+
const capabilityTags = buildAgentCapabilityTags(capabilities);
|
|
666
|
+
const idleSinceMs = parseIsoMs(agent.idleSince);
|
|
667
|
+
const lastActivityMs = parseIsoMs(agent.lastActivity);
|
|
668
|
+
const idleDurationMs = idleSinceMs == null ? null : Math.max(0, nowMs - idleSinceMs);
|
|
669
|
+
const lastActivityAgeMs = lastActivityMs == null ? null : Math.max(0, nowMs - lastActivityMs);
|
|
670
|
+
return {
|
|
671
|
+
emoji: agent.emoji,
|
|
672
|
+
name: agent.name,
|
|
673
|
+
id: agent.id,
|
|
674
|
+
...(agent.pid != null ? { pid: agent.pid } : {}),
|
|
675
|
+
status: agent.status,
|
|
676
|
+
metadata: metadata
|
|
677
|
+
? {
|
|
678
|
+
cwd: asString(metadata.cwd),
|
|
679
|
+
branch: asString(metadata.branch),
|
|
680
|
+
host: asString(metadata.host),
|
|
681
|
+
repo: asString(metadata.repo) ?? capabilities.repo,
|
|
682
|
+
role: asString(metadata.role) ?? capabilities.role,
|
|
683
|
+
skinTheme: asString(metadata.skinTheme),
|
|
684
|
+
personality: asString(metadata.personality),
|
|
685
|
+
capabilities,
|
|
686
|
+
}
|
|
687
|
+
: null,
|
|
688
|
+
lastHeartbeat: agent.lastHeartbeat,
|
|
689
|
+
leaseExpiresAt: computedLeaseExpiresAt,
|
|
690
|
+
heartbeatAgeMs,
|
|
691
|
+
heartbeatSummary: formatAge(heartbeatAgeMs),
|
|
692
|
+
leaseSummary: formatLease(computedLeaseExpiresAt, nowMs),
|
|
693
|
+
health,
|
|
694
|
+
ghost: health === "ghost",
|
|
695
|
+
stuck: false,
|
|
696
|
+
idleSince: agent.idleSince ?? null,
|
|
697
|
+
lastActivity: agent.lastActivity ?? null,
|
|
698
|
+
idleDuration: formatAge(idleDurationMs),
|
|
699
|
+
lastActivityAge: formatAge(lastActivityAgeMs),
|
|
700
|
+
capabilityTags,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
function rankAgentsForRouting(agents, hint) {
|
|
704
|
+
const requiredTools = new Set((hint.requiredTools ?? []).map((tool) => tool.toLowerCase()));
|
|
705
|
+
const taskTokens = normalizeTaskTokens(hint.task);
|
|
706
|
+
const ranked = agents.map((agent) => {
|
|
707
|
+
let score = 0;
|
|
708
|
+
const reasons = [];
|
|
709
|
+
const capabilities = agent.metadata?.capabilities ?? {};
|
|
710
|
+
const capabilityTags = agent.capabilityTags ?? [];
|
|
711
|
+
const searchable = [
|
|
712
|
+
agent.name,
|
|
713
|
+
agent.metadata?.repo,
|
|
714
|
+
capabilities.repo,
|
|
715
|
+
agent.metadata?.branch,
|
|
716
|
+
capabilities.branch,
|
|
717
|
+
agent.metadata?.role,
|
|
718
|
+
capabilities.role,
|
|
719
|
+
...capabilityTags,
|
|
720
|
+
...(capabilities.tools ?? []),
|
|
721
|
+
]
|
|
722
|
+
.filter((value) => Boolean(value))
|
|
723
|
+
.map((value) => value.toLowerCase());
|
|
724
|
+
if (agent.health === "ghost") {
|
|
725
|
+
score -= 1000;
|
|
726
|
+
reasons.push("ghost");
|
|
727
|
+
}
|
|
728
|
+
else if (agent.health === "resumable") {
|
|
729
|
+
score -= 200;
|
|
730
|
+
reasons.push("resumable");
|
|
731
|
+
}
|
|
732
|
+
else if (agent.health === "stale") {
|
|
733
|
+
score -= 20;
|
|
734
|
+
reasons.push("stale heartbeat");
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
score += 20;
|
|
738
|
+
reasons.push("healthy heartbeat");
|
|
739
|
+
}
|
|
740
|
+
if (agent.status === "idle") {
|
|
741
|
+
score += 10;
|
|
742
|
+
reasons.push("idle");
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
score += 2;
|
|
746
|
+
reasons.push("working");
|
|
747
|
+
}
|
|
748
|
+
const repo = (capabilities.repo ?? agent.metadata?.repo)?.toLowerCase();
|
|
749
|
+
const branch = (capabilities.branch ?? agent.metadata?.branch)?.toLowerCase();
|
|
750
|
+
const role = (capabilities.role ?? agent.metadata?.role)?.toLowerCase();
|
|
751
|
+
const tools = new Set((capabilities.tools ?? []).map((tool) => tool.toLowerCase()));
|
|
752
|
+
const lowerCapabilityTags = capabilityTags.map((tag) => tag.toLowerCase());
|
|
753
|
+
if (hint.repo && repo === hint.repo.toLowerCase()) {
|
|
754
|
+
score += 40;
|
|
755
|
+
reasons.push(`repo:${hint.repo}`);
|
|
756
|
+
}
|
|
757
|
+
if (hint.branch && branch === hint.branch.toLowerCase()) {
|
|
758
|
+
score += 30;
|
|
759
|
+
reasons.push(`branch:${hint.branch}`);
|
|
760
|
+
}
|
|
761
|
+
if (hint.role && role === hint.role.toLowerCase()) {
|
|
762
|
+
score += 20;
|
|
763
|
+
reasons.push(`role:${hint.role}`);
|
|
764
|
+
}
|
|
765
|
+
if (requiredTools.size > 0) {
|
|
766
|
+
let matchedTools = 0;
|
|
767
|
+
for (const tool of requiredTools) {
|
|
768
|
+
if (tools.has(tool) || lowerCapabilityTags.includes(`tool:${tool}`)) {
|
|
769
|
+
matchedTools += 1;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (matchedTools > 0) {
|
|
773
|
+
score += matchedTools * 12;
|
|
774
|
+
reasons.push(`tools:${matchedTools}/${requiredTools.size}`);
|
|
775
|
+
}
|
|
776
|
+
if (matchedTools !== requiredTools.size) {
|
|
777
|
+
score -= (requiredTools.size - matchedTools) * 15;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
if (taskTokens.length > 0) {
|
|
781
|
+
const overlaps = taskTokens.filter((token) => searchable.some((value) => value.includes(token)));
|
|
782
|
+
if (overlaps.length > 0) {
|
|
783
|
+
score += overlaps.length * 3;
|
|
784
|
+
reasons.push(`task:${overlaps.slice(0, 3).join(",")}`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return {
|
|
788
|
+
...agent,
|
|
789
|
+
routingScore: score,
|
|
790
|
+
routingReasons: reasons,
|
|
791
|
+
};
|
|
792
|
+
});
|
|
793
|
+
return ranked.sort(compareRouting);
|
|
794
|
+
}
|
|
795
|
+
exports.DEFAULT_RALPH_LOOP_INTERVAL_MS = 30_000;
|
|
796
|
+
exports.DEFAULT_RALPH_LOOP_IDLE_WITH_WORK_THRESHOLD_MS = 60_000;
|
|
797
|
+
exports.DEFAULT_RALPH_LOOP_NUDGE_COOLDOWN_MS = 5 * 60_000;
|
|
798
|
+
exports.DEFAULT_RALPH_LOOP_FOLLOW_UP_COOLDOWN_MS = 60_000;
|
|
799
|
+
exports.DEFAULT_RALPH_LOOP_STUCK_WORKING_THRESHOLD_MS = 5 * 60_000;
|
|
800
|
+
function evaluateRalphLoopCycle(workloads, options = {}) {
|
|
801
|
+
const nowMs = options.now ?? Date.now();
|
|
802
|
+
const idleWithWorkThresholdMs = options.idleWithWorkThresholdMs ?? exports.DEFAULT_RALPH_LOOP_IDLE_WITH_WORK_THRESHOLD_MS;
|
|
803
|
+
const stuckWorkingThresholdMs = options.stuckWorkingThresholdMs ?? exports.DEFAULT_RALPH_LOOP_STUCK_WORKING_THRESHOLD_MS;
|
|
804
|
+
const pendingBacklogCount = options.pendingBacklogCount ?? 0;
|
|
805
|
+
const expectedMainBranch = options.expectedMainBranch ?? "main";
|
|
806
|
+
const brokerAgentId = options.brokerAgentId;
|
|
807
|
+
const anomalies = [];
|
|
808
|
+
const ghostAgentIds = [];
|
|
809
|
+
const nudgeAgentIds = [];
|
|
810
|
+
const idleDrainAgentIds = [];
|
|
811
|
+
const stuckAgentIds = [];
|
|
812
|
+
for (const workload of workloads) {
|
|
813
|
+
if (brokerAgentId && workload.id === brokerAgentId) {
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
const metadata = asRecord(workload.metadata);
|
|
817
|
+
const capabilities = extractAgentCapabilities(metadata);
|
|
818
|
+
const role = (capabilities.role ?? asString(metadata?.role) ?? "worker").toLowerCase();
|
|
819
|
+
if (role === "broker") {
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
const display = buildAgentDisplayInfo(workload, options);
|
|
823
|
+
if (display.health === "ghost") {
|
|
824
|
+
ghostAgentIds.push(workload.id);
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
const hasAssignedWork = workload.pendingInboxCount > 0 || workload.ownedThreadCount > 0;
|
|
828
|
+
const lastSeenMs = parseIsoMs(workload.lastSeen) ?? parseIsoMs(workload.lastHeartbeat);
|
|
829
|
+
const idleAgeMs = lastSeenMs == null ? null : Math.max(0, nowMs - lastSeenMs);
|
|
830
|
+
if (hasAssignedWork &&
|
|
831
|
+
workload.status === "idle" &&
|
|
832
|
+
display.health !== "resumable" &&
|
|
833
|
+
idleAgeMs != null &&
|
|
834
|
+
idleAgeMs >= idleWithWorkThresholdMs) {
|
|
835
|
+
nudgeAgentIds.push(workload.id);
|
|
836
|
+
anomalies.push(`${workload.name} idle with assigned work (${workload.pendingInboxCount} inbox, ${workload.ownedThreadCount} threads)`);
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
// Stuck detection: agent reports "working" but no activity for > threshold
|
|
840
|
+
if (workload.status === "working" && display.health === "healthy") {
|
|
841
|
+
const lastActivityMs = parseIsoMs(workload.lastActivity);
|
|
842
|
+
const activityAgeMs = lastActivityMs == null ? null : Math.max(0, nowMs - lastActivityMs);
|
|
843
|
+
if (activityAgeMs != null && activityAgeMs >= stuckWorkingThresholdMs) {
|
|
844
|
+
stuckAgentIds.push(workload.id);
|
|
845
|
+
const ageMinutes = Math.round(activityAgeMs / 60_000);
|
|
846
|
+
anomalies.push(`${workload.name} appears stuck (working with no activity for ${ageMinutes}m)`);
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (!hasAssignedWork && workload.status === "idle" && display.health === "healthy") {
|
|
851
|
+
idleDrainAgentIds.push(workload.id);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (ghostAgentIds.length > 0) {
|
|
855
|
+
anomalies.push(`ghost agents detected: ${ghostAgentIds.join(", ")}`);
|
|
856
|
+
}
|
|
857
|
+
if (pendingBacklogCount > 0 && idleDrainAgentIds.length > 0) {
|
|
858
|
+
anomalies.push(`pending backlog (${pendingBacklogCount}) with ${idleDrainAgentIds.length} idle worker${idleDrainAgentIds.length === 1 ? "" : "s"}`);
|
|
859
|
+
}
|
|
860
|
+
if (options.currentBranch && options.currentBranch !== expectedMainBranch) {
|
|
861
|
+
anomalies.push(`main checkout is on \`${options.currentBranch}\`, expected \`${expectedMainBranch}\``);
|
|
862
|
+
}
|
|
863
|
+
if (options.brokerHeartbeatActive === false) {
|
|
864
|
+
anomalies.push("broker heartbeat timer is not running");
|
|
865
|
+
}
|
|
866
|
+
if (options.brokerMaintenanceActive === false) {
|
|
867
|
+
anomalies.push("broker maintenance timer is not running");
|
|
868
|
+
}
|
|
869
|
+
if (stuckAgentIds.length > 0 && ghostAgentIds.length === 0) {
|
|
870
|
+
// Only report stuck if not already mixed with ghost anomalies
|
|
871
|
+
// (ghosts are more urgent)
|
|
872
|
+
}
|
|
873
|
+
return {
|
|
874
|
+
ghostAgentIds,
|
|
875
|
+
nudgeAgentIds,
|
|
876
|
+
idleDrainAgentIds,
|
|
877
|
+
stuckAgentIds,
|
|
878
|
+
anomalies,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
function rewriteRalphLoopGhostAnomalies(evaluation, previousGhostIds = []) {
|
|
882
|
+
const priorGhostIds = new Set(previousGhostIds);
|
|
883
|
+
const nextReportedGhostIds = [...evaluation.ghostAgentIds];
|
|
884
|
+
const newGhostIds = evaluation.ghostAgentIds.filter((id) => !priorGhostIds.has(id));
|
|
885
|
+
const currentGhostIds = new Set(evaluation.ghostAgentIds);
|
|
886
|
+
const clearedGhostIds = [...priorGhostIds].filter((id) => !currentGhostIds.has(id));
|
|
887
|
+
const nonGhostAnomalies = evaluation.anomalies.filter((anomaly) => !anomaly.startsWith("ghost agents detected:"));
|
|
888
|
+
const anomalies = [...nonGhostAnomalies];
|
|
889
|
+
if (newGhostIds.length > 0) {
|
|
890
|
+
anomalies.push(`NEW ghost agents detected: ${newGhostIds.join(", ")}`);
|
|
891
|
+
}
|
|
892
|
+
if (clearedGhostIds.length > 0) {
|
|
893
|
+
anomalies.push(`ghost agents cleared from registry: ${clearedGhostIds.join(", ")}`);
|
|
894
|
+
}
|
|
895
|
+
return {
|
|
896
|
+
evaluation: {
|
|
897
|
+
...evaluation,
|
|
898
|
+
anomalies,
|
|
899
|
+
},
|
|
900
|
+
nonGhostAnomalies,
|
|
901
|
+
newGhostIds,
|
|
902
|
+
clearedGhostIds,
|
|
903
|
+
nextReportedGhostIds,
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
function buildRalphLoopNudgeMessage(pendingInboxCount, ownedThreadCount, cycleStartedAt) {
|
|
907
|
+
const parts = [];
|
|
908
|
+
if (pendingInboxCount > 0) {
|
|
909
|
+
parts.push(`${pendingInboxCount} inbox item${pendingInboxCount === 1 ? "" : "s"}`);
|
|
910
|
+
}
|
|
911
|
+
if (ownedThreadCount > 0) {
|
|
912
|
+
parts.push(`${ownedThreadCount} claimed thread${ownedThreadCount === 1 ? "" : "s"}`);
|
|
913
|
+
}
|
|
914
|
+
const workload = parts.length > 0 ? parts.join(" and ") : "assigned work";
|
|
915
|
+
const prefix = cycleStartedAt ? `RALPH LOOP nudge (${cycleStartedAt})` : "RALPH LOOP nudge";
|
|
916
|
+
return `${prefix}: you appear idle but still have ${workload}. Please pick it up, post a status update, or release ownership so the broker can reassign it.`;
|
|
917
|
+
}
|
|
918
|
+
function buildRalphLoopAnomalySignature(evaluation) {
|
|
919
|
+
return evaluation.anomalies.join("|");
|
|
920
|
+
}
|
|
921
|
+
function buildRalphLoopStatusMessage(summary, cycleStartedAt) {
|
|
922
|
+
return `RALPH loop (${cycleStartedAt}): ${summary}`;
|
|
923
|
+
}
|
|
924
|
+
function shouldDeliverRalphLoopFollowUp(options) {
|
|
925
|
+
const now = options.now ?? Date.now();
|
|
926
|
+
const lastDeliveredAt = options.lastDeliveredAt ?? 0;
|
|
927
|
+
const cooldownMs = options.cooldownMs ?? exports.DEFAULT_RALPH_LOOP_FOLLOW_UP_COOLDOWN_MS;
|
|
928
|
+
const pending = options.pending ?? false;
|
|
929
|
+
const idle = options.idle ?? true;
|
|
930
|
+
if (!options.signature) {
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
if (pending || !idle) {
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
if (lastDeliveredAt > 0 && now - lastDeliveredAt < cooldownMs) {
|
|
937
|
+
return false;
|
|
938
|
+
}
|
|
939
|
+
return true;
|
|
940
|
+
}
|
|
941
|
+
function buildRalphLoopFollowUpMessage(evaluation, cycleStartedAt) {
|
|
942
|
+
if (evaluation.anomalies.length === 0) {
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
return [
|
|
946
|
+
"RALPH LOOP CYCLE:",
|
|
947
|
+
`Timestamp: ${cycleStartedAt}`,
|
|
948
|
+
...evaluation.anomalies.map((anomaly) => `- ${anomaly}`),
|
|
949
|
+
"",
|
|
950
|
+
"Take action: reap ghosts, nudge idle workers, reassign stalled work, drain backlog, maintain momentum, and repair broker anomalies.",
|
|
951
|
+
].join("\n");
|
|
952
|
+
}
|
|
953
|
+
function buildRalphLoopCycleNotifications(evaluation, cycleStartedAt) {
|
|
954
|
+
return {
|
|
955
|
+
followUpPrompt: buildRalphLoopFollowUpMessage(evaluation, cycleStartedAt),
|
|
956
|
+
anomalyStatus: evaluation.anomalies.length > 0
|
|
957
|
+
? buildRalphLoopStatusMessage(evaluation.anomalies.join("; "), cycleStartedAt)
|
|
958
|
+
: null,
|
|
959
|
+
recoveryStatus: buildRalphLoopStatusMessage("health recovered", cycleStartedAt),
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
function buildBrokerPromptGuidelines(agentEmoji, agentName) {
|
|
963
|
+
return [
|
|
964
|
+
`You are ${agentEmoji} ${agentName}, the Pinet BROKER. Your ONLY role is coordination and infrastructure — NEVER implementation.`,
|
|
965
|
+
// ── HARD GUARDRAIL ──────────────────────────────────────────
|
|
966
|
+
"🚫 HARD RULE — NEVER WRITE CODE: You MUST NOT implement features, fix bugs, write tests, edit source files, or do any coding task. This is a non-negotiable constraint, not a preference. Violations stall the entire multi-agent mesh.",
|
|
967
|
+
"WHY THIS RULE EXISTS: You are the ONLY process routing Slack messages, monitoring agent health, and keeping the mesh alive. If you spend even one turn writing code, messages stop flowing, dead agents don't get reaped, backlog piles up, and the whole system stalls. Workers are computation, broker is infrastructure.",
|
|
968
|
+
// ── FORBIDDEN ACTIONS ───────────────────────────────────────
|
|
969
|
+
"FORBIDDEN — Do NOT do any of these, even if explicitly asked: (1) Use the Agent tool to spawn local subagents — they have no Slack/Pinet connectivity and can't be monitored. (2) Use edit, write, or bash to modify source code. (3) Pick up coding tasks, bug fixes, refactors, or implementation work. (4) Run test suites, linters, or build commands as part of implementation work. (5) Create or modify source files in any worktree.",
|
|
970
|
+
"IF ASKED TO CODE: Refuse politely and immediately delegate. Say: 'I'm the broker — I coordinate, not code. Let me find a worker for this.' Then check pinet_agents and delegate via pinet_message.",
|
|
971
|
+
// ── ALLOWED ACTIONS ─────────────────────────────────────────
|
|
972
|
+
"ALLOWED — These are your responsibilities: (1) Route messages between humans and agents. (2) Check pinet_agents for idle workers and delegate tasks via pinet_message. (3) File GitHub issues, create/merge PRs, run code reviews via the code-reviewer subagent. (4) Monitor agent health via the RALPH loop. (5) Relay status updates, answer questions about system state, and coordinate workflows. (6) Use bash for read-only inspection: git log, git status, gh pr list, ls, cat — never for code changes.",
|
|
973
|
+
"When a human asks for work to be done, ALWAYS check `pinet_agents` for idle workers and delegate via `pinet_message`. Pick the agent on the right repo/branch when possible.",
|
|
974
|
+
"When delegating, include: the task description, relevant issue/PR numbers, branch to work on, and where to report back (Slack thread_ts).",
|
|
975
|
+
"If no workers are available, tell the human and suggest they spin up a new agent. NEVER do the work yourself as a fallback.",
|
|
976
|
+
"WORKTREE RULE: The main repo checkout must ALWAYS stay on the `main` branch. NEVER run `git checkout <branch>` or `git switch <branch>` in the main checkout.",
|
|
977
|
+
"For feature work, ALWAYS create a git worktree: `git worktree add .worktrees/<name> -b <branch>`. Tell delegated agents to do the same.",
|
|
978
|
+
"When delegating to an agent, include the worktree setup command. Example: `git worktree add .worktrees/fix-foo-123 -b fix/foo-123 && cd .worktrees/fix-foo-123`",
|
|
979
|
+
"Clean up worktrees after PRs merge: `git worktree remove .worktrees/<name>`. Flag orphaned worktrees from dead agents for cleanup.",
|
|
980
|
+
"RALPH LOOP: Run autonomous maintenance every cycle. Don't wait to be asked. Proactively: (1) REAP — ping idle agents, mark non-responders as ghost. (2) NUDGE — check assigned work, poll branches for commits, escalate stalled agents. (3) REASSIGN — if an assigned agent is dead, reassign to next idle agent immediately. (4) DRAIN — find idle agents with no work, assign queued tasks. (5) SELF-REPAIR — verify main is on `main`, check mesh health, report anomalies.",
|
|
981
|
+
];
|
|
982
|
+
}
|
|
983
|
+
function buildWorkerPromptGuidelines() {
|
|
984
|
+
return [
|
|
985
|
+
"TASK WORKFLOW: When you receive work, follow these steps:",
|
|
986
|
+
"1. ACK briefly so the sender knows you picked it up — then start working immediately. Do not stop after the ACK.",
|
|
987
|
+
"2. Do the work.",
|
|
988
|
+
"3. If you hit a blocker, report it immediately and ask for what you need — blocked work must be visible so it can be unblocked or reassigned.",
|
|
989
|
+
"4. When done, report the outcome (what changed, branch/PR, test results) — the sender needs closure and next steps.",
|
|
990
|
+
"5. When you have finished all assigned work and are waiting for more, call `pinet_free` (or `/pinet-free`) to mark yourself idle/free for the broker.",
|
|
991
|
+
"Always reply where the task came from.",
|
|
992
|
+
"If a Pinet thread explicitly says things like 'no further replies are needed', 'hard stop', or 'stay free/quiet unless a new task appears', treat it as a terminal closeout. Do NOT send another acknowledgement unless you have a real blocker, a materially new finding, or a genuinely new task arrives in that thread.",
|
|
993
|
+
"",
|
|
994
|
+
"REPLY TOOL RULES:",
|
|
995
|
+
"- If you received a task via `pinet_message`, reply via `pinet_message` to the sender.",
|
|
996
|
+
"- If you received a task in a Slack thread, reply via `slack_send` in that thread.",
|
|
997
|
+
"- Never use `slack_post_channel` with a pinet thread ID (e.g. `a2a:...`) — it will fail. Pinet threads are not Slack channels.",
|
|
998
|
+
"",
|
|
999
|
+
"PINET DELEGATION RULES:",
|
|
1000
|
+
"- When you need another connected agent to take work or parallelize, do NOT use the Agent tool to spawn a local subagent for delegation.",
|
|
1001
|
+
"- Prefer Pinet delegation: first use `pinet_agents` to find a suitable connected worker, then delegate via `pinet_message`.",
|
|
1002
|
+
"- Keep delegation inside the Pinet or Slack thread so ACKs, blockers, status updates, and final results flow back to the original sender.",
|
|
1003
|
+
"- When delegating, include the workflow (`ack/work/ask/report`), the task, relevant issue/PR numbers, repo/branch/worktree setup, important files, acceptance criteria, and where to reply.",
|
|
1004
|
+
];
|
|
1005
|
+
}
|
|
1006
|
+
function buildPinetSkinPromptGuideline(theme, personality) {
|
|
1007
|
+
if (!theme || !personality)
|
|
1008
|
+
return null;
|
|
1009
|
+
return clampPinetSkinText(`PINET SKIN (${formatPinetSkinThemeLabel(theme)}): ${clampPinetSkinText(personality, MAX_PINET_SKIN_PERSONALITY_LENGTH)} Keep it additive: flavor cadence and word choice, never clarity, accuracy, blocker/status discipline, or role boundaries.`, MAX_PINET_SKIN_PROMPT_GUIDELINE_LENGTH);
|
|
1010
|
+
}
|
|
1011
|
+
function buildIdentityReplyGuidelines(agentEmoji, agentName, location) {
|
|
1012
|
+
return [
|
|
1013
|
+
`First message in a new thread: use exact format — '${agentEmoji} \`${agentName}\` reporting from \`${location}\`\\n\\n<message body>'`,
|
|
1014
|
+
`Follow-up messages in the same thread: keep the same full identity prefix — '${agentEmoji} \`${agentName}\` <message>'`,
|
|
1015
|
+
"Never use emoji-only prefixes (for example, '🦅 Working now') — always include the full identity prefix above on every post.",
|
|
1016
|
+
];
|
|
1017
|
+
}
|
|
1018
|
+
const DEFAULT_PERSONALITY_TRAITS = ["thoughtful", "steady", "clear"];
|
|
1019
|
+
const DESCRIPTOR_PERSONALITY_TRAITS = {};
|
|
1020
|
+
const ANIMAL_PERSONALITY_TRAITS = {};
|
|
1021
|
+
function assignPersonalityTraits(target, names, traits) {
|
|
1022
|
+
for (const name of names) {
|
|
1023
|
+
target[name] = traits;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
assignPersonalityTraits(DESCRIPTOR_PERSONALITY_TRAITS, ["Rocket", "Turbo", "Hyper", "Ultra", "Mega", "Sonic", "Rapid"], ["fast", "playful", "bold"]);
|
|
1027
|
+
assignPersonalityTraits(DESCRIPTOR_PERSONALITY_TRAITS, ["Silent", "Shadow", "Velvet", "Frozen", "Glacial"], ["quiet", "patient", "precise"]);
|
|
1028
|
+
assignPersonalityTraits(DESCRIPTOR_PERSONALITY_TRAITS, ["Cosmic", "Solar", "Stellar", "Galactic", "Lunar", "Nova", "Aurora", "Nimbus", "Orbit", "Comet"], ["far-seeing", "thoughtful", "imaginative"]);
|
|
1029
|
+
assignPersonalityTraits(DESCRIPTOR_PERSONALITY_TRAITS, ["Quantum", "Pixel", "Cyber", "Atomic", "Binary", "Vector", "Prism", "Ionic", "Laser"], ["analytical", "curious", "precise"]);
|
|
1030
|
+
assignPersonalityTraits(DESCRIPTOR_PERSONALITY_TRAITS, ["Neon", "Electric", "Radiant", "Blazing", "Thunder", "Ember", "Echo"], ["energetic", "expressive", "confident"]);
|
|
1031
|
+
assignPersonalityTraits(DESCRIPTOR_PERSONALITY_TRAITS, ["Crystal", "Mystic", "Jade"], ["elegant", "intuitive", "thoughtful"]);
|
|
1032
|
+
assignPersonalityTraits(DESCRIPTOR_PERSONALITY_TRAITS, ["Golden", "Silver", "Scarlet", "Cobalt", "Iron", "Obsidian", "Slate"], ["steady", "composed", "direct"]);
|
|
1033
|
+
assignPersonalityTraits(ANIMAL_PERSONALITY_TRAITS, ["Dolphin"], ["intelligent", "agile", "friendly"]);
|
|
1034
|
+
assignPersonalityTraits(ANIMAL_PERSONALITY_TRAITS, ["Crocodile"], ["patient", "precise", "formidable"]);
|
|
1035
|
+
assignPersonalityTraits(ANIMAL_PERSONALITY_TRAITS, ["Crane"], ["elegant", "observant", "poised"]);
|
|
1036
|
+
assignPersonalityTraits(ANIMAL_PERSONALITY_TRAITS, ["Eagle", "Owl", "Raven", "Parrot", "Goose"], ["observant", "articulate", "far-seeing"]);
|
|
1037
|
+
assignPersonalityTraits(ANIMAL_PERSONALITY_TRAITS, ["Fox", "Wolf", "Lynx", "Jaguar", "Tiger", "Lion", "Cobra", "Shark", "Dragon"], ["sharp", "decisive", "confident"]);
|
|
1038
|
+
assignPersonalityTraits(ANIMAL_PERSONALITY_TRAITS, [
|
|
1039
|
+
"Badger",
|
|
1040
|
+
"Beaver",
|
|
1041
|
+
"Bison",
|
|
1042
|
+
"Buffalo",
|
|
1043
|
+
"Boar",
|
|
1044
|
+
"Bear",
|
|
1045
|
+
"Rhino",
|
|
1046
|
+
"Elephant",
|
|
1047
|
+
"Moose",
|
|
1048
|
+
"Horse",
|
|
1049
|
+
"Camel",
|
|
1050
|
+
"Goat",
|
|
1051
|
+
], ["steady", "resilient", "grounded"]);
|
|
1052
|
+
assignPersonalityTraits(ANIMAL_PERSONALITY_TRAITS, [
|
|
1053
|
+
"Otter",
|
|
1054
|
+
"Rabbit",
|
|
1055
|
+
"Koala",
|
|
1056
|
+
"Panda",
|
|
1057
|
+
"Monkey",
|
|
1058
|
+
"Sloth",
|
|
1059
|
+
"Turtle",
|
|
1060
|
+
"Whale",
|
|
1061
|
+
"Kangaroo",
|
|
1062
|
+
"Llama",
|
|
1063
|
+
"Deer",
|
|
1064
|
+
"Giraffe",
|
|
1065
|
+
"Hippo",
|
|
1066
|
+
"Zebra",
|
|
1067
|
+
], ["warm", "calm", "approachable"]);
|
|
1068
|
+
assignPersonalityTraits(ANIMAL_PERSONALITY_TRAITS, ["Raccoon", "Hedgehog", "Gecko", "Mantis"], ["meticulous", "curious", "nimble"]);
|
|
1069
|
+
function mergePersonalityTraits(descriptorTraits, animalTraits) {
|
|
1070
|
+
const merged = [];
|
|
1071
|
+
const push = (trait) => {
|
|
1072
|
+
if (!trait || merged.includes(trait))
|
|
1073
|
+
return;
|
|
1074
|
+
merged.push(trait);
|
|
1075
|
+
};
|
|
1076
|
+
const limit = Math.max(descriptorTraits.length, animalTraits.length);
|
|
1077
|
+
for (let index = 0; index < limit; index++) {
|
|
1078
|
+
push(descriptorTraits[index]);
|
|
1079
|
+
push(animalTraits[index]);
|
|
1080
|
+
}
|
|
1081
|
+
if (merged.length === 0) {
|
|
1082
|
+
for (const trait of DEFAULT_PERSONALITY_TRAITS) {
|
|
1083
|
+
push(trait);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return merged.slice(0, 4);
|
|
1087
|
+
}
|
|
1088
|
+
function resolveAgentPersonality(agentName) {
|
|
1089
|
+
const tokens = agentName.trim().split(/\s+/).filter(Boolean);
|
|
1090
|
+
const descriptor = tokens[0];
|
|
1091
|
+
const animal = tokens.length >= 2 ? tokens.at(-1) : undefined;
|
|
1092
|
+
return {
|
|
1093
|
+
descriptor,
|
|
1094
|
+
animal,
|
|
1095
|
+
traits: mergePersonalityTraits(descriptor ? (DESCRIPTOR_PERSONALITY_TRAITS[descriptor] ?? []) : [], animal ? (ANIMAL_PERSONALITY_TRAITS[animal] ?? []) : []),
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
function buildAgentPersonalityGuidelines(agentName) {
|
|
1099
|
+
const personality = resolveAgentPersonality(agentName);
|
|
1100
|
+
return [
|
|
1101
|
+
"COMMUNICATION STYLE: Let your wording lightly reflect your agent name so your updates feel like the persona behind the name.",
|
|
1102
|
+
`For \`${agentName}\`, aim for a ${personality.traits.join(", ")} tone in Slack and Pinet messages.`,
|
|
1103
|
+
"Keep the style subtle: shape cadence, word choice, and flavor — not the underlying facts or recommendations.",
|
|
1104
|
+
"PERSONALITY SAFETY RAIL: This must NOT change task execution quality, correctness, honesty, safety, technical rigor, or willingness to surface blockers and test results.",
|
|
1105
|
+
];
|
|
1106
|
+
}
|
|
1107
|
+
function resolvePersistedAgentIdentity(settings, persistedName, persistedEmoji, envNickname, seed, role = "worker") {
|
|
1108
|
+
if (persistedName && persistedEmoji) {
|
|
1109
|
+
return { name: persistedName, emoji: persistedEmoji };
|
|
1110
|
+
}
|
|
1111
|
+
return resolveAgentIdentity(settings, envNickname, seed, role);
|
|
1112
|
+
}
|
|
1113
|
+
function buildAgentStableId(sessionFile, host = os.hostname(), cwd = process.cwd(), leafId) {
|
|
1114
|
+
if (sessionFile) {
|
|
1115
|
+
return `${host}:session:${path.resolve(sessionFile)}`;
|
|
1116
|
+
}
|
|
1117
|
+
if (leafId) {
|
|
1118
|
+
return `${host}:leaf:${leafId}`;
|
|
1119
|
+
}
|
|
1120
|
+
return `${host}:cwd:${path.resolve(cwd)}`;
|
|
1121
|
+
}
|
|
1122
|
+
function resolveAgentStableId(persistedStableId, sessionFile, host = os.hostname(), cwd = process.cwd(), leafId) {
|
|
1123
|
+
return persistedStableId || buildAgentStableId(sessionFile, host, cwd, leafId);
|
|
1124
|
+
}
|
|
1125
|
+
function isLikelyLocalSubagentContext(context = {}) {
|
|
1126
|
+
const parentSession = context.sessionHeader?.parentSession;
|
|
1127
|
+
if (typeof parentSession === "string" && parentSession.trim().length > 0) {
|
|
1128
|
+
return true;
|
|
1129
|
+
}
|
|
1130
|
+
const argv = context.argv ?? process.argv.slice(2);
|
|
1131
|
+
const hasNoSession = argv.includes("--no-session");
|
|
1132
|
+
const hasPrint = argv.includes("--print") || argv.includes("-p");
|
|
1133
|
+
const modeIndex = argv.indexOf("--mode");
|
|
1134
|
+
const mode = modeIndex >= 0 ? argv[modeIndex + 1] : undefined;
|
|
1135
|
+
const hasHeadlessMode = mode === "json" || mode === "rpc";
|
|
1136
|
+
if (hasNoSession && (hasPrint || hasHeadlessMode)) {
|
|
1137
|
+
return true;
|
|
1138
|
+
}
|
|
1139
|
+
const sessionFile = typeof context.sessionFile === "string" && context.sessionFile.trim().length > 0
|
|
1140
|
+
? context.sessionFile.trim()
|
|
1141
|
+
: undefined;
|
|
1142
|
+
const leafId = typeof context.leafId === "string" && context.leafId.trim().length > 0
|
|
1143
|
+
? context.leafId.trim()
|
|
1144
|
+
: undefined;
|
|
1145
|
+
// Agent-tool subagents commonly show up as ephemeral leaf sessions: no persisted
|
|
1146
|
+
// session file, a generated leaf id, and no attached TTY. Those should never join
|
|
1147
|
+
// the mesh even when auto-follow is enabled.
|
|
1148
|
+
const hasEphemeralLeafSession = !sessionFile && Boolean(leafId);
|
|
1149
|
+
const hasTTY = Boolean((context.stdinIsTTY ?? process.stdin.isTTY) || (context.stdoutIsTTY ?? process.stdout.isTTY));
|
|
1150
|
+
return hasEphemeralLeafSession && (hasHeadlessMode || context.hasUI === false || !hasTTY);
|
|
1151
|
+
}
|
|
1152
|
+
function isDirectMessageChannel(channel) {
|
|
1153
|
+
return /^D[A-Z0-9]+$/.test(channel);
|
|
1154
|
+
}
|
|
1155
|
+
function syncFollowerInboxEntries(entries, existingThreads, agentOwner, lastDmChannel) {
|
|
1156
|
+
let nextLastDmChannel = lastDmChannel;
|
|
1157
|
+
let changed = false;
|
|
1158
|
+
const threadUpdates = [];
|
|
1159
|
+
const inboxMessages = entries.map((entry) => {
|
|
1160
|
+
const meta = entry.message.metadata ?? {};
|
|
1161
|
+
const threadTs = entry.message.threadId ?? "";
|
|
1162
|
+
const channel = typeof meta.channel === "string" ? meta.channel : "";
|
|
1163
|
+
const sender = entry.message.sender ?? "";
|
|
1164
|
+
if (threadTs && channel) {
|
|
1165
|
+
const existing = existingThreads.get(threadTs);
|
|
1166
|
+
const nextThread = {
|
|
1167
|
+
channelId: channel,
|
|
1168
|
+
threadTs,
|
|
1169
|
+
userId: existing?.userId || sender,
|
|
1170
|
+
owner: existing?.owner ?? agentOwner,
|
|
1171
|
+
};
|
|
1172
|
+
if (!existing ||
|
|
1173
|
+
existing.channelId !== nextThread.channelId ||
|
|
1174
|
+
existing.userId !== nextThread.userId ||
|
|
1175
|
+
existing.owner !== nextThread.owner) {
|
|
1176
|
+
changed = true;
|
|
1177
|
+
}
|
|
1178
|
+
threadUpdates.push(nextThread);
|
|
1179
|
+
}
|
|
1180
|
+
if (isDirectMessageChannel(channel) && nextLastDmChannel !== channel) {
|
|
1181
|
+
nextLastDmChannel = channel;
|
|
1182
|
+
changed = true;
|
|
1183
|
+
}
|
|
1184
|
+
return {
|
|
1185
|
+
channel,
|
|
1186
|
+
threadTs,
|
|
1187
|
+
userId: sender,
|
|
1188
|
+
text: entry.message.body ?? "",
|
|
1189
|
+
timestamp: entry.message.createdAt ?? "",
|
|
1190
|
+
brokerInboxId: entry.inboxId,
|
|
1191
|
+
metadata: meta,
|
|
1192
|
+
};
|
|
1193
|
+
});
|
|
1194
|
+
return {
|
|
1195
|
+
inboxMessages,
|
|
1196
|
+
threadUpdates,
|
|
1197
|
+
lastDmChannel: nextLastDmChannel,
|
|
1198
|
+
changed,
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
function syncBrokerInboxEntries(entries) {
|
|
1202
|
+
const controlEntries = [];
|
|
1203
|
+
const skinEntries = [];
|
|
1204
|
+
const inboxMessages = [];
|
|
1205
|
+
for (const entry of entries) {
|
|
1206
|
+
const meta = entry.message.metadata ?? {};
|
|
1207
|
+
const threadTs = entry.message.threadId ?? "";
|
|
1208
|
+
const sender = entry.message.sender ?? "";
|
|
1209
|
+
const body = entry.message.body ?? "";
|
|
1210
|
+
const createdAt = entry.message.createdAt ?? "";
|
|
1211
|
+
const inboxId = entry.inboxId;
|
|
1212
|
+
const control = extractPinetControlCommand({
|
|
1213
|
+
threadId: threadTs,
|
|
1214
|
+
body,
|
|
1215
|
+
metadata: meta,
|
|
1216
|
+
});
|
|
1217
|
+
if (control && inboxId != null) {
|
|
1218
|
+
controlEntries.push({ inboxId, command: control });
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
const skinUpdate = extractPinetSkinUpdate({
|
|
1222
|
+
threadId: threadTs,
|
|
1223
|
+
body,
|
|
1224
|
+
metadata: meta,
|
|
1225
|
+
});
|
|
1226
|
+
if (skinUpdate && inboxId != null) {
|
|
1227
|
+
skinEntries.push({ inboxId, update: skinUpdate });
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
inboxMessages.push({
|
|
1231
|
+
channel: "",
|
|
1232
|
+
threadTs,
|
|
1233
|
+
userId: sender,
|
|
1234
|
+
text: body,
|
|
1235
|
+
timestamp: createdAt,
|
|
1236
|
+
brokerInboxId: inboxId,
|
|
1237
|
+
metadata: meta,
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
return {
|
|
1241
|
+
controlEntries,
|
|
1242
|
+
skinEntries,
|
|
1243
|
+
inboxMessages,
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
async function resolveFollowerThreadChannel(threadTs, existingThread, resolveThread) {
|
|
1247
|
+
if (!threadTs) {
|
|
1248
|
+
return { channelId: null, changed: false };
|
|
1249
|
+
}
|
|
1250
|
+
if (!resolveThread) {
|
|
1251
|
+
return existingThread?.channelId
|
|
1252
|
+
? { channelId: existingThread.channelId, changed: false }
|
|
1253
|
+
: { channelId: null, changed: false };
|
|
1254
|
+
}
|
|
1255
|
+
try {
|
|
1256
|
+
const channelId = await resolveThread(threadTs);
|
|
1257
|
+
if (!channelId) {
|
|
1258
|
+
return { channelId: null, changed: false };
|
|
1259
|
+
}
|
|
1260
|
+
if (existingThread?.channelId === channelId) {
|
|
1261
|
+
return { channelId, changed: false };
|
|
1262
|
+
}
|
|
1263
|
+
return {
|
|
1264
|
+
channelId,
|
|
1265
|
+
changed: true,
|
|
1266
|
+
threadUpdate: {
|
|
1267
|
+
channelId,
|
|
1268
|
+
threadTs,
|
|
1269
|
+
userId: existingThread?.userId ?? "",
|
|
1270
|
+
owner: existingThread?.owner,
|
|
1271
|
+
},
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
catch {
|
|
1275
|
+
return { channelId: null, changed: false };
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
function getFollowerReconnectUiUpdate(event, wasDisconnected) {
|
|
1279
|
+
if (event === "disconnect") {
|
|
1280
|
+
return wasDisconnected
|
|
1281
|
+
? { nextWasDisconnected: true }
|
|
1282
|
+
: {
|
|
1283
|
+
nextWasDisconnected: true,
|
|
1284
|
+
notify: {
|
|
1285
|
+
level: "warning",
|
|
1286
|
+
message: "Pinet broker disconnected — reconnecting...",
|
|
1287
|
+
},
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
if (!wasDisconnected) {
|
|
1291
|
+
return { nextWasDisconnected: false };
|
|
1292
|
+
}
|
|
1293
|
+
return {
|
|
1294
|
+
nextWasDisconnected: false,
|
|
1295
|
+
notify: {
|
|
1296
|
+
level: "info",
|
|
1297
|
+
message: "Pinet broker reconnected",
|
|
1298
|
+
},
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
function agentOwnsThread(owner, agentName, agentAliases = [], ownerToken) {
|
|
1302
|
+
if (!owner)
|
|
1303
|
+
return false;
|
|
1304
|
+
if (ownerToken && owner === ownerToken)
|
|
1305
|
+
return true;
|
|
1306
|
+
if (owner === agentName)
|
|
1307
|
+
return true;
|
|
1308
|
+
for (const alias of agentAliases) {
|
|
1309
|
+
if (owner === alias)
|
|
1310
|
+
return true;
|
|
1311
|
+
}
|
|
1312
|
+
return false;
|
|
1313
|
+
}
|
|
1314
|
+
function normalizeOwnedThreads(threads, agentName, ownerToken, agentAliases = []) {
|
|
1315
|
+
let changed = false;
|
|
1316
|
+
for (const thread of threads) {
|
|
1317
|
+
if (!agentOwnsThread(thread.owner, agentName, agentAliases, ownerToken))
|
|
1318
|
+
continue;
|
|
1319
|
+
if (thread.owner === ownerToken)
|
|
1320
|
+
continue;
|
|
1321
|
+
thread.owner = ownerToken;
|
|
1322
|
+
changed = true;
|
|
1323
|
+
}
|
|
1324
|
+
return changed;
|
|
1325
|
+
}
|
|
1326
|
+
function getFollowerOwnedThreadClaims(threads, agentName, agentAliases = [], ownerToken) {
|
|
1327
|
+
return [...threads.values()]
|
|
1328
|
+
.filter((thread) => agentOwnsThread(thread.owner, agentName, agentAliases, ownerToken) &&
|
|
1329
|
+
Boolean(thread.threadTs) &&
|
|
1330
|
+
Boolean(thread.channelId))
|
|
1331
|
+
.map((thread) => ({
|
|
1332
|
+
threadTs: thread.threadTs,
|
|
1333
|
+
channelId: thread.channelId,
|
|
1334
|
+
}));
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Cache a thread from a broker inbound message in the local threads map.
|
|
1338
|
+
* The broker DB remains the source of truth; this is only a read-through
|
|
1339
|
+
* cache so Slack tools can resolve channels without hitting the DB every time.
|
|
1340
|
+
*/
|
|
1341
|
+
function trackBrokerInboundThread(threads, inMsg, owner) {
|
|
1342
|
+
if (!inMsg.threadId || !inMsg.channel)
|
|
1343
|
+
return;
|
|
1344
|
+
if (!threads.has(inMsg.threadId)) {
|
|
1345
|
+
threads.set(inMsg.threadId, {
|
|
1346
|
+
channelId: inMsg.channel,
|
|
1347
|
+
threadTs: inMsg.threadId,
|
|
1348
|
+
userId: inMsg.userId ?? "",
|
|
1349
|
+
owner,
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
function formatAgentList(agents, homedir) {
|
|
1354
|
+
if (agents.length === 0)
|
|
1355
|
+
return "(no agents connected)";
|
|
1356
|
+
return agents
|
|
1357
|
+
.map((a) => {
|
|
1358
|
+
const health = a.health ? ` [${a.health}]` : "";
|
|
1359
|
+
const stuckTag = a.stuck ? " [stuck]" : "";
|
|
1360
|
+
const pid = a.pid != null ? ` pid:${a.pid}` : "";
|
|
1361
|
+
let line = `${a.emoji} ${a.name} (${a.id}) \u2014 ${a.status}${health}${stuckTag}${pid}`;
|
|
1362
|
+
const meta = a.metadata;
|
|
1363
|
+
if (meta && (meta.cwd || meta.branch || meta.host)) {
|
|
1364
|
+
const cwd = meta.cwd ? shortenPath(meta.cwd, homedir) : "";
|
|
1365
|
+
const branch = meta.branch ? ` (${meta.branch})` : "";
|
|
1366
|
+
const host = meta.host ? ` @ ${meta.host}` : "";
|
|
1367
|
+
line += `\n ${cwd}${branch}${host}`;
|
|
1368
|
+
}
|
|
1369
|
+
if (meta?.skinTheme) {
|
|
1370
|
+
line += `\n skin: ${meta.skinTheme}`;
|
|
1371
|
+
}
|
|
1372
|
+
if (meta?.personality) {
|
|
1373
|
+
const personaPreview = meta.personality.length > 96 ? `${meta.personality.slice(0, 93)}...` : meta.personality;
|
|
1374
|
+
line += `\n persona: ${personaPreview}`;
|
|
1375
|
+
}
|
|
1376
|
+
const heartbeat = a.heartbeatSummary ?? formatAge(a.heartbeatAgeMs);
|
|
1377
|
+
const lease = a.leaseSummary ?? null;
|
|
1378
|
+
const idleInfo = a.status === "idle" && a.idleDuration ? `idle ${a.idleDuration}` : null;
|
|
1379
|
+
const activityInfo = a.status === "working" && a.lastActivityAge ? `activity ${a.lastActivityAge}` : null;
|
|
1380
|
+
if (heartbeat || lease || idleInfo || activityInfo) {
|
|
1381
|
+
const summary = [
|
|
1382
|
+
heartbeat ? `heartbeat ${heartbeat}` : null,
|
|
1383
|
+
lease,
|
|
1384
|
+
idleInfo,
|
|
1385
|
+
activityInfo,
|
|
1386
|
+
].filter((item) => Boolean(item));
|
|
1387
|
+
line += `\n ${summary.join(" · ")}`;
|
|
1388
|
+
}
|
|
1389
|
+
const tags = (a.capabilityTags ?? []).slice(0, 6);
|
|
1390
|
+
if (tags.length > 0) {
|
|
1391
|
+
const suffix = (a.capabilityTags?.length ?? 0) > tags.length ? " …" : "";
|
|
1392
|
+
line += `\n caps: ${tags.join(", ")}${suffix}`;
|
|
1393
|
+
}
|
|
1394
|
+
if (a.routingScore != null) {
|
|
1395
|
+
const reasons = (a.routingReasons ?? []).slice(0, 4);
|
|
1396
|
+
line += `\n routing: ${a.routingScore}`;
|
|
1397
|
+
if (reasons.length > 0) {
|
|
1398
|
+
line += ` (${reasons.join(", ")})`;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return line;
|
|
1402
|
+
})
|
|
1403
|
+
.join("\n");
|
|
1404
|
+
}
|
|
1405
|
+
// ─── Random / deterministic agent names ──────────────────
|
|
1406
|
+
const ADJECTIVES = [
|
|
1407
|
+
"Cosmic",
|
|
1408
|
+
"Turbo",
|
|
1409
|
+
"Neon",
|
|
1410
|
+
"Solar",
|
|
1411
|
+
"Quantum",
|
|
1412
|
+
"Pixel",
|
|
1413
|
+
"Cyber",
|
|
1414
|
+
"Atomic",
|
|
1415
|
+
"Stellar",
|
|
1416
|
+
"Thunder",
|
|
1417
|
+
"Crystal",
|
|
1418
|
+
"Mystic",
|
|
1419
|
+
"Hyper",
|
|
1420
|
+
"Ultra",
|
|
1421
|
+
"Mega",
|
|
1422
|
+
"Electric",
|
|
1423
|
+
"Galactic",
|
|
1424
|
+
"Sonic",
|
|
1425
|
+
"Laser",
|
|
1426
|
+
"Rocket",
|
|
1427
|
+
"Shadow",
|
|
1428
|
+
"Blazing",
|
|
1429
|
+
"Frozen",
|
|
1430
|
+
"Lunar",
|
|
1431
|
+
"Nova",
|
|
1432
|
+
"Aurora",
|
|
1433
|
+
"Radiant",
|
|
1434
|
+
"Velvet",
|
|
1435
|
+
"Iron",
|
|
1436
|
+
"Golden",
|
|
1437
|
+
"Silver",
|
|
1438
|
+
"Scarlet",
|
|
1439
|
+
"Cobalt",
|
|
1440
|
+
"Slate",
|
|
1441
|
+
"Obsidian",
|
|
1442
|
+
"Rapid",
|
|
1443
|
+
"Silent",
|
|
1444
|
+
"Binary",
|
|
1445
|
+
"Vector",
|
|
1446
|
+
"Prism",
|
|
1447
|
+
"Nimbus",
|
|
1448
|
+
"Orbit",
|
|
1449
|
+
"Comet",
|
|
1450
|
+
"Echo",
|
|
1451
|
+
"Ember",
|
|
1452
|
+
"Glacial",
|
|
1453
|
+
"Ionic",
|
|
1454
|
+
"Jade",
|
|
1455
|
+
];
|
|
1456
|
+
const ANIMALS = [
|
|
1457
|
+
"Badger",
|
|
1458
|
+
"Penguin",
|
|
1459
|
+
"Otter",
|
|
1460
|
+
"Raccoon",
|
|
1461
|
+
"Fox",
|
|
1462
|
+
"Panda",
|
|
1463
|
+
"Wolf",
|
|
1464
|
+
"Eagle",
|
|
1465
|
+
"Dolphin",
|
|
1466
|
+
"Lynx",
|
|
1467
|
+
"Cobra",
|
|
1468
|
+
"Raven",
|
|
1469
|
+
"Gecko",
|
|
1470
|
+
"Mantis",
|
|
1471
|
+
"Jaguar",
|
|
1472
|
+
"Goose",
|
|
1473
|
+
"Bison",
|
|
1474
|
+
"Crane",
|
|
1475
|
+
"Moose",
|
|
1476
|
+
"Owl",
|
|
1477
|
+
"Beaver",
|
|
1478
|
+
"Hedgehog",
|
|
1479
|
+
"Rabbit",
|
|
1480
|
+
"Koala",
|
|
1481
|
+
"Tiger",
|
|
1482
|
+
"Lion",
|
|
1483
|
+
"Zebra",
|
|
1484
|
+
"Giraffe",
|
|
1485
|
+
"Elephant",
|
|
1486
|
+
"Rhino",
|
|
1487
|
+
"Hippo",
|
|
1488
|
+
"Kangaroo",
|
|
1489
|
+
"Camel",
|
|
1490
|
+
"Llama",
|
|
1491
|
+
"Goat",
|
|
1492
|
+
"Deer",
|
|
1493
|
+
"Buffalo",
|
|
1494
|
+
"Horse",
|
|
1495
|
+
"Boar",
|
|
1496
|
+
"Bear",
|
|
1497
|
+
"Monkey",
|
|
1498
|
+
"Sloth",
|
|
1499
|
+
"Turtle",
|
|
1500
|
+
"Whale",
|
|
1501
|
+
"Shark",
|
|
1502
|
+
"Crocodile",
|
|
1503
|
+
"Dragon",
|
|
1504
|
+
"Parrot",
|
|
1505
|
+
];
|
|
1506
|
+
const COLORS = [
|
|
1507
|
+
"Slate",
|
|
1508
|
+
"Azure",
|
|
1509
|
+
"Blush",
|
|
1510
|
+
"Bronze",
|
|
1511
|
+
"Burgundy",
|
|
1512
|
+
"Chalk",
|
|
1513
|
+
"Coral",
|
|
1514
|
+
"Crimson",
|
|
1515
|
+
"Ebony",
|
|
1516
|
+
"Emerald",
|
|
1517
|
+
"Hazel",
|
|
1518
|
+
"Indigo",
|
|
1519
|
+
"Ivory",
|
|
1520
|
+
"Lime",
|
|
1521
|
+
"Magenta",
|
|
1522
|
+
"Navy",
|
|
1523
|
+
"Olive",
|
|
1524
|
+
"Pearl",
|
|
1525
|
+
"Rose",
|
|
1526
|
+
"Rust",
|
|
1527
|
+
];
|
|
1528
|
+
const EMOJIS = [
|
|
1529
|
+
"🦡",
|
|
1530
|
+
"🐧",
|
|
1531
|
+
"🦦",
|
|
1532
|
+
"🦝",
|
|
1533
|
+
"🦊",
|
|
1534
|
+
"🐼",
|
|
1535
|
+
"🐺",
|
|
1536
|
+
"🦅",
|
|
1537
|
+
"🐬",
|
|
1538
|
+
"🐱",
|
|
1539
|
+
"🐍",
|
|
1540
|
+
"🐦⬛",
|
|
1541
|
+
"🦎",
|
|
1542
|
+
"🦗",
|
|
1543
|
+
"🐆",
|
|
1544
|
+
"🪿",
|
|
1545
|
+
"🦬",
|
|
1546
|
+
"🦩",
|
|
1547
|
+
"🫎",
|
|
1548
|
+
"🦉",
|
|
1549
|
+
"🦫",
|
|
1550
|
+
"🦔",
|
|
1551
|
+
"🐇",
|
|
1552
|
+
"🐨",
|
|
1553
|
+
"🐯",
|
|
1554
|
+
"🦁",
|
|
1555
|
+
"🦓",
|
|
1556
|
+
"🦒",
|
|
1557
|
+
"🐘",
|
|
1558
|
+
"🦏",
|
|
1559
|
+
"🦛",
|
|
1560
|
+
"🦘",
|
|
1561
|
+
"🐫",
|
|
1562
|
+
"🦙",
|
|
1563
|
+
"🐐",
|
|
1564
|
+
"🦌",
|
|
1565
|
+
"🐃",
|
|
1566
|
+
"🐎",
|
|
1567
|
+
"🐗",
|
|
1568
|
+
"🐻",
|
|
1569
|
+
"🐒",
|
|
1570
|
+
"🦥",
|
|
1571
|
+
"🐢",
|
|
1572
|
+
"🐋",
|
|
1573
|
+
"🦈",
|
|
1574
|
+
"🐊",
|
|
1575
|
+
"🐉",
|
|
1576
|
+
"🦜",
|
|
1577
|
+
];
|
|
1578
|
+
function hashString(value) {
|
|
1579
|
+
let hash = 2166136261;
|
|
1580
|
+
for (let i = 0; i < value.length; i++) {
|
|
1581
|
+
hash ^= value.charCodeAt(i);
|
|
1582
|
+
hash = Math.imul(hash, 16777619);
|
|
1583
|
+
}
|
|
1584
|
+
return hash >>> 0;
|
|
1585
|
+
}
|
|
1586
|
+
function buildPinetOwnerToken(stableId) {
|
|
1587
|
+
const primary = hashString(stableId).toString(16).padStart(8, "0");
|
|
1588
|
+
const secondary = hashString(`${stableId}:owner`).toString(16).padStart(8, "0");
|
|
1589
|
+
return `owner:${primary}${secondary}`;
|
|
1590
|
+
}
|
|
1591
|
+
function generateAgentName(seed, role = "worker") {
|
|
1592
|
+
const animalIndex = seed
|
|
1593
|
+
? hashString(`${seed}:animal`) % ANIMALS.length
|
|
1594
|
+
: Math.floor(Math.random() * ANIMALS.length);
|
|
1595
|
+
const emoji = EMOJIS[animalIndex];
|
|
1596
|
+
if (role === "broker") {
|
|
1597
|
+
return {
|
|
1598
|
+
name: `The Broker ${ANIMALS[animalIndex]}`,
|
|
1599
|
+
emoji,
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
const adjectiveIndex = seed
|
|
1603
|
+
? hashString(`${seed}:adjective`) % ADJECTIVES.length
|
|
1604
|
+
: Math.floor(Math.random() * ADJECTIVES.length);
|
|
1605
|
+
const colorIndex = seed
|
|
1606
|
+
? hashString(`${seed}:color`) % COLORS.length
|
|
1607
|
+
: Math.floor(Math.random() * COLORS.length);
|
|
1608
|
+
return {
|
|
1609
|
+
name: `${ADJECTIVES[adjectiveIndex]} ${COLORS[colorIndex]} ${ANIMALS[animalIndex]}`,
|
|
1610
|
+
emoji,
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
exports.DEFAULT_PINET_SKIN_THEME = "default";
|
|
1614
|
+
const MAX_PINET_SKIN_THEME_LABEL_LENGTH = 60;
|
|
1615
|
+
const MAX_PINET_SKIN_PERSONALITY_LENGTH = 260;
|
|
1616
|
+
const MAX_PINET_SKIN_PROMPT_GUIDELINE_LENGTH = 460;
|
|
1617
|
+
const PINET_SKIN_STOP_WORDS = new Set([
|
|
1618
|
+
"a",
|
|
1619
|
+
"an",
|
|
1620
|
+
"and",
|
|
1621
|
+
"as",
|
|
1622
|
+
"at",
|
|
1623
|
+
"be",
|
|
1624
|
+
"for",
|
|
1625
|
+
"from",
|
|
1626
|
+
"in",
|
|
1627
|
+
"into",
|
|
1628
|
+
"of",
|
|
1629
|
+
"on",
|
|
1630
|
+
"the",
|
|
1631
|
+
"to",
|
|
1632
|
+
"with",
|
|
1633
|
+
]);
|
|
1634
|
+
const PINET_SKIN_LEADER_TITLES = [
|
|
1635
|
+
"Commander",
|
|
1636
|
+
"Oracle",
|
|
1637
|
+
"Navigator",
|
|
1638
|
+
"Steward",
|
|
1639
|
+
"Marshal",
|
|
1640
|
+
"Warden",
|
|
1641
|
+
"Architect",
|
|
1642
|
+
"Signalmaster",
|
|
1643
|
+
"Anchor",
|
|
1644
|
+
"Captain",
|
|
1645
|
+
];
|
|
1646
|
+
const PINET_SKIN_WORKER_TITLES = [
|
|
1647
|
+
"Scout",
|
|
1648
|
+
"Ranger",
|
|
1649
|
+
"Runner",
|
|
1650
|
+
"Cipher",
|
|
1651
|
+
"Pilot",
|
|
1652
|
+
"Smith",
|
|
1653
|
+
"Courier",
|
|
1654
|
+
"Weaver",
|
|
1655
|
+
"Seeker",
|
|
1656
|
+
"Operator",
|
|
1657
|
+
"Vanguard",
|
|
1658
|
+
"Scribe",
|
|
1659
|
+
];
|
|
1660
|
+
const PINET_SKIN_MODIFIERS = [
|
|
1661
|
+
"Ash",
|
|
1662
|
+
"Chrome",
|
|
1663
|
+
"Cinder",
|
|
1664
|
+
"Circuit",
|
|
1665
|
+
"Copper",
|
|
1666
|
+
"Echo",
|
|
1667
|
+
"Ember",
|
|
1668
|
+
"Ghost",
|
|
1669
|
+
"Gilded",
|
|
1670
|
+
"Hollow",
|
|
1671
|
+
"Ivory",
|
|
1672
|
+
"Jade",
|
|
1673
|
+
"Lumen",
|
|
1674
|
+
"Night",
|
|
1675
|
+
"Nova",
|
|
1676
|
+
"Onyx",
|
|
1677
|
+
"Quartz",
|
|
1678
|
+
"Silver",
|
|
1679
|
+
"Static",
|
|
1680
|
+
"Storm",
|
|
1681
|
+
"Velvet",
|
|
1682
|
+
"Violet",
|
|
1683
|
+
];
|
|
1684
|
+
const PINET_SKIN_BROKER_EMOJIS = ["🧭", "🛡️", "🛰️", "🪄", "👑", "🧠", "🦉", "🌙"];
|
|
1685
|
+
const PINET_SKIN_WORKER_EMOJIS = ["⚡", "🗡️", "🛠️", "🕶️", "🧪", "🧰", "🧿", "📡", "🔧", "🛰️"];
|
|
1686
|
+
const PINET_SKIN_DEMEANORS = [
|
|
1687
|
+
"calm under pressure",
|
|
1688
|
+
"sharp-eyed about details",
|
|
1689
|
+
"quietly theatrical",
|
|
1690
|
+
"fast with a comeback",
|
|
1691
|
+
"ritualistic about checklists",
|
|
1692
|
+
"suspicious of sloppy work",
|
|
1693
|
+
"protective of teammates",
|
|
1694
|
+
"fond of dramatic mission language",
|
|
1695
|
+
"precise about timing",
|
|
1696
|
+
"improvisational when plans crack",
|
|
1697
|
+
"obsessed with clean handoffs",
|
|
1698
|
+
"confident without being loud",
|
|
1699
|
+
];
|
|
1700
|
+
const PINET_SKIN_ROLE_FOCUS = {
|
|
1701
|
+
broker: [
|
|
1702
|
+
"Coordinate, delegate, and guard role lines.",
|
|
1703
|
+
"Stay in mission control: route cleanly and watch mesh health.",
|
|
1704
|
+
"Lead quietly, keep the mesh moving, never blur roles.",
|
|
1705
|
+
],
|
|
1706
|
+
worker: [
|
|
1707
|
+
"Ship cleanly, show progress, surface blockers fast.",
|
|
1708
|
+
"Work autonomously, hand off crisply, keep status visible.",
|
|
1709
|
+
"Keep the flair light and the execution exact.",
|
|
1710
|
+
],
|
|
1711
|
+
};
|
|
1712
|
+
const PINET_SKIN_GENERIC_VOICE_PROFILE = {
|
|
1713
|
+
matchTokens: [],
|
|
1714
|
+
cadences: [
|
|
1715
|
+
"distinct but disciplined pacing",
|
|
1716
|
+
"measured specialist calm",
|
|
1717
|
+
"confident, signal-first rhythm",
|
|
1718
|
+
],
|
|
1719
|
+
imagery: [
|
|
1720
|
+
"just enough theme-colored detail",
|
|
1721
|
+
"a few memorable touches",
|
|
1722
|
+
"light scene-setting color",
|
|
1723
|
+
],
|
|
1724
|
+
diction: [
|
|
1725
|
+
"precise verbs and clean nouns",
|
|
1726
|
+
"readable specialist phrasing",
|
|
1727
|
+
"tight status language",
|
|
1728
|
+
],
|
|
1729
|
+
};
|
|
1730
|
+
const PINET_SKIN_VOICE_PROFILES = [
|
|
1731
|
+
{
|
|
1732
|
+
matchTokens: ["cyber", "cyberpunk", "hacker", "hackers", "neon", "chrome", "circuit", "matrix"],
|
|
1733
|
+
cadences: [
|
|
1734
|
+
"clipped, high-signal tempo",
|
|
1735
|
+
"cool operator composure",
|
|
1736
|
+
"a fast terminal-room rhythm",
|
|
1737
|
+
],
|
|
1738
|
+
imagery: ["neon-and-static touches", "back-alley console imagery", "midnight terminal color"],
|
|
1739
|
+
diction: [
|
|
1740
|
+
"precise technical verbs",
|
|
1741
|
+
"sharp nouns and terse status lines",
|
|
1742
|
+
"clean operator shorthand kept readable",
|
|
1743
|
+
],
|
|
1744
|
+
},
|
|
1745
|
+
{
|
|
1746
|
+
matchTokens: [
|
|
1747
|
+
"night",
|
|
1748
|
+
"watch",
|
|
1749
|
+
"fellowship",
|
|
1750
|
+
"ring",
|
|
1751
|
+
"quest",
|
|
1752
|
+
"kingdom",
|
|
1753
|
+
"dragon",
|
|
1754
|
+
"throne",
|
|
1755
|
+
"asoiaf",
|
|
1756
|
+
"myth",
|
|
1757
|
+
],
|
|
1758
|
+
cadences: [
|
|
1759
|
+
"watchful, oath-keeping calm",
|
|
1760
|
+
"quest-log steadiness",
|
|
1761
|
+
"campfire-veteran confidence",
|
|
1762
|
+
],
|
|
1763
|
+
imagery: [
|
|
1764
|
+
"watchfire and banner imagery",
|
|
1765
|
+
"maps, oaths, and cold-night touches",
|
|
1766
|
+
"sentinel-and-hearth color",
|
|
1767
|
+
],
|
|
1768
|
+
diction: [
|
|
1769
|
+
"plainspoken duty-first wording",
|
|
1770
|
+
"measured reports with a hint of legend",
|
|
1771
|
+
"mythic color without purple prose",
|
|
1772
|
+
],
|
|
1773
|
+
},
|
|
1774
|
+
{
|
|
1775
|
+
matchTokens: [
|
|
1776
|
+
"apollo",
|
|
1777
|
+
"mission",
|
|
1778
|
+
"control",
|
|
1779
|
+
"space",
|
|
1780
|
+
"orbit",
|
|
1781
|
+
"orbital",
|
|
1782
|
+
"rocket",
|
|
1783
|
+
"lunar",
|
|
1784
|
+
"solar",
|
|
1785
|
+
"star",
|
|
1786
|
+
"galaxy",
|
|
1787
|
+
],
|
|
1788
|
+
cadences: ["mission-control composure", "countdown-ready brevity", "flight-loop precision"],
|
|
1789
|
+
imagery: [
|
|
1790
|
+
"telemetry and console-room touches",
|
|
1791
|
+
"launch-window color",
|
|
1792
|
+
"starfield and instrument-panel imagery",
|
|
1793
|
+
],
|
|
1794
|
+
diction: [
|
|
1795
|
+
"checklist language and clean callouts",
|
|
1796
|
+
"status-board phrasing",
|
|
1797
|
+
"disciplined technical shorthand",
|
|
1798
|
+
],
|
|
1799
|
+
},
|
|
1800
|
+
{
|
|
1801
|
+
matchTokens: [
|
|
1802
|
+
"ghibli",
|
|
1803
|
+
"spirit",
|
|
1804
|
+
"forest",
|
|
1805
|
+
"garden",
|
|
1806
|
+
"moss",
|
|
1807
|
+
"river",
|
|
1808
|
+
"wind",
|
|
1809
|
+
"moon",
|
|
1810
|
+
"woods",
|
|
1811
|
+
"meadow",
|
|
1812
|
+
"lantern",
|
|
1813
|
+
],
|
|
1814
|
+
cadences: ["quiet, observant calm", "gentle confidence", "an unhurried but alert pace"],
|
|
1815
|
+
imagery: [
|
|
1816
|
+
"lantern, weather, and small-wonder touches",
|
|
1817
|
+
"moss, wind, and water color",
|
|
1818
|
+
"natural textures used lightly",
|
|
1819
|
+
],
|
|
1820
|
+
diction: [
|
|
1821
|
+
"warm precise wording",
|
|
1822
|
+
"soft edges around hard facts",
|
|
1823
|
+
"calm plain language with a little glow",
|
|
1824
|
+
],
|
|
1825
|
+
},
|
|
1826
|
+
{
|
|
1827
|
+
matchTokens: [
|
|
1828
|
+
"deep",
|
|
1829
|
+
"sea",
|
|
1830
|
+
"ocean",
|
|
1831
|
+
"salvage",
|
|
1832
|
+
"tide",
|
|
1833
|
+
"reef",
|
|
1834
|
+
"abyss",
|
|
1835
|
+
"harbor",
|
|
1836
|
+
"submarine",
|
|
1837
|
+
"dive",
|
|
1838
|
+
"nautical",
|
|
1839
|
+
],
|
|
1840
|
+
cadences: ["steady dive-team calm", "sonar-sweep patience", "weathered deckhand brevity"],
|
|
1841
|
+
imagery: [
|
|
1842
|
+
"rope, tide, and pressure-gauge touches",
|
|
1843
|
+
"salvage-yard detail",
|
|
1844
|
+
"deep-water color used sparingly",
|
|
1845
|
+
],
|
|
1846
|
+
diction: [
|
|
1847
|
+
"measured callouts and sturdy verbs",
|
|
1848
|
+
"crew-ready status language",
|
|
1849
|
+
"practical field vocabulary",
|
|
1850
|
+
],
|
|
1851
|
+
},
|
|
1852
|
+
];
|
|
1853
|
+
const PINET_SKIN_PERSONALITY_OPENERS = [
|
|
1854
|
+
'Let "{theme}" steer the cadence: {cadence}, {demeanor}.',
|
|
1855
|
+
'Take cues from "{theme}": {cadence}, {demeanor}.',
|
|
1856
|
+
'Carry "{theme}" as a quiet doctrine: {cadence}, {demeanor}.',
|
|
1857
|
+
];
|
|
1858
|
+
const PINET_SKIN_PERSONALITY_STYLE_TEMPLATES = [
|
|
1859
|
+
"Favor {diction} with {imagery}; stay scannable.",
|
|
1860
|
+
"Use {diction} and {imagery}; keep status crisp.",
|
|
1861
|
+
"Keep the wording {diction}, brushed with {imagery}, and quick to read.",
|
|
1862
|
+
];
|
|
1863
|
+
function titleCaseSkinToken(token) {
|
|
1864
|
+
return token.length === 0 ? token : token[0].toUpperCase() + token.slice(1);
|
|
1865
|
+
}
|
|
1866
|
+
function singularizeSkinToken(token) {
|
|
1867
|
+
if (token.endsWith("ies") && token.length > 4) {
|
|
1868
|
+
return `${token.slice(0, -3)}y`;
|
|
1869
|
+
}
|
|
1870
|
+
if (token.endsWith("s") && !token.endsWith("ss") && token.length > 4) {
|
|
1871
|
+
return token.slice(0, -1);
|
|
1872
|
+
}
|
|
1873
|
+
return token;
|
|
1874
|
+
}
|
|
1875
|
+
function pickSkinValue(values, seed, label) {
|
|
1876
|
+
return values[hashString(`${seed}:${label}`) % values.length];
|
|
1877
|
+
}
|
|
1878
|
+
function clampPinetSkinText(value, maxLength) {
|
|
1879
|
+
const trimmed = value.trim();
|
|
1880
|
+
if (trimmed.length <= maxLength) {
|
|
1881
|
+
return trimmed;
|
|
1882
|
+
}
|
|
1883
|
+
const sliceLength = Math.max(0, maxLength - 1);
|
|
1884
|
+
return `${trimmed.slice(0, sliceLength).trimEnd()}…`;
|
|
1885
|
+
}
|
|
1886
|
+
function formatPinetSkinThemeLabel(theme) {
|
|
1887
|
+
return clampPinetSkinText(theme, MAX_PINET_SKIN_THEME_LABEL_LENGTH);
|
|
1888
|
+
}
|
|
1889
|
+
function getPinetSkinTokens(theme) {
|
|
1890
|
+
const rawTokens = theme.toLowerCase().match(/[a-z0-9]+/g) ?? [];
|
|
1891
|
+
const meaningful = rawTokens
|
|
1892
|
+
.map((token) => singularizeSkinToken(token))
|
|
1893
|
+
.filter((token) => token.length > 1 && !PINET_SKIN_STOP_WORDS.has(token));
|
|
1894
|
+
const tokens = (meaningful.length > 0 ? meaningful : rawTokens)
|
|
1895
|
+
.map((token) => titleCaseSkinToken(token))
|
|
1896
|
+
.filter((token, index, list) => list.indexOf(token) === index);
|
|
1897
|
+
return tokens.length > 0 ? tokens : ["Signal"];
|
|
1898
|
+
}
|
|
1899
|
+
function normalizePinetSkinTheme(theme) {
|
|
1900
|
+
const trimmed = theme?.trim();
|
|
1901
|
+
if (!trimmed)
|
|
1902
|
+
return null;
|
|
1903
|
+
return trimmed.toLowerCase() === exports.DEFAULT_PINET_SKIN_THEME ? exports.DEFAULT_PINET_SKIN_THEME : trimmed;
|
|
1904
|
+
}
|
|
1905
|
+
function mergeUniqueSkinValues(...lists) {
|
|
1906
|
+
const merged = [];
|
|
1907
|
+
for (const list of lists) {
|
|
1908
|
+
for (const value of list) {
|
|
1909
|
+
if (!merged.includes(value)) {
|
|
1910
|
+
merged.push(value);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
return merged;
|
|
1915
|
+
}
|
|
1916
|
+
function resolvePinetSkinVoiceProfile(theme) {
|
|
1917
|
+
const rawTokens = theme.toLowerCase().match(/[a-z0-9]+/g) ?? [];
|
|
1918
|
+
const matchedProfiles = PINET_SKIN_VOICE_PROFILES.filter((profile) => profile.matchTokens.some((token) => rawTokens.includes(token)));
|
|
1919
|
+
if (matchedProfiles.length === 0) {
|
|
1920
|
+
return PINET_SKIN_GENERIC_VOICE_PROFILE;
|
|
1921
|
+
}
|
|
1922
|
+
return {
|
|
1923
|
+
cadences: mergeUniqueSkinValues(...matchedProfiles.map((profile) => profile.cadences)),
|
|
1924
|
+
imagery: mergeUniqueSkinValues(...matchedProfiles.map((profile) => profile.imagery)),
|
|
1925
|
+
diction: mergeUniqueSkinValues(...matchedProfiles.map((profile) => profile.diction)),
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
function fillPinetSkinTemplate(template, values) {
|
|
1929
|
+
return Object.entries(values).reduce((result, [key, value]) => result.replaceAll(`{${key}}`, value), template);
|
|
1930
|
+
}
|
|
1931
|
+
function buildPinetSkinAssignment(options) {
|
|
1932
|
+
const normalizedTheme = normalizePinetSkinTheme(options.theme) ?? exports.DEFAULT_PINET_SKIN_THEME;
|
|
1933
|
+
if (normalizedTheme === exports.DEFAULT_PINET_SKIN_THEME) {
|
|
1934
|
+
const generated = generateAgentName(options.seed);
|
|
1935
|
+
const personality = options.role === "broker"
|
|
1936
|
+
? "Default whimsical broker skin. Be playful but disciplined, delegate clearly, and keep the mesh coordinated."
|
|
1937
|
+
: "Default whimsical worker skin. Be playful but focused, do the work, and report blockers and outcomes clearly.";
|
|
1938
|
+
return {
|
|
1939
|
+
theme: normalizedTheme,
|
|
1940
|
+
role: options.role,
|
|
1941
|
+
name: generated.name,
|
|
1942
|
+
emoji: generated.emoji,
|
|
1943
|
+
personality,
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
const themeLabel = formatPinetSkinThemeLabel(normalizedTheme);
|
|
1947
|
+
const tokens = getPinetSkinTokens(normalizedTheme);
|
|
1948
|
+
const primary = pickSkinValue(tokens, options.seed, "primary-token");
|
|
1949
|
+
const alternateTokens = tokens.filter((token) => token !== primary);
|
|
1950
|
+
const secondary = alternateTokens.length > 0
|
|
1951
|
+
? pickSkinValue(alternateTokens, `${options.seed}:${normalizedTheme}`, "secondary-token")
|
|
1952
|
+
: primary;
|
|
1953
|
+
const modifier = pickSkinValue(PINET_SKIN_MODIFIERS, options.seed, "modifier");
|
|
1954
|
+
const title = options.role === "broker"
|
|
1955
|
+
? pickSkinValue(PINET_SKIN_LEADER_TITLES, options.seed, "leader-title")
|
|
1956
|
+
: pickSkinValue(PINET_SKIN_WORKER_TITLES, options.seed, "worker-title");
|
|
1957
|
+
const emoji = options.role === "broker"
|
|
1958
|
+
? pickSkinValue(PINET_SKIN_BROKER_EMOJIS, options.seed, "leader-emoji")
|
|
1959
|
+
: pickSkinValue(PINET_SKIN_WORKER_EMOJIS, options.seed, "worker-emoji");
|
|
1960
|
+
const demeanor = pickSkinValue(PINET_SKIN_DEMEANORS, `${options.seed}:${normalizedTheme}`, "demeanor");
|
|
1961
|
+
const voice = resolvePinetSkinVoiceProfile(normalizedTheme);
|
|
1962
|
+
const cadence = pickSkinValue(voice.cadences, `${options.seed}:${normalizedTheme}`, "voice-cadence");
|
|
1963
|
+
const imagery = pickSkinValue(voice.imagery, `${options.seed}:${normalizedTheme}`, "voice-imagery");
|
|
1964
|
+
const diction = pickSkinValue(voice.diction, `${options.seed}:${normalizedTheme}`, "voice-diction");
|
|
1965
|
+
const opener = fillPinetSkinTemplate(pickSkinValue(PINET_SKIN_PERSONALITY_OPENERS, `${options.seed}:${normalizedTheme}`, "persona-opener"), {
|
|
1966
|
+
theme: themeLabel,
|
|
1967
|
+
cadence,
|
|
1968
|
+
demeanor,
|
|
1969
|
+
});
|
|
1970
|
+
const style = fillPinetSkinTemplate(pickSkinValue(PINET_SKIN_PERSONALITY_STYLE_TEMPLATES, `${options.seed}:${normalizedTheme}:${options.role}`, "persona-style"), {
|
|
1971
|
+
diction,
|
|
1972
|
+
imagery,
|
|
1973
|
+
});
|
|
1974
|
+
const roleFocus = pickSkinValue(PINET_SKIN_ROLE_FOCUS[options.role], options.seed, "role-focus");
|
|
1975
|
+
const workerCore = secondary === modifier ? primary : secondary;
|
|
1976
|
+
const name = options.role === "broker" ? `${primary} ${title}` : `${modifier} ${workerCore} ${title}`;
|
|
1977
|
+
return {
|
|
1978
|
+
theme: normalizedTheme,
|
|
1979
|
+
role: options.role,
|
|
1980
|
+
name,
|
|
1981
|
+
emoji,
|
|
1982
|
+
personality: clampPinetSkinText(`${opener} ${style} ${roleFocus}`, MAX_PINET_SKIN_PERSONALITY_LENGTH),
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
// ─── Agent identity persistence ─────────────────────────
|
|
1986
|
+
function resolveAgentIdentity(settings, envNickname, seed, role = "worker") {
|
|
1987
|
+
// 1. Explicit config (both must be present)
|
|
1988
|
+
if (settings.agentName && settings.agentEmoji) {
|
|
1989
|
+
return { name: settings.agentName, emoji: settings.agentEmoji };
|
|
1990
|
+
}
|
|
1991
|
+
// 2. PI_NICKNAME env var (name fixed, emoji deterministic when seeded)
|
|
1992
|
+
if (envNickname) {
|
|
1993
|
+
const generated = generateAgentName(seed, role);
|
|
1994
|
+
return { name: envNickname, emoji: generated.emoji };
|
|
1995
|
+
}
|
|
1996
|
+
// 3. Fully generated
|
|
1997
|
+
return generateAgentName(seed, role);
|
|
1998
|
+
}
|
|
1999
|
+
function alignAgentIdentityToRole(currentIdentity, settings, envNickname, seed, role = "worker") {
|
|
2000
|
+
const workerIdentity = resolveAgentIdentity(settings, envNickname, seed, "worker");
|
|
2001
|
+
const brokerIdentity = resolveAgentIdentity(settings, envNickname, seed, "broker");
|
|
2002
|
+
const targetIdentity = role === "broker" ? brokerIdentity : workerIdentity;
|
|
2003
|
+
if ((currentIdentity.name === workerIdentity.name &&
|
|
2004
|
+
currentIdentity.emoji === workerIdentity.emoji) ||
|
|
2005
|
+
(currentIdentity.name === brokerIdentity.name && currentIdentity.emoji === brokerIdentity.emoji)) {
|
|
2006
|
+
return targetIdentity;
|
|
2007
|
+
}
|
|
2008
|
+
return currentIdentity;
|
|
2009
|
+
}
|
|
2010
|
+
function resolveRuntimeAgentIdentity(currentIdentity, settings, envNickname, seed, role = "worker") {
|
|
2011
|
+
if (settings.agentName && settings.agentEmoji) {
|
|
2012
|
+
return { name: settings.agentName, emoji: settings.agentEmoji };
|
|
2013
|
+
}
|
|
2014
|
+
if (envNickname) {
|
|
2015
|
+
const generated = generateAgentName(seed, role);
|
|
2016
|
+
return { name: envNickname, emoji: generated.emoji };
|
|
2017
|
+
}
|
|
2018
|
+
return alignAgentIdentityToRole(currentIdentity, settings, undefined, seed, role);
|
|
2019
|
+
}
|
|
2020
|
+
exports.DEFAULT_CONFIRMATION_REQUEST_TTL_MS = 5 * 60_000;
|
|
2021
|
+
function normalizeThreadConfirmationState(state, now = Date.now(), ttlMs = exports.DEFAULT_CONFIRMATION_REQUEST_TTL_MS) {
|
|
2022
|
+
const keepFresh = (request) => now - request.requestedAt < ttlMs;
|
|
2023
|
+
const pending = state.pending.filter(keepFresh);
|
|
2024
|
+
return {
|
|
2025
|
+
pending: pending.length > 1 ? [] : pending,
|
|
2026
|
+
approved: state.approved.filter(keepFresh),
|
|
2027
|
+
rejected: state.rejected.filter(keepFresh),
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
function isThreadConfirmationStateEmpty(state) {
|
|
2031
|
+
return state.pending.length === 0 && state.approved.length === 0 && state.rejected.length === 0;
|
|
2032
|
+
}
|
|
2033
|
+
function confirmationRequestMatches(request, toolName, action) {
|
|
2034
|
+
return (0, guardrails_js_1.matchesToolPattern)(toolName, [request.toolPattern]) && request.action === action;
|
|
2035
|
+
}
|
|
2036
|
+
function consumeMatchingConfirmationRequest(list, toolName, action) {
|
|
2037
|
+
const idx = list.findIndex((request) => confirmationRequestMatches(request, toolName, action));
|
|
2038
|
+
if (idx === -1)
|
|
2039
|
+
return null;
|
|
2040
|
+
const [match] = list.splice(idx, 1);
|
|
2041
|
+
return match;
|
|
2042
|
+
}
|
|
2043
|
+
function registerThreadConfirmationRequest(state, request, now = Date.now()) {
|
|
2044
|
+
const normalized = normalizeThreadConfirmationState(state, now);
|
|
2045
|
+
const nextState = {
|
|
2046
|
+
pending: normalized.pending,
|
|
2047
|
+
approved: normalized.approved.filter((entry) => !confirmationRequestMatches(entry, request.toolPattern, request.action)),
|
|
2048
|
+
rejected: normalized.rejected.filter((entry) => !confirmationRequestMatches(entry, request.toolPattern, request.action)),
|
|
2049
|
+
};
|
|
2050
|
+
const existingPending = nextState.pending[0];
|
|
2051
|
+
if (!existingPending) {
|
|
2052
|
+
return {
|
|
2053
|
+
state: {
|
|
2054
|
+
...nextState,
|
|
2055
|
+
pending: [request],
|
|
2056
|
+
},
|
|
2057
|
+
status: "created",
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
if (existingPending.toolPattern === request.toolPattern &&
|
|
2061
|
+
existingPending.action === request.action) {
|
|
2062
|
+
return {
|
|
2063
|
+
state: {
|
|
2064
|
+
...nextState,
|
|
2065
|
+
pending: [{ ...existingPending, requestedAt: request.requestedAt }],
|
|
2066
|
+
},
|
|
2067
|
+
status: "refreshed",
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
return {
|
|
2071
|
+
state: nextState,
|
|
2072
|
+
status: "conflict",
|
|
2073
|
+
conflict: existingPending,
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Call Slack API with bounded retry logic (max 3 retries on rate limit).
|
|
2078
|
+
* Handles 429 rate-limit responses by waiting retry-after duration and retrying.
|
|
2079
|
+
* Throws error if retries are exhausted or API returns error.
|
|
2080
|
+
*/
|
|
2081
|
+
async function callSlackAPI(method, token, body, options = {}) {
|
|
2082
|
+
const { signal, retryCount = 0 } = options;
|
|
2083
|
+
const { url, init } = buildSlackRequest(method, token, body);
|
|
2084
|
+
const res = await fetch(url, signal ? { ...init, signal } : init);
|
|
2085
|
+
if (res.status === 429) {
|
|
2086
|
+
const maxRetries = 3;
|
|
2087
|
+
if (retryCount >= maxRetries) {
|
|
2088
|
+
throw new Error(`Slack ${method}: rate limited after ${maxRetries} retries`);
|
|
2089
|
+
}
|
|
2090
|
+
const wait = Number(res.headers.get("retry-after") ?? "3");
|
|
2091
|
+
if (signal) {
|
|
2092
|
+
await abortableDelay(wait * 1000, signal);
|
|
2093
|
+
}
|
|
2094
|
+
else {
|
|
2095
|
+
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
|
|
2096
|
+
}
|
|
2097
|
+
return callSlackAPI(method, token, body, { signal, retryCount: retryCount + 1 });
|
|
2098
|
+
}
|
|
2099
|
+
const data = (await res.json());
|
|
2100
|
+
if (!data.ok)
|
|
2101
|
+
throw new Error(`Slack ${method}: ${data.error ?? "unknown error"}`);
|
|
2102
|
+
return data;
|
|
2103
|
+
}
|
|
2104
|
+
// ─── Follower inbox partitioning (#102, #175) ───────────
|
|
2105
|
+
function isRalphNudgeEntry(entry) {
|
|
2106
|
+
return entry.message.metadata?.kind === "ralph_loop_nudge";
|
|
2107
|
+
}
|
|
2108
|
+
function isAgentToAgentEntry(entry) {
|
|
2109
|
+
const threadId = entry.message.threadId ?? "";
|
|
2110
|
+
return threadId.startsWith("a2a:") || entry.message.metadata?.a2a === true;
|
|
2111
|
+
}
|
|
2112
|
+
function partitionFollowerInboxEntries(entries) {
|
|
2113
|
+
const nudges = [];
|
|
2114
|
+
const agentMessages = [];
|
|
2115
|
+
const regular = [];
|
|
2116
|
+
for (const entry of entries) {
|
|
2117
|
+
if (isRalphNudgeEntry(entry)) {
|
|
2118
|
+
nudges.push(entry);
|
|
2119
|
+
}
|
|
2120
|
+
else if (isAgentToAgentEntry(entry)) {
|
|
2121
|
+
agentMessages.push(entry);
|
|
2122
|
+
}
|
|
2123
|
+
else {
|
|
2124
|
+
regular.push(entry);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
return { nudges, agentMessages, regular };
|
|
2128
|
+
}
|