@pinet/slack-bridge 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +86 -34
  2. package/dist/broker/adapters/slack.d.ts +19 -1
  3. package/dist/broker/adapters/slack.js +111 -22
  4. package/dist/broker/client.d.ts +2 -1
  5. package/dist/broker/client.js +1 -0
  6. package/dist/broker/socket-server.js +18 -0
  7. package/dist/deploy-manifest.d.ts +5 -0
  8. package/dist/deploy-manifest.js +30 -1
  9. package/dist/follower-runtime.js +5 -1
  10. package/dist/helpers.d.ts +14 -0
  11. package/dist/helpers.js +45 -21
  12. package/dist/index.js +60 -0
  13. package/dist/pinet-commands.d.ts +6 -1
  14. package/dist/pinet-commands.js +166 -1
  15. package/dist/pinet-mesh-ops.d.ts +11 -0
  16. package/dist/pinet-mesh-ops.js +17 -0
  17. package/dist/pinet-tools.d.ts +47 -0
  18. package/dist/pinet-tools.js +496 -36
  19. package/dist/prompts/broker/tmux.md +2 -2
  20. package/dist/reaction-triggers.d.ts +1 -0
  21. package/dist/reaction-triggers.js +26 -15
  22. package/dist/runtime-agent-context.js +19 -0
  23. package/dist/runtime-mode.js +7 -1
  24. package/dist/single-player-runtime.js +22 -26
  25. package/dist/slack-access.d.ts +11 -0
  26. package/dist/slack-access.js +30 -0
  27. package/dist/slack-agents-command.d.ts +19 -0
  28. package/dist/slack-agents-command.js +90 -0
  29. package/dist/slack-export.d.ts +1 -1
  30. package/dist/slack-export.js +6 -4
  31. package/dist/slack-file-access.d.ts +34 -0
  32. package/dist/slack-file-access.js +209 -0
  33. package/dist/slack-message-context.d.ts +0 -1
  34. package/dist/slack-message-context.js +1 -6
  35. package/dist/slack-pinet-runtime-adapter.d.ts +4 -2
  36. package/dist/slack-pinet-runtime-adapter.js +12 -0
  37. package/dist/slack-tools.d.ts +6 -0
  38. package/dist/slack-tools.js +290 -36
  39. package/dist/slack-upload.d.ts +13 -1
  40. package/dist/slack-upload.js +29 -2
  41. package/dist/stale-slack-messages.d.ts +12 -0
  42. package/dist/stale-slack-messages.js +29 -0
  43. package/dist/subtree-broker-runtime.d.ts +109 -0
  44. package/dist/subtree-broker-runtime.js +558 -0
  45. package/manifest.yaml +9 -0
  46. package/package.json +16 -9
  47. package/skills/slack-bridge/SKILL.md +60 -1
