@pinet/slack-bridge 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +59 -32
  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 +506 -38
  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 +9 -7
  47. package/skills/slack-bridge/SKILL.md +60 -1
package/dist/helpers.d.ts CHANGED
@@ -23,6 +23,8 @@ export interface SlackBridgeSettings {
23
23
  ralphSnoozeAfterEmptyCycles?: number;
24
24
  ralphSnoozeDurationMs?: number;
25
25
  skinTheme?: string;
26
+ slackCommandName?: string;
27
+ slackCommandNames?: string[];
26
28
  agentName?: string;
27
29
  agentEmoji?: string;
28
30
  meshSecret?: string;
@@ -173,6 +175,12 @@ export interface AgentDisplayInfo {
173
175
  launchSource?: string;
174
176
  tmuxSession?: string;
175
177
  brokerManagedAt?: string;
178
+ parentAgentId?: string;
179
+ rootAgentId?: string;
180
+ treeDepth?: number;
181
+ supervisionState?: string;
182
+ subtreeRole?: string;
183
+ laneId?: string;
176
184
  skinTheme?: string;
177
185
  personality?: string;
178
186
  skinStatusVocabulary?: PinetSkinStatusVocabulary;
@@ -212,6 +220,12 @@ export interface AgentVisibilityInput {
212
220
  lastActivity?: string | null;
213
221
  outboundCount?: number | null;
214
222
  pendingInboxCount?: number | null;
223
+ parentAgentId?: string | null;
224
+ rootAgentId?: string | null;
225
+ treeDepth?: number;
226
+ supervisionState?: string;
227
+ subtreeRole?: string | null;
228
+ laneId?: string | null;
215
229
  }
216
230
  export interface AgentVisibilityOptions {
217
231
  now?: number;
package/dist/helpers.js CHANGED
@@ -837,8 +837,15 @@ export function buildAgentDisplayInfo(agent, options = {}) {
837
837
  health = "stale";
838
838
  }
839
839
  const metadata = asRecord(agent.metadata);
840
+ const hasHierarchyMetadata = Boolean(agent.parentAgentId ||
841
+ agent.rootAgentId ||
842
+ typeof agent.treeDepth === "number" ||
843
+ agent.supervisionState ||
844
+ agent.subtreeRole ||
845
+ agent.laneId);
840
846
  const capabilities = extractAgentCapabilities(metadata);
841
847
  const capabilityTags = buildAgentCapabilityTags(capabilities);
848
+ const displayMetadata = metadata ?? {};
842
849
  const idleSinceMs = parseIsoMs(agent.idleSince);
843
850
  const lastActivityMs = parseIsoMs(agent.lastActivity);
844
851
  const idleDurationMs = idleSinceMs == null ? null : Math.max(0, nowMs - idleSinceMs);
@@ -849,31 +856,37 @@ export function buildAgentDisplayInfo(agent, options = {}) {
849
856
  id: agent.id,
850
857
  ...(agent.pid != null ? { pid: agent.pid } : {}),
851
858
  status: agent.status,
852
- metadata: metadata
859
+ metadata: metadata || hasHierarchyMetadata
853
860
  ? {
854
- cwd: asString(metadata.cwd),
855
- branch: asString(metadata.branch),
856
- ...(metadata.workdirDirty === true ? { workdirDirty: true } : {}),
857
- ...(typeof metadata.workdirDirtyFileCount === "number"
858
- ? { workdirDirtyFileCount: metadata.workdirDirtyFileCount }
861
+ cwd: asString(displayMetadata.cwd),
862
+ branch: asString(displayMetadata.branch),
863
+ ...(displayMetadata.workdirDirty === true ? { workdirDirty: true } : {}),
864
+ ...(typeof displayMetadata.workdirDirtyFileCount === "number"
865
+ ? { workdirDirtyFileCount: displayMetadata.workdirDirtyFileCount }
859
866
  : {}),
860
- ...(metadata.gitProbeFailed === true ? { gitProbeFailed: true } : {}),
861
- ...(asString(metadata.gitProbedAt)
862
- ? { gitProbedAt: asString(metadata.gitProbedAt) }
867
+ ...(displayMetadata.gitProbeFailed === true ? { gitProbeFailed: true } : {}),
868
+ ...(asString(displayMetadata.gitProbedAt)
869
+ ? { gitProbedAt: asString(displayMetadata.gitProbedAt) }
863
870
  : {}),
864
- host: asString(metadata.host),
865
- repo: asString(metadata.repo) ?? capabilities.repo,
866
- role: asString(metadata.role) ?? capabilities.role,
867
- brokerManaged: metadata.brokerManaged === true,
868
- brokerManagedBy: asString(metadata.brokerManagedBy),
869
- launchSource: asString(metadata.launchSource),
870
- tmuxSession: asString(metadata.tmuxSession),
871
- brokerManagedAt: asString(metadata.brokerManagedAt),
872
- skinTheme: asString(metadata.skinTheme),
873
- personality: asString(metadata.personality),
874
- ...(extractPinetSkinStatusVocabulary(metadata.skinStatusVocabulary)
871
+ host: asString(displayMetadata.host),
872
+ repo: asString(displayMetadata.repo) ?? capabilities.repo,
873
+ role: asString(displayMetadata.role) ?? capabilities.role,
874
+ brokerManaged: displayMetadata.brokerManaged === true,
875
+ brokerManagedBy: asString(displayMetadata.brokerManagedBy),
876
+ launchSource: asString(displayMetadata.launchSource),
877
+ tmuxSession: asString(displayMetadata.tmuxSession),
878
+ brokerManagedAt: asString(displayMetadata.brokerManagedAt),
879
+ parentAgentId: agent.parentAgentId ?? asString(displayMetadata.parentAgentId),
880
+ rootAgentId: agent.rootAgentId ?? asString(displayMetadata.rootAgentId),
881
+ ...(typeof agent.treeDepth === "number" ? { treeDepth: agent.treeDepth } : {}),
882
+ supervisionState: agent.supervisionState,
883
+ subtreeRole: agent.subtreeRole ?? asString(displayMetadata.subtreeRole),
884
+ laneId: agent.laneId ?? asString(displayMetadata.laneId),
885
+ skinTheme: asString(displayMetadata.skinTheme),
886
+ personality: asString(displayMetadata.personality),
887
+ ...(extractPinetSkinStatusVocabulary(displayMetadata.skinStatusVocabulary)
875
888
  ? {
876
- skinStatusVocabulary: extractPinetSkinStatusVocabulary(metadata.skinStatusVocabulary),
889
+ skinStatusVocabulary: extractPinetSkinStatusVocabulary(displayMetadata.skinStatusVocabulary),
877
890
  }
878
891
  : {}),
879
892
  ...(capabilities.scope ? { scope: capabilities.scope } : {}),
@@ -1729,6 +1742,17 @@ export function formatAgentList(agents, homedir) {
1729
1742
  const probe = meta.gitProbeFailed ? " [git probe failed]" : "";
1730
1743
  line += `\n ${cwd}${branch}${host}${probe}`;
1731
1744
  }
1745
+ if (meta?.parentAgentId || meta?.supervisionState === "orphaned") {
1746
+ const hierarchy = [
1747
+ meta.parentAgentId ? `parent=${meta.parentAgentId}` : null,
1748
+ meta.rootAgentId ? `root=${meta.rootAgentId}` : null,
1749
+ typeof meta.treeDepth === "number" ? `depth=${meta.treeDepth}` : null,
1750
+ meta.supervisionState ? `state=${meta.supervisionState}` : null,
1751
+ meta.subtreeRole ? `role=${meta.subtreeRole}` : null,
1752
+ meta.laneId ? `lane=${meta.laneId}` : null,
1753
+ ].filter((item) => Boolean(item));
1754
+ line += `\n subtree: ${hierarchy.join(" · ")}`;
1755
+ }
1732
1756
  if (meta?.brokerManaged) {
1733
1757
  const managed = [
1734
1758
  meta.launchSource ? `source=${meta.launchSource}` : "source=broker",
package/dist/index.js CHANGED
@@ -37,9 +37,12 @@ import { createAgentPromptGuidance } from "./agent-prompt-guidance.js";
37
37
  import { createAgentEventRuntime } from "./agent-event-runtime.js";
38
38
  import { createSessionUiRuntime } from "./session-ui-runtime.js";
39
39
  import { createSlackRequestRuntime } from "./slack-request-runtime.js";
40
+ import { getSlackMessageAgeMs, isStaleSlackMessage } from "./stale-slack-messages.js";
41
+ import { formatSlackAgentsDashboard, formatSlackAgentsUsage, isSlackAgentCommand, isSlackAgentsListCommand, resolveSlackAgentCommandNames, shouldIncludeSlackAgentsGhosts, } from "./slack-agents-command.js";
40
42
  import { createPinetRegistrationGate } from "./pinet-registration-gate.js";
41
43
  import { createBrokerRuntimeAccess } from "./broker-runtime-access.js";
42
44
  import { createInboxDrainRuntime } from "./inbox-drain-runtime.js";
45
+ import { createSubtreeBrokerRuntime } from "./subtree-broker-runtime.js";
43
46
  import { createAgentCompletionRuntime } from "./agent-completion-runtime.js";
44
47
  import { sendBrokerMessage } from "./broker/message-send.js";
45
48
  import { SlackThreadStatusManager } from "./slack-thread-status.js";
@@ -406,6 +409,23 @@ export default function (pi) {
406
409
  formatError: msg,
407
410
  });
408
411
  const { requestRemoteControl, runRemoteControl, resetRemoteControlState } = pinetRemoteControl;
412
+ const subtreeBrokerRuntime = createSubtreeBrokerRuntime({
413
+ cwd: process.cwd(),
414
+ getSettings: () => settings,
415
+ getAgentStableId: () => agentStableId,
416
+ getCentralAgentId: () => brokerClient?.client.getRegisteredIdentity()?.agentId ?? null,
417
+ getAgentIdentity: () => ({ name: agentName, emoji: agentEmoji }),
418
+ getAgentMetadata,
419
+ getMeshRoleFromMetadata: (metadata, fallbackRole) => getMeshRoleFromMetadata(metadata, fallbackRole),
420
+ pushInboxMessages: (messages) => {
421
+ inbox.push(...messages);
422
+ },
423
+ updateBadge,
424
+ maybeDrainInboxIfIdle,
425
+ requestRemoteControl,
426
+ runRemoteControl,
427
+ formatError: msg,
428
+ });
409
429
  const pinetActivityFormatting = createPinetActivityFormatting({
410
430
  getActiveBrokerDb,
411
431
  });
@@ -491,6 +511,20 @@ export default function (pi) {
491
511
  const handleBrokerAppHomeOpened = async (userId, ctx) => {
492
512
  await pinetHomeTabs.publishCurrentPinetHomeTabSafely(userId, ctx, new Date().toISOString());
493
513
  };
514
+ const handleBrokerSlackSlashCommand = async (event) => {
515
+ const commandNames = resolveSlackAgentCommandNames(settings, activeSkinTheme);
516
+ if (!isSlackAgentCommand(event.command, commandNames)) {
517
+ return null;
518
+ }
519
+ if (!isSlackAgentsListCommand(event.command, event.text, commandNames)) {
520
+ return formatSlackAgentsUsage(commandNames);
521
+ }
522
+ const snapshot = await buildCurrentBrokerControlPlaneDashboardSnapshot();
523
+ if (!snapshot) {
524
+ return "Pinet broker dashboard data is not available yet. Try again after the broker completes a maintenance cycle.";
525
+ }
526
+ return formatSlackAgentsDashboard(snapshot, shouldIncludeSlackAgentsGhosts(event.text));
527
+ };
494
528
  const slackPinetAdapterFactory = createSlackPinetRuntimeAdapterFactory({
495
529
  getSettings: () => settings,
496
530
  getBotToken: () => botToken,
@@ -498,6 +532,7 @@ export default function (pi) {
498
532
  getAllowedUsers: () => allowedUsers,
499
533
  shouldAllowAllWorkspaceUsers: () => resolveAllowAllWorkspaceUsers(settings, process.env.SLACK_ALLOW_ALL_WORKSPACE_USERS),
500
534
  onAppHomeOpened: handleBrokerAppHomeOpened,
535
+ onSlashCommand: handleBrokerSlackSlashCommand,
501
536
  });
502
537
  const brokerRuntime = createBrokerRuntime({
503
538
  getSettings: () => settings,
@@ -519,6 +554,11 @@ export default function (pi) {
519
554
  getMeshRoleFromMetadata: (metadata, fallbackRole) => getMeshRoleFromMetadata(metadata ?? undefined, fallbackRole),
520
555
  handleInboundMessage: async ({ message, broker, router, selfId, ctx }) => {
521
556
  try {
557
+ if (isStaleSlackMessage(message)) {
558
+ const ageMs = getSlackMessageAgeMs(message);
559
+ console.info(`[slack-bridge] skipped stale Slack inbound message older than 15m: channel=${message.channel} thread=${message.threadId} ts=${message.timestamp} age_ms=${ageMs ?? "unknown"}`);
560
+ return;
561
+ }
522
562
  const ownerHint = message.source === "slack" && message.threadId && message.channel
523
563
  ? await resolveBrokerThreadOwnerHint(message.channel, message.threadId)
524
564
  : null;
@@ -687,6 +727,7 @@ export default function (pi) {
687
727
  getActiveBrokerSelfId,
688
728
  getAgentName: () => agentName,
689
729
  getFollowerClient: () => brokerClient?.client ?? null,
730
+ sendSubtreeAgentMessage: (target, body, metadata) => subtreeBrokerRuntime.sendMessage(target, body, metadata),
690
731
  formatTrackedAgent,
691
732
  logActivity: (entry) => {
692
733
  brokerRuntime.logActivity(entry);
@@ -830,6 +871,10 @@ export default function (pi) {
830
871
  }, consumeConfirmationReply);
831
872
  }
832
873
  if (brokerRole === "follower" && brokerClient?.client) {
874
+ const subtreeResult = subtreeBrokerRuntime.readInbox(options);
875
+ if (subtreeResult && (subtreeResult.messages.length > 0 || Boolean(options.threadId))) {
876
+ return consumePinetReadConfirmationReplies(subtreeResult, consumeConfirmationReply);
877
+ }
833
878
  const result = await brokerClient.client.readInbox(options);
834
879
  return consumePinetReadConfirmationReplies(result, consumeConfirmationReply);
835
880
  }
@@ -872,6 +917,7 @@ export default function (pi) {
872
917
  }
873
918
  async function stopPinetRuntime(ctx, options) {
874
919
  flushPersist();
920
+ await subtreeBrokerRuntime.stop({ releaseIdentity: options.releaseIdentity });
875
921
  await brokerRuntime.disconnect({ releaseIdentity: options.releaseIdentity });
876
922
  if (brokerClient) {
877
923
  if (options.releaseIdentity) {
@@ -980,6 +1026,7 @@ export default function (pi) {
980
1026
  channel: input.channel,
981
1027
  content,
982
1028
  ...(input.blocks && input.blocks.length > 0 ? { blocks: input.blocks } : {}),
1029
+ ...(input.files && input.files.length > 0 ? { files: input.files } : {}),
983
1030
  agentName,
984
1031
  agentEmoji,
985
1032
  agentOwnerToken,
@@ -1003,6 +1050,7 @@ export default function (pi) {
1003
1050
  channel: input.channel,
1004
1051
  content,
1005
1052
  ...(input.blocks && input.blocks.length > 0 ? { blocks: input.blocks } : {}),
1053
+ ...(input.files && input.files.length > 0 ? { files: input.files } : {}),
1006
1054
  agentName,
1007
1055
  agentEmoji,
1008
1056
  agentOwnerToken,
@@ -1024,6 +1072,14 @@ export default function (pi) {
1024
1072
  readPinetInbox,
1025
1073
  listBrokerAgents,
1026
1074
  listFollowerAgents,
1075
+ listSubtreeAgents: (includeGhosts) => subtreeBrokerRuntime.listAgents(includeGhosts),
1076
+ getSubtreeSelfAgentId: () => subtreeBrokerRuntime.getStatus().selfAgentId,
1077
+ spawnSubtreeWorker: async (input) => {
1078
+ const activeCtx = sessionUiRuntime.getExtensionContext();
1079
+ if (!activeCtx)
1080
+ throw new Error("No active Pi extension context for subtree spawn.");
1081
+ return await subtreeBrokerRuntime.spawnWorker(activeCtx, input);
1082
+ },
1027
1083
  listPinetLanes,
1028
1084
  upsertPinetLane,
1029
1085
  setPinetLaneParticipant,
@@ -1234,11 +1290,15 @@ export default function (pi) {
1234
1290
  getBrokerControlPlaneHomeTabViewerIds,
1235
1291
  lastBrokerControlPlaneHomeTabRefreshAt: () => brokerRuntime.getLastHomeTabRefreshAt(),
1236
1292
  lastBrokerControlPlaneHomeTabError: () => brokerRuntime.getLastHomeTabError(),
1293
+ subtreeBrokerStatus: () => subtreeBrokerRuntime.getStatus(),
1237
1294
  getPinetRegistrationBlockReason: pinetRegistrationGate.getBlockReason,
1238
1295
  connectAsBroker: (ctx) => transitionToRuntimeMode(ctx, "broker"),
1239
1296
  connectAsFollower: (ctx) => transitionToRuntimeMode(ctx, "follower"),
1240
1297
  reloadPinetRuntime,
1241
1298
  disconnectFollower,
1299
+ startSubtreeBroker: (ctx) => subtreeBrokerRuntime.start(ctx),
1300
+ stopSubtreeBroker: () => subtreeBrokerRuntime.stop({ releaseIdentity: true }),
1301
+ spawnSubtreeWorker: (ctx, input) => subtreeBrokerRuntime.spawnWorker(ctx, input),
1242
1302
  sendPinetAgentMessage,
1243
1303
  signalAgentFree,
1244
1304
  applyLocalAgentIdentity,
@@ -4,6 +4,7 @@ import { type LoggedActivityLogEntry } from "./activity-log.js";
4
4
  import { type SlackScopeDiagnostics } from "./slack-scope-diagnostics.js";
5
5
  import type { SlackBridgeRuntimeMode } from "./runtime-mode.js";
6
6
  import type { RalphSnoozeStatus } from "./ralph-loop.js";
7
+ import type { SubtreeBrokerStatus, SubtreeSpawnInput, SubtreeSpawnResult } from "./subtree-broker-runtime.js";
7
8
  export interface PinetCommandsDeps {
8
9
  pinetEnabled: () => boolean;
9
10
  pinetRegistrationBlocked: () => boolean;
@@ -43,6 +44,7 @@ export interface PinetCommandsDeps {
43
44
  getBrokerControlPlaneHomeTabViewerIds: () => string[];
44
45
  lastBrokerControlPlaneHomeTabRefreshAt: () => string | null;
45
46
  lastBrokerControlPlaneHomeTabError: () => string | null;
47
+ subtreeBrokerStatus: () => SubtreeBrokerStatus;
46
48
  getPinetRegistrationBlockReason: () => string;
47
49
  connectAsBroker: (ctx: ExtensionContext) => Promise<void>;
48
50
  connectAsFollower: (ctx: ExtensionContext) => Promise<void>;
@@ -50,6 +52,9 @@ export interface PinetCommandsDeps {
50
52
  disconnectFollower: (ctx: ExtensionContext) => Promise<{
51
53
  unregisterError: string | null;
52
54
  }>;
55
+ startSubtreeBroker: (ctx: ExtensionContext) => Promise<SubtreeBrokerStatus>;
56
+ stopSubtreeBroker: () => Promise<void>;
57
+ spawnSubtreeWorker: (ctx: ExtensionContext, input: SubtreeSpawnInput) => Promise<SubtreeSpawnResult>;
53
58
  sendPinetAgentMessage: (target: string, body: string) => Promise<{
54
59
  messageId: number;
55
60
  target: string;
@@ -64,7 +69,7 @@ export interface PinetCommandsDeps {
64
69
  setExtStatus: (ctx: ExtensionContext, state: "ok" | "reconnecting" | "error" | "off") => void;
65
70
  setExtCtx: (ctx: ExtensionContext) => void;
66
71
  }
67
- export type PinetCommandAction = "start" | "follow" | "unfollow" | "reload" | "exit" | "free" | "status" | "logs" | "rename" | "snooze";
72
+ export type PinetCommandAction = "start" | "follow" | "unfollow" | "reload" | "exit" | "free" | "status" | "logs" | "rename" | "snooze" | "subtree";
68
73
  export declare function formatPinetCommandHelp(): string;
69
74
  export declare function runPinetCommandAction(deps: PinetCommandsDeps, action: PinetCommandAction, args: string, ctx: ExtensionContext, usageCommand?: string): Promise<void>;
70
75
  export declare function registerPinetCommands(pi: ExtensionAPI, deps: PinetCommandsDeps): void;
@@ -11,6 +11,11 @@ const PINET_PRIMARY_COMMANDS = [
11
11
  { action: "exit", args: "<agent>", description: "Ask another agent to exit" },
12
12
  { action: "free", args: "", description: "Mark this agent as idle" },
13
13
  { action: "snooze", args: "[duration|off|status]", description: "Quiet empty RALPH cycles" },
14
+ {
15
+ action: "subtree",
16
+ args: "[start|status|spawn|stop]",
17
+ description: "Run this worker as a subtree broker for child followers",
18
+ },
14
19
  ];
15
20
  const PINET_SECONDARY_COMMANDS = [
16
21
  { action: "status", args: "", description: "Show Pinet status" },
@@ -89,6 +94,9 @@ function normalizePinetCommandAction(rawAction) {
89
94
  case "snooze":
90
95
  case "quiet":
91
96
  return "snooze";
97
+ case "subtree":
98
+ case "subbroker":
99
+ return "subtree";
92
100
  case "help":
93
101
  return null;
94
102
  default:
@@ -127,11 +135,14 @@ export async function runPinetCommandAction(deps, action, args, ctx, usageComman
127
135
  case "snooze":
128
136
  runPinetSnooze(deps, args, ctx);
129
137
  return;
138
+ case "subtree":
139
+ await runPinetSubtree(deps, args, ctx);
140
+ return;
130
141
  }
131
142
  }
132
143
  export function registerPinetCommands(pi, deps) {
133
144
  pi.registerCommand("pinet", {
134
- description: "Unified Pinet command surface: start, follow, unfollow, reload, exit, free, snooze, status, logs, rename",
145
+ description: "Unified Pinet command surface: start, follow, unfollow, reload, exit, free, snooze, subtree, status, logs, rename",
135
146
  handler: async (args, ctx) => {
136
147
  const parsed = parsePinetCommandAction(args);
137
148
  if (!parsed) {
@@ -318,6 +329,152 @@ function runPinetSnooze(deps, args, ctx) {
318
329
  });
319
330
  ctx.ui.notify(formatRalphSnoozeStatus(status), "info");
320
331
  }
332
+ function formatSubtreeBrokerStatus(status) {
333
+ if (!status.active || !status.paths || !status.selfAgentId) {
334
+ return "Subtree broker: off\nUse /pinet subtree start from a worker that is already following the central broker.";
335
+ }
336
+ const envLines = Object.entries(status.childLaunchEnv).map(([key, value]) => `${key}=${value}`);
337
+ return [
338
+ "Subtree broker: running",
339
+ `Self agent: ${status.selfAgentId}`,
340
+ `Started: ${status.startedAt ?? "unknown"}`,
341
+ `Socket: ${status.paths.socketPath}`,
342
+ `Database: ${status.paths.dbPath}`,
343
+ `Lock: ${status.paths.lockPath}`,
344
+ `Children: ${status.childCount}`,
345
+ ...(status.spawnedWorkers.length > 0
346
+ ? [
347
+ "Spawned workers:",
348
+ ...status.spawnedWorkers.map((worker) => `- ${worker.sessionName} role=${worker.role} agent=${worker.agentId ?? "pending"} repo=${worker.repoPath}`),
349
+ ]
350
+ : []),
351
+ "Child follower environment:",
352
+ ...envLines,
353
+ ...(status.childLaunchHint ? ["Child launch hint:", status.childLaunchHint] : []),
354
+ ].join("\n");
355
+ }
356
+ function parseSubtreeSpawnArgs(args) {
357
+ const tokens = args
358
+ .trim()
359
+ .split(/\s+/)
360
+ .map((token) => token.trim())
361
+ .filter(Boolean);
362
+ let repo = null;
363
+ let role;
364
+ let laneId;
365
+ const taskParts = [];
366
+ for (let index = 0; index < tokens.length; index += 1) {
367
+ const token = tokens[index];
368
+ const next = tokens[index + 1];
369
+ if (token === "--") {
370
+ taskParts.push(...tokens.slice(index + 1));
371
+ break;
372
+ }
373
+ if (token === "--repo" || token === "repo") {
374
+ if (!next)
375
+ return null;
376
+ repo = next;
377
+ index += 1;
378
+ continue;
379
+ }
380
+ if (token === "--role" || token === "role") {
381
+ if (!next)
382
+ return null;
383
+ role = next;
384
+ index += 1;
385
+ continue;
386
+ }
387
+ if (token === "--lane" || token === "--lane-id" || token === "lane" || token === "lane_id") {
388
+ if (!next)
389
+ return null;
390
+ laneId = next;
391
+ index += 1;
392
+ continue;
393
+ }
394
+ if (token.startsWith("repo=")) {
395
+ repo = token.slice("repo=".length);
396
+ continue;
397
+ }
398
+ if (token.startsWith("role=")) {
399
+ role = token.slice("role=".length);
400
+ continue;
401
+ }
402
+ if (token.startsWith("lane=")) {
403
+ laneId = token.slice("lane=".length);
404
+ continue;
405
+ }
406
+ if (token.startsWith("lane_id=")) {
407
+ laneId = token.slice("lane_id=".length);
408
+ continue;
409
+ }
410
+ taskParts.push(token);
411
+ }
412
+ const task = taskParts.join(" ").trim();
413
+ if (!repo || !task)
414
+ return null;
415
+ return {
416
+ repo,
417
+ task,
418
+ ...(role ? { role } : {}),
419
+ ...(laneId ? { laneId } : {}),
420
+ };
421
+ }
422
+ function formatSubtreeSpawnResult(result) {
423
+ return [
424
+ "Subtree worker started",
425
+ `Agent: ${result.agentName} (${result.agentId})`,
426
+ `Session: ${result.sessionName}`,
427
+ `Repo: ${result.repoPath}`,
428
+ `Role: ${result.role}`,
429
+ ...(result.laneId ? [`Lane: ${result.laneId}`] : []),
430
+ `Task message: ${result.messageId}`,
431
+ `Thread: ${result.threadId}`,
432
+ `Monitor: ${result.monitorCommand}`,
433
+ ].join("\n");
434
+ }
435
+ async function runPinetSubtree(deps, args, ctx) {
436
+ const [rawSubcommand = "status"] = args.trim().split(/\s+/);
437
+ const subcommand = rawSubcommand.toLowerCase();
438
+ if (["status", "show", "info", ""].includes(subcommand)) {
439
+ ctx.ui.notify(formatSubtreeBrokerStatus(deps.subtreeBrokerStatus()), "info");
440
+ return;
441
+ }
442
+ if (["stop", "off", "down"].includes(subcommand)) {
443
+ await deps.stopSubtreeBroker();
444
+ ctx.ui.notify("Subtree broker stopped. Spawned child followers were asked to exit.", "info");
445
+ return;
446
+ }
447
+ if (deps.runtimeMode() !== "follower" || deps.brokerRole() !== "follower") {
448
+ ctx.ui.notify("Subtree broker operations require this session to be running as a Pinet worker/follower. Run /pinet follow first.", "warning");
449
+ return;
450
+ }
451
+ if (["spawn", "child", "worker"].includes(subcommand)) {
452
+ const spawnInput = parseSubtreeSpawnArgs(args.trim().slice(rawSubcommand.length));
453
+ if (!spawnInput) {
454
+ ctx.ui.notify("Usage: /pinet subtree spawn repo=<repo-or-path> [role=<role>] [lane=<lane>] <task>", "warning");
455
+ return;
456
+ }
457
+ try {
458
+ const result = await deps.spawnSubtreeWorker(ctx, spawnInput);
459
+ ctx.ui.notify(formatSubtreeSpawnResult(result), "info");
460
+ }
461
+ catch (err) {
462
+ ctx.ui.notify(`Subtree worker spawn failed: ${errorMsg(err)}`, "error");
463
+ }
464
+ return;
465
+ }
466
+ if (!["start", "on", "broker", "promote"].includes(subcommand)) {
467
+ ctx.ui.notify("Usage: /pinet subtree [start|status|spawn|stop]", "warning");
468
+ return;
469
+ }
470
+ try {
471
+ const status = await deps.startSubtreeBroker(ctx);
472
+ ctx.ui.notify(formatSubtreeBrokerStatus(status), "info");
473
+ }
474
+ catch (err) {
475
+ ctx.ui.notify(`Subtree broker start failed: ${errorMsg(err)}`, "error");
476
+ }
477
+ }
321
478
  function runPinetStatus(deps, ctx) {
322
479
  const mode = deps.runtimeMode();
323
480
  const ownedCount = [...deps.threads().values()].filter((t) => agentOwnsThread(t.owner, deps.agentName(), deps.agentAliases(), deps.agentOwnerToken())).length;
@@ -357,6 +514,13 @@ function runPinetStatus(deps, ctx) {
357
514
  : []),
358
515
  ]
359
516
  : [];
517
+ const subtreeStatus = deps.subtreeBrokerStatus();
518
+ const subtreeBrokerInfo = subtreeStatus.active
519
+ ? [
520
+ `Subtree broker: running (${subtreeStatus.selfAgentId ?? "unknown"})`,
521
+ ...(subtreeStatus.paths ? [`Subtree socket: ${subtreeStatus.paths.socketPath}`] : []),
522
+ ]
523
+ : [];
360
524
  ctx.ui.notify([
361
525
  `Mode: ${mode}`,
362
526
  `Agent: ${deps.agentEmoji()} ${deps.agentName()}`,
@@ -376,6 +540,7 @@ function runPinetStatus(deps, ctx) {
376
540
  ...brokerHealthInfo,
377
541
  ...ralphSnoozeInfo,
378
542
  ...brokerHomeTabInfo,
543
+ ...subtreeBrokerInfo,
379
544
  ].join("\n"), "info");
380
545
  }
381
546
  function runPinetLogs(deps, ctx) {
@@ -14,6 +14,12 @@ export interface PinetMeshOpsAgentRecord {
14
14
  resumableUntil?: string | null;
15
15
  outboundCount?: number;
16
16
  pendingInboxCount?: number;
17
+ parentAgentId?: string | null;
18
+ rootAgentId?: string | null;
19
+ treeDepth?: number;
20
+ supervisionState?: string;
21
+ subtreeRole?: string | null;
22
+ laneId?: string | null;
17
23
  }
18
24
  export interface PinetMeshOpsRecordedAssignment {
19
25
  issueNumber: number;
@@ -64,6 +70,11 @@ export interface PinetMeshOpsDeps {
64
70
  getActiveBrokerSelfId: () => string | null;
65
71
  getAgentName: () => string;
66
72
  getFollowerClient: () => PinetMeshOpsFollowerClientPort | null;
73
+ sendSubtreeAgentMessage?: (target: string, body: string, metadata?: Record<string, unknown>) => Promise<{
74
+ messageId: number;
75
+ target: string;
76
+ threadId: string;
77
+ } | null>;
67
78
  formatTrackedAgent: (agentId: string) => string;
68
79
  logActivity: (entry: ActivityLogEntry) => void;
69
80
  }
@@ -109,6 +109,7 @@ export function createPinetMeshOps(deps) {
109
109
  target: targetRef,
110
110
  body: dispatchBody,
111
111
  metadata: dispatchMetadata,
112
+ trustedBrokerAgentId: selfId,
112
113
  });
113
114
  if (transferThreadId) {
114
115
  const transfer = db.transferThreadOwnership(transferThreadId, result.target.id);
@@ -171,6 +172,10 @@ export function createPinetMeshOps(deps) {
171
172
  };
172
173
  }
173
174
  if (deps.getBrokerRole() === "follower") {
175
+ const subtreeResult = await deps.sendSubtreeAgentMessage?.(targetRef, finalBody, finalMetadata);
176
+ if (subtreeResult) {
177
+ return { messageId: subtreeResult.messageId, target: subtreeResult.target };
178
+ }
174
179
  const client = deps.getFollowerClient();
175
180
  if (!client) {
176
181
  throw new Error("Pinet is in an unexpected state.");
@@ -233,6 +238,12 @@ export function createPinetMeshOps(deps) {
233
238
  resumableUntil: agent.resumableUntil,
234
239
  outboundCount: agent.outboundCount,
235
240
  pendingInboxCount: db.getPendingInboxCount(agent.id),
241
+ parentAgentId: agent.parentAgentId,
242
+ rootAgentId: agent.rootAgentId,
243
+ treeDepth: agent.treeDepth,
244
+ supervisionState: agent.supervisionState,
245
+ subtreeRole: agent.subtreeRole,
246
+ laneId: agent.laneId,
236
247
  }));
237
248
  }
238
249
  async function listFollowerAgents(includeGhosts) {
@@ -253,6 +264,12 @@ export function createPinetMeshOps(deps) {
253
264
  resumableUntil: agent.resumableUntil,
254
265
  outboundCount: agent.outboundCount,
255
266
  pendingInboxCount: agent.pendingInboxCount,
267
+ parentAgentId: agent.parentAgentId,
268
+ rootAgentId: agent.rootAgentId,
269
+ treeDepth: agent.treeDepth,
270
+ supervisionState: agent.supervisionState,
271
+ subtreeRole: agent.subtreeRole,
272
+ laneId: agent.laneId,
256
273
  }));
257
274
  }
258
275
  return {
@@ -15,6 +15,34 @@ export interface PinetToolsAgentRecord {
15
15
  resumableUntil?: string | null;
16
16
  outboundCount?: number;
17
17
  pendingInboxCount?: number;
18
+ parentAgentId?: string | null;
19
+ rootAgentId?: string | null;
20
+ treeDepth?: number;
21
+ supervisionState?: string;
22
+ subtreeRole?: string | null;
23
+ laneId?: string | null;
24
+ }
25
+ export interface PinetSubtreeSpawnInput {
26
+ task: string;
27
+ repo: string;
28
+ role?: string;
29
+ laneId?: string;
30
+ }
31
+ export interface PinetSubtreeSpawnResult {
32
+ status: "started";
33
+ launchId: string;
34
+ sessionName: string;
35
+ repoPath: string;
36
+ role: string;
37
+ laneId: string | null;
38
+ agentId: string;
39
+ agentName: string;
40
+ messageId: number;
41
+ threadId: string;
42
+ monitorCommand: string;
43
+ socketPath: string;
44
+ dbPath: string;
45
+ childLaunchEnv: Record<string, string>;
18
46
  }
19
47
  export interface RegisterPinetToolsDeps {
20
48
  pinetEnabled: () => boolean;
@@ -48,6 +76,9 @@ export interface RegisterPinetToolsDeps {
48
76
  readPinetInbox: (options: PinetReadOptions) => Promise<PinetReadResult>;
49
77
  listBrokerAgents: () => PinetToolsAgentRecord[];
50
78
  listFollowerAgents: (includeGhosts: boolean) => Promise<PinetToolsAgentRecord[]>;
79
+ listSubtreeAgents?: (includeGhosts: boolean) => PinetToolsAgentRecord[] | null;
80
+ getSubtreeSelfAgentId?: () => string | null;
81
+ spawnSubtreeWorker?: (input: PinetSubtreeSpawnInput) => Promise<PinetSubtreeSpawnResult>;
51
82
  listPinetLanes: (options: PinetLaneListOptions) => Promise<PinetLaneInfo[]>;
52
83
  upsertPinetLane: (input: PinetLaneUpsertInput) => Promise<PinetLaneInfo>;
53
84
  setPinetLaneParticipant: (input: PinetLaneParticipantUpsertInput) => Promise<PinetLaneParticipantInfo>;
@@ -64,4 +95,20 @@ export interface RegisterPinetToolsDeps {
64
95
  }) => RalphSnoozeStatus;
65
96
  clearRalphSnooze?: () => RalphSnoozeStatus;
66
97
  }
98
+ type PinetDispatcherStatus = "succeeded" | "failed";
99
+ interface PinetRenderContentBlock {
100
+ type: string;
101
+ text?: string;
102
+ }
103
+ interface PinetRenderResultInput {
104
+ content?: PinetRenderContentBlock[];
105
+ details?: unknown;
106
+ expandedText?: string;
107
+ displayText?: string;
108
+ }
109
+ export declare function formatPinetDispatcherResultForDisplay(result: PinetRenderResultInput, expanded: boolean): {
110
+ status: PinetDispatcherStatus | "unknown";
111
+ text: string;
112
+ };
67
113
  export declare function registerPinetTools(pi: ExtensionAPI, deps: RegisterPinetToolsDeps): void;
114
+ export {};