@@ -0,0 +1,558 @@
1
+ import { execFile } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { promisify } from "node:util";
7
+ import { dispatchDirectAgentMessage, resolveDirectAgentTarget } from "./broker/agent-messaging.js";
8
+ import { startBroker } from "./broker/index.js";
9
+ import { HEARTBEAT_INTERVAL_MS } from "./broker/client.js";
10
+ import { buildPinetOwnerToken, generateAgentName, normalizeOutgoingPinetControlMessage, resolvePinetMeshAuth, syncBrokerInboxEntries, } from "./helpers.js";
11
+ const execFileAsync = promisify(execFile);
12
+ const DEFAULT_SPAWN_REGISTRATION_TIMEOUT_MS = 45_000;
13
+ const SUBTREE_CHILD_EXIT_GRACE_MS = 5_000;
14
+ function sanitizePathSegment(value) {
15
+ const sanitized = value.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
16
+ return sanitized || "agent";
17
+ }
18
+ function randomSuffix() {
19
+ return Math.random().toString(36).slice(2, 8);
20
+ }
21
+ function delay(ms) {
22
+ return new Promise((resolve) => {
23
+ setTimeout(resolve, ms);
24
+ });
25
+ }
26
+ export function buildSubtreeBrokerPaths(stableId) {
27
+ const rootDir = path.join(os.homedir(), ".pi", "pinet-subtrees", sanitizePathSegment(stableId));
28
+ return {
29
+ rootDir,
30
+ socketPath: path.join(rootDir, "pinet.sock"),
31
+ dbPath: path.join(rootDir, "pinet-broker.db"),
32
+ lockPath: path.join(rootDir, "pinet-broker.lock"),
33
+ };
34
+ }
35
+ function buildSelfAgentId(stableId) {
36
+ return `subbroker-${sanitizePathSegment(stableId).slice(0, 80)}`;
37
+ }
38
+ function buildChildLaunchEnv(paths, selfAgentId, input = {}) {
39
+ return {
40
+ PINET_SOCKET_PATH: paths.socketPath,
41
+ PINET_BROKER_MANAGED: "1",
42
+ PINET_PARENT_AGENT_ID: selfAgentId,
43
+ PINET_ROOT_AGENT_ID: selfAgentId,
44
+ PINET_SPAWNED_BY_AGENT_ID: selfAgentId,
45
+ PINET_LAUNCH_SOURCE: "subtree-broker-tmux",
46
+ ...(input.launchId ? { PINET_LAUNCH_ID: input.launchId } : {}),
47
+ ...(input.role ? { PINET_SUBTREE_ROLE: input.role } : {}),
48
+ ...(input.laneId ? { PINET_LANE_ID: input.laneId } : {}),
49
+ ...(input.tmuxSession ? { PINET_TMUX_SESSION: input.tmuxSession } : {}),
50
+ };
51
+ }
52
+ function quoteShellValue(value) {
53
+ return `'${value.replace(/'/g, `'\\''`)}'`;
54
+ }
55
+ function buildChildLaunchHint(paths, selfAgentId, cwd) {
56
+ const env = buildChildLaunchEnv(paths, selfAgentId);
57
+ const envPrefix = Object.entries(env)
58
+ .map(([key, value]) => `${key}=${quoteShellValue(value)}`)
59
+ .join(" ");
60
+ return `cd ${quoteShellValue(cwd)} && ${envPrefix} pi`;
61
+ }
62
+ function toFollowerInboxEntry(input) {
63
+ return {
64
+ inboxId: input.entry.id,
65
+ message: {
66
+ threadId: input.message.threadId,
67
+ source: input.message.source,
68
+ sender: input.message.sender,
69
+ body: input.message.body,
70
+ createdAt: input.message.createdAt,
71
+ metadata: input.message.metadata,
72
+ },
73
+ };
74
+ }
75
+ function metadataString(metadata, key) {
76
+ const value = metadata?.[key];
77
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
78
+ }
79
+ function resolveRepoPath(repo, cwd) {
80
+ const trimmed = repo.trim();
81
+ if (!trimmed)
82
+ throw new Error("spawn requires repo");
83
+ const candidates = [
84
+ path.isAbsolute(trimmed) ? trimmed : null,
85
+ trimmed === "." ? cwd : null,
86
+ path.resolve(cwd, trimmed),
87
+ path.join(os.homedir(), trimmed),
88
+ ].filter((candidate) => Boolean(candidate));
89
+ for (const candidate of candidates) {
90
+ try {
91
+ if (fs.statSync(candidate).isDirectory())
92
+ return candidate;
93
+ }
94
+ catch {
95
+ // Try the next candidate.
96
+ }
97
+ }
98
+ throw new Error(`spawn repo not found: ${repo}`);
99
+ }
100
+ function normalizeRole(role) {
101
+ const normalized = role?.trim();
102
+ return normalized && normalized.length > 0 ? normalized : "subworker";
103
+ }
104
+ function buildTmuxSessionName(repoPath, role, launchId) {
105
+ const repoName = sanitizePathSegment(path.basename(repoPath));
106
+ const roleName = sanitizePathSegment(role);
107
+ const shortLaunch = sanitizePathSegment(launchId).slice(-8);
108
+ return sanitizePathSegment(`pinet-${repoName}-${roleName}-${shortLaunch}`).slice(0, 80);
109
+ }
110
+ function findTmuxSocketPath() {
111
+ const configuredDir = process.env.CLAUDE_TMUX_SOCKET_DIR?.trim();
112
+ const candidates = [
113
+ configuredDir ? path.join(configuredDir, "claude.sock") : null,
114
+ process.env.TMUX?.split(",")[0] ?? null,
115
+ process.env.TMPDIR ? path.join(process.env.TMPDIR, "claude-tmux-sockets", "claude.sock") : null,
116
+ ].filter((candidate) => Boolean(candidate));
117
+ return candidates.find((candidate) => fs.existsSync(candidate)) ?? null;
118
+ }
119
+ function buildTmuxBaseArgs(socketPath) {
120
+ return socketPath ? ["-S", socketPath] : [];
121
+ }
122
+ function buildTmuxMonitorCommand(sessionName, socketPath) {
123
+ const socketArgs = socketPath ? `-S ${quoteShellValue(socketPath)} ` : "";
124
+ return `tmux ${socketArgs}attach -t ${quoteShellValue(sessionName)}`;
125
+ }
126
+ function getExtensionEntryPath() {
127
+ const currentPath = fileURLToPath(import.meta.url);
128
+ const extension = path.extname(currentPath) || ".js";
129
+ return path.join(path.dirname(currentPath), `index${extension}`);
130
+ }
131
+ function childStartupPrompt(parentAgentId) {
132
+ return [
133
+ `You are a Pinet subtree child supervised by ${parentAgentId}.`,
134
+ "Wait for the supervising worker's Pinet task, then do that task and report back through Pinet.",
135
+ "If you are not following Pinet yet, wait for the launcher to run /pinet follow.",
136
+ ].join(" ");
137
+ }
138
+ function buildLauncherScript(input) {
139
+ const inheritedEnvKeys = [
140
+ "PI_CODING_AGENT_DIR",
141
+ "PI_CODING_AGENT_SESSION_DIR",
142
+ "PI_OFFLINE",
143
+ "PI_SETTINGS_PATH",
144
+ "PINET_MESH_SECRET",
145
+ "PINET_MESH_SECRET_PATH",
146
+ "SLACK_APP_TOKEN",
147
+ "SLACK_BOT_TOKEN",
148
+ ];
149
+ const inheritedExports = inheritedEnvKeys
150
+ .map((key) => {
151
+ const value = process.env[key];
152
+ return value ? `export ${key}=${quoteShellValue(value)}` : null;
153
+ })
154
+ .filter((line) => Boolean(line));
155
+ const envExports = Object.entries(input.env).map(([key, value]) => `export ${key}=${quoteShellValue(value)}`);
156
+ const nickname = `Subtree ${input.env.PINET_SUBTREE_ROLE ?? "Worker"} ${input.env.PINET_LAUNCH_ID ?? randomSuffix()}`;
157
+ return [
158
+ "#!/bin/bash",
159
+ "set -euo pipefail",
160
+ `cd ${quoteShellValue(input.repoPath)}`,
161
+ ...inheritedExports,
162
+ ...envExports,
163
+ `export PI_NICKNAME=${quoteShellValue(nickname)}`,
164
+ `exec pi -e ${quoteShellValue(input.extensionEntryPath)} ${quoteShellValue(input.startupPrompt)}`,
165
+ "",
166
+ ].join("\n");
167
+ }
168
+ function isSubtreeChildAgent(agent, selfAgentId) {
169
+ return agent.id !== selfAgentId && agent.parentAgentId === selfAgentId;
170
+ }
171
+ function toSubtreeAgentRecord(db, agent) {
172
+ return {
173
+ emoji: agent.emoji,
174
+ name: agent.name,
175
+ id: agent.id,
176
+ pid: agent.pid,
177
+ status: agent.status,
178
+ metadata: agent.metadata,
179
+ lastHeartbeat: agent.lastHeartbeat,
180
+ lastSeen: agent.lastSeen,
181
+ disconnectedAt: agent.disconnectedAt,
182
+ resumableUntil: agent.resumableUntil,
183
+ outboundCount: agent.outboundCount,
184
+ pendingInboxCount: db.getPendingInboxCount(agent.id),
185
+ parentAgentId: agent.parentAgentId,
186
+ rootAgentId: agent.rootAgentId,
187
+ treeDepth: agent.treeDepth,
188
+ supervisionState: agent.supervisionState,
189
+ subtreeRole: agent.subtreeRole,
190
+ laneId: agent.laneId,
191
+ };
192
+ }
193
+ export function createSubtreeBrokerRuntime(deps) {
194
+ let activeBroker = null;
195
+ let selfAgentId = null;
196
+ let startedAt = null;
197
+ let activePaths = null;
198
+ let heartbeatTimer = null;
199
+ const spawnedWorkers = new Map();
200
+ function stopHeartbeat() {
201
+ if (!heartbeatTimer)
202
+ return;
203
+ clearInterval(heartbeatTimer);
204
+ heartbeatTimer = null;
205
+ }
206
+ function startHeartbeat(broker, agentId) {
207
+ stopHeartbeat();
208
+ heartbeatTimer = setInterval(() => {
209
+ try {
210
+ broker.db.heartbeatAgent(agentId);
211
+ }
212
+ catch {
213
+ // Best effort only; normal broker maintenance will notice if this fails persistently.
214
+ }
215
+ }, HEARTBEAT_INTERVAL_MS);
216
+ heartbeatTimer.unref?.();
217
+ }
218
+ function currentChildren() {
219
+ const broker = activeBroker;
220
+ const agentId = selfAgentId;
221
+ if (!broker || !agentId)
222
+ return [];
223
+ return broker.db.getAllAgents().filter((agent) => isSubtreeChildAgent(agent, agentId));
224
+ }
225
+ function getStatus() {
226
+ const childLaunchEnv = activePaths && selfAgentId ? buildChildLaunchEnv(activePaths, selfAgentId) : {};
227
+ return {
228
+ active: activeBroker !== null,
229
+ selfAgentId,
230
+ startedAt,
231
+ paths: activePaths,
232
+ childLaunchEnv,
233
+ childLaunchHint: activePaths && selfAgentId
234
+ ? buildChildLaunchHint(activePaths, selfAgentId, deps.cwd)
235
+ : null,
236
+ childCount: currentChildren().length,
237
+ spawnedWorkers: [...spawnedWorkers.values()],
238
+ };
239
+ }
240
+ function drainSelfInbox(ctx, broker, agentId) {
241
+ const entries = broker.db.getInbox(agentId).map(toFollowerInboxEntry);
242
+ if (entries.length === 0)
243
+ return;
244
+ const synced = syncBrokerInboxEntries(entries);
245
+ const handledControlInboxIds = new Set();
246
+ for (const entry of synced.controlEntries) {
247
+ try {
248
+ const queued = deps.requestRemoteControl(entry.command, ctx);
249
+ if (queued.ackDisposition === "immediate") {
250
+ handledControlInboxIds.add(entry.inboxId);
251
+ }
252
+ if (queued.shouldStartNow) {
253
+ deps.runRemoteControl(entry.command, ctx);
254
+ }
255
+ }
256
+ catch (error) {
257
+ ctx.ui.notify(`Subtree Pinet control failed: ${deps.formatError(error)}`, "error");
258
+ }
259
+ }
260
+ if (handledControlInboxIds.size > 0) {
261
+ broker.db.markDelivered([...handledControlInboxIds], agentId);
262
+ }
263
+ if (synced.inboxMessages.length === 0)
264
+ return;
265
+ deps.pushInboxMessages(synced.inboxMessages);
266
+ deps.updateBadge();
267
+ deps.maybeDrainInboxIfIdle(ctx);
268
+ }
269
+ function readInbox(options = {}) {
270
+ if (!activeBroker || !selfAgentId)
271
+ return null;
272
+ if (options.threadId && !activeBroker.db.getThread(options.threadId))
273
+ return null;
274
+ const result = activeBroker.db.readInbox(selfAgentId, options);
275
+ return {
276
+ messages: result.messages.map((item) => ({
277
+ inboxId: item.entry.id,
278
+ delivered: item.entry.delivered,
279
+ readAt: item.entry.readAt,
280
+ message: item.message,
281
+ })),
282
+ unreadCountBefore: result.unreadCountBefore,
283
+ unreadCountAfter: result.unreadCountAfter,
284
+ unreadThreads: result.unreadThreads,
285
+ markedReadIds: result.markedReadIds,
286
+ };
287
+ }
288
+ async function sendMessage(target, body, metadata) {
289
+ if (!activeBroker || !selfAgentId)
290
+ return null;
291
+ const targetAgent = resolveDirectAgentTarget(activeBroker.db.getAgents(), target);
292
+ if (!targetAgent || targetAgent.id === selfAgentId)
293
+ return null;
294
+ const control = normalizeOutgoingPinetControlMessage(body, metadata);
295
+ const finalBody = control?.body ?? body;
296
+ const finalMetadata = control?.metadata ?? metadata;
297
+ const identity = deps.getAgentIdentity();
298
+ const result = dispatchDirectAgentMessage(activeBroker.db, {
299
+ senderAgentId: selfAgentId,
300
+ senderAgentName: identity.name || "Subtree Broker",
301
+ target,
302
+ body: finalBody,
303
+ ...(finalMetadata ? { metadata: finalMetadata } : {}),
304
+ });
305
+ return {
306
+ messageId: result.messageId,
307
+ target: result.target.name,
308
+ threadId: result.threadId,
309
+ };
310
+ }
311
+ function listAgents(includeGhosts = false) {
312
+ const broker = activeBroker;
313
+ if (!broker)
314
+ return null;
315
+ const agents = broker.db.getAllAgents();
316
+ const filtered = includeGhosts ? agents : agents.filter((agent) => !agent.disconnectedAt);
317
+ return filtered.map((agent) => toSubtreeAgentRecord(broker.db, agent));
318
+ }
319
+ async function sendFollowCommand(sessionName, tmuxBaseArgs) {
320
+ await execFileAsync("tmux", [
321
+ ...tmuxBaseArgs,
322
+ "send-keys",
323
+ "-t",
324
+ sessionName,
325
+ "-l",
326
+ "--",
327
+ "/pinet follow",
328
+ ]);
329
+ await execFileAsync("tmux", [...tmuxBaseArgs, "send-keys", "-t", sessionName, "Enter"]);
330
+ }
331
+ async function waitForSpawnedAgent(input) {
332
+ const deadline = Date.now() + input.timeoutMs;
333
+ let lastFollowAttemptAt = 0;
334
+ while (Date.now() < deadline) {
335
+ const agent = input.broker.db
336
+ .getAllAgents()
337
+ .find((candidate) => metadataString(candidate.metadata, "launchId") === input.launchId);
338
+ if (agent)
339
+ return agent;
340
+ if (Date.now() - lastFollowAttemptAt > 6_000) {
341
+ lastFollowAttemptAt = Date.now();
342
+ await sendFollowCommand(input.sessionName, input.tmuxBaseArgs).catch(() => {
343
+ // The session may still be starting; the loop retries until timeout.
344
+ });
345
+ }
346
+ await delay(1_000);
347
+ }
348
+ throw new Error(`subtree worker session ${input.sessionName} started but did not register within ${input.timeoutMs}ms`);
349
+ }
350
+ async function requestChildExit(agent) {
351
+ await sendMessage(agent.id, "/exit", { subtreeLifecycle: "stop" }).catch(() => null);
352
+ }
353
+ async function killTmuxSession(sessionName, tmuxBaseArgs) {
354
+ await execFileAsync("tmux", [...tmuxBaseArgs, "has-session", "-t", sessionName]).catch(() => {
355
+ throw new Error("missing");
356
+ });
357
+ await execFileAsync("tmux", [...tmuxBaseArgs, "kill-session", "-t", sessionName]);
358
+ }
359
+ function childTmuxSessions(broker, agentId) {
360
+ const sessions = new Set();
361
+ for (const worker of spawnedWorkers.values()) {
362
+ sessions.add(worker.sessionName);
363
+ }
364
+ for (const agent of broker.db.getAllAgents()) {
365
+ if (!isSubtreeChildAgent(agent, agentId))
366
+ continue;
367
+ const session = metadataString(agent.metadata, "tmuxSession");
368
+ if (session)
369
+ sessions.add(session);
370
+ }
371
+ return [...sessions];
372
+ }
373
+ async function stopChildren(broker, agentId) {
374
+ const children = broker.db
375
+ .getAllAgents()
376
+ .filter((agent) => isSubtreeChildAgent(agent, agentId));
377
+ await Promise.all(children.map(requestChildExit));
378
+ if (children.length > 0) {
379
+ await delay(SUBTREE_CHILD_EXIT_GRACE_MS);
380
+ }
381
+ const tmuxSocketPath = findTmuxSocketPath();
382
+ const tmuxBaseArgs = buildTmuxBaseArgs(tmuxSocketPath);
383
+ await Promise.all(childTmuxSessions(broker, agentId).map((sessionName) => killTmuxSession(sessionName, tmuxBaseArgs).catch(() => undefined)));
384
+ }
385
+ async function stop(options = {}) {
386
+ stopHeartbeat();
387
+ const broker = activeBroker;
388
+ const agentId = selfAgentId;
389
+ if (!broker)
390
+ return;
391
+ try {
392
+ if (agentId && options.stopChildren !== false) {
393
+ await stopChildren(broker, agentId);
394
+ }
395
+ if (options.releaseIdentity && agentId) {
396
+ broker.db.unregisterAgent(agentId);
397
+ }
398
+ await broker.stop();
399
+ }
400
+ catch {
401
+ // Best effort; callers should be able to continue even if shutdown cleanup is partial.
402
+ }
403
+ finally {
404
+ activeBroker = null;
405
+ selfAgentId = null;
406
+ startedAt = null;
407
+ activePaths = null;
408
+ spawnedWorkers.clear();
409
+ }
410
+ }
411
+ async function start(ctx) {
412
+ if (activeBroker)
413
+ return getStatus();
414
+ const stableId = deps.getCentralAgentId() ?? deps.getAgentStableId();
415
+ const paths = buildSubtreeBrokerPaths(stableId);
416
+ fs.mkdirSync(paths.rootDir, { recursive: true });
417
+ const meshAuth = resolvePinetMeshAuth(deps.getSettings());
418
+ const broker = await startBroker({
419
+ dbPath: paths.dbPath,
420
+ socketPath: paths.socketPath,
421
+ lockPath: paths.lockPath,
422
+ ...(meshAuth.meshSecret ? { meshSecret: meshAuth.meshSecret } : {}),
423
+ ...(meshAuth.meshSecretPath ? { meshSecretPath: meshAuth.meshSecretPath } : {}),
424
+ });
425
+ const selfId = buildSelfAgentId(stableId);
426
+ const { name, emoji } = deps.getAgentIdentity();
427
+ const metadata = {
428
+ ...(await deps.getAgentMetadata("broker")),
429
+ subtreeBroker: true,
430
+ upstreamAgentId: deps.getCentralAgentId(),
431
+ subtreeSocketPath: paths.socketPath,
432
+ };
433
+ const selfAgent = broker.db.registerAgent(selfId, name ? `Subtree Broker ${name}` : "Subtree Broker", emoji || "🌳", process.pid, metadata, `${stableId}:subtree-broker`);
434
+ broker.server.setAgentRegistrationResolver((registration) => {
435
+ const role = deps.getMeshRoleFromMetadata(registration.metadata, "worker");
436
+ const identity = generateAgentName(registration.stableId ?? registration.agentId, role);
437
+ return {
438
+ name: registration.name || identity.name,
439
+ emoji: registration.emoji || identity.emoji,
440
+ metadata: {
441
+ ...(registration.metadata ?? {}),
442
+ subtreeBrokerAgentId: selfAgent.id,
443
+ subtreeRootAgentId: selfAgent.id,
444
+ },
445
+ };
446
+ });
447
+ broker.server.onAgentMessage((targetAgentId) => {
448
+ if (targetAgentId !== selfAgent.id)
449
+ return;
450
+ drainSelfInbox(ctx, broker, selfAgent.id);
451
+ });
452
+ activeBroker = broker;
453
+ selfAgentId = selfAgent.id;
454
+ startedAt = new Date().toISOString();
455
+ activePaths = paths;
456
+ startHeartbeat(broker, selfAgent.id);
457
+ broker.db.setSetting("pinet.subtreeBrokerParentStableId", deps.getAgentStableId());
458
+ broker.db.setSetting("pinet.subtreeBrokerOwnerToken", buildPinetOwnerToken(stableId));
459
+ return getStatus();
460
+ }
461
+ async function spawnWorker(ctx, input) {
462
+ if (!input.task.trim())
463
+ throw new Error("spawn requires task");
464
+ if (!input.repo.trim())
465
+ throw new Error("spawn requires repo");
466
+ if (!activeBroker) {
467
+ await start(ctx);
468
+ }
469
+ if (!activeBroker || !activePaths || !selfAgentId) {
470
+ throw new Error("Subtree broker is not running.");
471
+ }
472
+ const repoPath = resolveRepoPath(input.repo, deps.cwd);
473
+ const role = normalizeRole(input.role);
474
+ const launchId = `subtree-${Date.now().toString(36)}-${randomSuffix()}`;
475
+ const sessionName = buildTmuxSessionName(repoPath, role, launchId);
476
+ const tmuxSocketPath = findTmuxSocketPath();
477
+ const tmuxBaseArgs = buildTmuxBaseArgs(tmuxSocketPath);
478
+ const monitorCommand = buildTmuxMonitorCommand(sessionName, tmuxSocketPath);
479
+ const childLaunchEnv = buildChildLaunchEnv(activePaths, selfAgentId, {
480
+ launchId,
481
+ role,
482
+ ...(input.laneId ? { laneId: input.laneId } : {}),
483
+ tmuxSession: sessionName,
484
+ });
485
+ const launchersDir = path.join(activePaths.rootDir, "launchers");
486
+ fs.mkdirSync(launchersDir, { recursive: true });
487
+ const launcherPath = path.join(launchersDir, `${sessionName}.sh`);
488
+ fs.writeFileSync(launcherPath, buildLauncherScript({
489
+ repoPath,
490
+ env: childLaunchEnv,
491
+ extensionEntryPath: getExtensionEntryPath(),
492
+ startupPrompt: childStartupPrompt(selfAgentId),
493
+ }), { mode: 0o700 });
494
+ await execFileAsync("tmux", [
495
+ ...tmuxBaseArgs,
496
+ "new-session",
497
+ "-d",
498
+ "-s",
499
+ sessionName,
500
+ launcherPath,
501
+ ]);
502
+ const workerRecord = {
503
+ launchId,
504
+ sessionName,
505
+ repoPath,
506
+ role,
507
+ laneId: input.laneId ?? null,
508
+ agentId: null,
509
+ startedAt: new Date().toISOString(),
510
+ monitorCommand,
511
+ };
512
+ spawnedWorkers.set(launchId, workerRecord);
513
+ const agent = await waitForSpawnedAgent({
514
+ broker: activeBroker,
515
+ launchId,
516
+ sessionName,
517
+ tmuxBaseArgs,
518
+ timeoutMs: input.waitForRegistrationMs ?? DEFAULT_SPAWN_REGISTRATION_TIMEOUT_MS,
519
+ });
520
+ const updatedRecord = { ...workerRecord, agentId: agent.id };
521
+ spawnedWorkers.set(launchId, updatedRecord);
522
+ const messageResult = await sendMessage(agent.id, input.task, {
523
+ subtreeTask: true,
524
+ launchId,
525
+ role,
526
+ ...(input.laneId ? { laneId: input.laneId } : {}),
527
+ });
528
+ if (!messageResult) {
529
+ throw new Error(`subtree worker ${agent.id} registered but could not receive the task`);
530
+ }
531
+ return {
532
+ status: "started",
533
+ launchId,
534
+ sessionName,
535
+ repoPath,
536
+ role,
537
+ laneId: input.laneId ?? null,
538
+ agentId: agent.id,
539
+ agentName: agent.name,
540
+ messageId: messageResult.messageId,
541
+ threadId: messageResult.threadId,
542
+ monitorCommand,
543
+ socketPath: activePaths.socketPath,
544
+ dbPath: activePaths.dbPath,
545
+ childLaunchEnv,
546
+ };
547
+ }
548
+ return {
549
+ start,
550
+ stop,
551
+ getStatus,
552
+ readInbox,
553
+ sendMessage,
554
+ listAgents,
555
+ spawnWorker,
556
+ isActive: () => activeBroker !== null,
557
+ };
558
+ }
package/manifest.yaml CHANGED
@@ -11,6 +11,14 @@ features:
11
11
  bot_user:
12
12
  display_name: Pinet
13
13
  always_online: true
14
+ # Default packaged command for the Pinet app. For Oathgate or another app name,
15
+ # either edit this command before manual Slack import or run `pnpm deploy:slack`
16
+ # with slackCommandName/slackCommandNames configured so deployment rewrites it.
17
+ slash_commands:
18
+ - command: /pinet
19
+ description: Show the Pinet broker roster and current work
20
+ usage_hint: "agents list [all]"
21
+ should_escape: false
14
22
  assistant_view:
15
23
  assistant_description: Pi coding agent
16
24
  suggested_prompts: []
@@ -23,6 +31,7 @@ oauth_config:
23
31
  - channels:history
24
32
  - channels:read
25
33
  - chat:write
34
+ - commands
26
35
  - files:read
27
36
  - files:write
28
37
  - bookmarks:read
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@pinet/slack-bridge",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
- "description": "Slack assistant integration for pi (Pinet) — multi-agent broker, thread routing, and inbox tools",
5
+ "description": "Pi package for Pinet Slack assistant integration — multi-agent broker, thread routing, and inbox tools",
6
6
  "author": "Will Porcellini <5994936+gugu91@users.noreply.github.com>",
7
7
  "license": "MIT",
8
8
  "repository": {
@@ -26,7 +26,12 @@
26
26
  "dist/"
27
27
  ],
28
28
  "keywords": [
29
- "pi-package"
29
+ "pi-package",
30
+ "pi",
31
+ "pinet",
32
+ "slack",
33
+ "assistant",
34
+ "agent"
30
35
  ],
31
36
  "pi": {
32
37
  "extensions": [
@@ -44,17 +49,19 @@
44
49
  "test": "vitest run"
45
50
  },
46
51
  "dependencies": {
47
- "@pinet/broker-core": "0.1.0",
48
- "@pinet/imessage-bridge": "0.1.0",
49
- "@pinet/pinet-core": "0.1.0",
50
- "@pinet/transport-core": "0.1.0",
52
+ "@pinet/broker-core": "0.2.0",
53
+ "@pinet/imessage-bridge": "0.2.0",
54
+ "@pinet/pinet-core": "0.2.0",
55
+ "@pinet/transport-core": "0.2.0",
51
56
  "@sinclair/typebox": "^0.34.49"
52
57
  },
53
58
  "types": "./dist/index.d.ts",
54
59
  "peerDependencies": {
55
- "@earendil-works/pi-coding-agent": "*"
60
+ "@earendil-works/pi-coding-agent": "*",
61
+ "@earendil-works/pi-tui": "*"
56
62
  },
57
63
  "devDependencies": {
58
- "@earendil-works/pi-coding-agent": "^0.74.0"
64
+ "@earendil-works/pi-coding-agent": "^0.74.0",
65
+ "@earendil-works/pi-tui": "^0.74.0"
59
66
  }
60
67
  }