@pinet/slack-bridge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +570 -0
  3. package/dist/activity-log.d.ts +62 -0
  4. package/dist/activity-log.js +293 -0
  5. package/dist/agent-completion-runtime.d.ts +17 -0
  6. package/dist/agent-completion-runtime.js +26 -0
  7. package/dist/agent-event-runtime.d.ts +13 -0
  8. package/dist/agent-event-runtime.js +27 -0
  9. package/dist/agent-prompt-guidance.d.ts +22 -0
  10. package/dist/agent-prompt-guidance.js +46 -0
  11. package/dist/broker/adapters/slack.d.ts +74 -0
  12. package/dist/broker/adapters/slack.js +563 -0
  13. package/dist/broker/adapters/types.d.ts +1 -0
  14. package/dist/broker/adapters/types.js +1 -0
  15. package/dist/broker/agent-messaging.d.ts +1 -0
  16. package/dist/broker/agent-messaging.js +1 -0
  17. package/dist/broker/auth.d.ts +1 -0
  18. package/dist/broker/auth.js +1 -0
  19. package/dist/broker/client.d.ts +167 -0
  20. package/dist/broker/client.js +624 -0
  21. package/dist/broker/control-plane-dashboard.d.ts +98 -0
  22. package/dist/broker/control-plane-dashboard.js +213 -0
  23. package/dist/broker/ghost-reaper.d.ts +49 -0
  24. package/dist/broker/ghost-reaper.js +209 -0
  25. package/dist/broker/index.d.ts +36 -0
  26. package/dist/broker/index.js +90 -0
  27. package/dist/broker/leader.d.ts +1 -0
  28. package/dist/broker/leader.js +1 -0
  29. package/dist/broker/maintenance.d.ts +1 -0
  30. package/dist/broker/maintenance.js +1 -0
  31. package/dist/broker/message-send.d.ts +1 -0
  32. package/dist/broker/message-send.js +1 -0
  33. package/dist/broker/paths.d.ts +1 -0
  34. package/dist/broker/paths.js +1 -0
  35. package/dist/broker/raw-tcp-loopback.d.ts +1 -0
  36. package/dist/broker/raw-tcp-loopback.js +1 -0
  37. package/dist/broker/router.d.ts +1 -0
  38. package/dist/broker/router.js +1 -0
  39. package/dist/broker/schema.d.ts +9 -0
  40. package/dist/broker/schema.js +63 -0
  41. package/dist/broker/socket-server.d.ts +120 -0
  42. package/dist/broker/socket-server.js +1087 -0
  43. package/dist/broker/types.d.ts +1 -0
  44. package/dist/broker/types.js +1 -0
  45. package/dist/broker-delivery.d.ts +10 -0
  46. package/dist/broker-delivery.js +26 -0
  47. package/dist/broker-inbound-persistence.d.ts +11 -0
  48. package/dist/broker-inbound-persistence.js +22 -0
  49. package/dist/broker-prompt-loader.d.ts +34 -0
  50. package/dist/broker-prompt-loader.js +185 -0
  51. package/dist/broker-runtime-access.d.ts +14 -0
  52. package/dist/broker-runtime-access.js +20 -0
  53. package/dist/broker-runtime.d.ts +100 -0
  54. package/dist/broker-runtime.js +533 -0
  55. package/dist/broker-thread-owner-hints.d.ts +10 -0
  56. package/dist/broker-thread-owner-hints.js +14 -0
  57. package/dist/canvases.d.ts +80 -0
  58. package/dist/canvases.js +221 -0
  59. package/dist/command-registration-runtime.d.ts +9 -0
  60. package/dist/command-registration-runtime.js +9 -0
  61. package/dist/core-tool-guardrails.d.ts +24 -0
  62. package/dist/core-tool-guardrails.js +66 -0
  63. package/dist/deploy-manifest.d.ts +27 -0
  64. package/dist/deploy-manifest.js +197 -0
  65. package/dist/follower-delivery.d.ts +16 -0
  66. package/dist/follower-delivery.js +70 -0
  67. package/dist/follower-runtime.d.ts +61 -0
  68. package/dist/follower-runtime.js +350 -0
  69. package/dist/git-metadata.d.ts +35 -0
  70. package/dist/git-metadata.js +88 -0
  71. package/dist/guardrails.d.ts +69 -0
  72. package/dist/guardrails.js +283 -0
  73. package/dist/helpers.d.ts +550 -0
  74. package/dist/helpers.js +2711 -0
  75. package/dist/home-tab.d.ts +27 -0
  76. package/dist/home-tab.js +242 -0
  77. package/dist/imessage-tools.d.ts +33 -0
  78. package/dist/imessage-tools.js +96 -0
  79. package/dist/inbox-drain-runtime.d.ts +30 -0
  80. package/dist/inbox-drain-runtime.js +71 -0
  81. package/dist/index.d.ts +2 -0
  82. package/dist/index.js +1330 -0
  83. package/dist/persisted-runtime-state.d.ts +52 -0
  84. package/dist/persisted-runtime-state.js +98 -0
  85. package/dist/pinet-activity-formatting.d.ts +20 -0
  86. package/dist/pinet-activity-formatting.js +43 -0
  87. package/dist/pinet-agent-status.d.ts +36 -0
  88. package/dist/pinet-agent-status.js +48 -0
  89. package/dist/pinet-commands.d.ts +70 -0
  90. package/dist/pinet-commands.js +402 -0
  91. package/dist/pinet-confirmation-replies.d.ts +7 -0
  92. package/dist/pinet-confirmation-replies.js +30 -0
  93. package/dist/pinet-control-plane-dashboard.d.ts +34 -0
  94. package/dist/pinet-control-plane-dashboard.js +86 -0
  95. package/dist/pinet-home-tabs.d.ts +36 -0
  96. package/dist/pinet-home-tabs.js +101 -0
  97. package/dist/pinet-maintenance-delivery.d.ts +22 -0
  98. package/dist/pinet-maintenance-delivery.js +45 -0
  99. package/dist/pinet-mesh-ops.d.ts +97 -0
  100. package/dist/pinet-mesh-ops.js +266 -0
  101. package/dist/pinet-registration-gate.d.ts +14 -0
  102. package/dist/pinet-registration-gate.js +42 -0
  103. package/dist/pinet-remote-control-acks.d.ts +16 -0
  104. package/dist/pinet-remote-control-acks.js +46 -0
  105. package/dist/pinet-remote-control.d.ts +17 -0
  106. package/dist/pinet-remote-control.js +74 -0
  107. package/dist/pinet-runtime-composition.d.ts +27 -0
  108. package/dist/pinet-runtime-composition.js +20 -0
  109. package/dist/pinet-tools.d.ts +67 -0
  110. package/dist/pinet-tools.js +1211 -0
  111. package/dist/prompts/broker/default.md +41 -0
  112. package/dist/prompts/broker/tmux.md +49 -0
  113. package/dist/ralph-loop.d.ts +76 -0
  114. package/dist/ralph-loop.js +524 -0
  115. package/dist/reaction-triggers.d.ts +25 -0
  116. package/dist/reaction-triggers.js +171 -0
  117. package/dist/repo-tool-guardrails.d.ts +20 -0
  118. package/dist/repo-tool-guardrails.js +81 -0
  119. package/dist/runtime-agent-context.d.ts +93 -0
  120. package/dist/runtime-agent-context.js +302 -0
  121. package/dist/runtime-mode.d.ts +10 -0
  122. package/dist/runtime-mode.js +39 -0
  123. package/dist/scheduled-wakeups.d.ts +1 -0
  124. package/dist/scheduled-wakeups.js +1 -0
  125. package/dist/session-ui-runtime.d.ts +20 -0
  126. package/dist/session-ui-runtime.js +163 -0
  127. package/dist/single-player-runtime.d.ts +71 -0
  128. package/dist/single-player-runtime.js +436 -0
  129. package/dist/skins/cosmere.json +1571 -0
  130. package/dist/skins/foundation.json +1304 -0
  131. package/dist/slack-access.d.ts +186 -0
  132. package/dist/slack-access.js +462 -0
  133. package/dist/slack-api.d.ts +2 -0
  134. package/dist/slack-api.js +1 -0
  135. package/dist/slack-block-kit.d.ts +36 -0
  136. package/dist/slack-block-kit.js +242 -0
  137. package/dist/slack-export.d.ts +31 -0
  138. package/dist/slack-export.js +206 -0
  139. package/dist/slack-message-context.d.ts +15 -0
  140. package/dist/slack-message-context.js +207 -0
  141. package/dist/slack-modals.d.ts +13 -0
  142. package/dist/slack-modals.js +79 -0
  143. package/dist/slack-pinet-runtime-adapter.d.ts +16 -0
  144. package/dist/slack-pinet-runtime-adapter.js +72 -0
  145. package/dist/slack-presence.d.ts +36 -0
  146. package/dist/slack-presence.js +89 -0
  147. package/dist/slack-request-runtime.d.ts +8 -0
  148. package/dist/slack-request-runtime.js +19 -0
  149. package/dist/slack-runtime-access.d.ts +28 -0
  150. package/dist/slack-runtime-access.js +107 -0
  151. package/dist/slack-scope-diagnostics.d.ts +31 -0
  152. package/dist/slack-scope-diagnostics.js +235 -0
  153. package/dist/slack-socket-dedup.d.ts +7 -0
  154. package/dist/slack-socket-dedup.js +135 -0
  155. package/dist/slack-thread-status.d.ts +41 -0
  156. package/dist/slack-thread-status.js +156 -0
  157. package/dist/slack-tool-policy-runtime.d.ts +36 -0
  158. package/dist/slack-tool-policy-runtime.js +96 -0
  159. package/dist/slack-tools.d.ts +52 -0
  160. package/dist/slack-tools.js +2688 -0
  161. package/dist/slack-turn-guardrails.d.ts +19 -0
  162. package/dist/slack-turn-guardrails.js +36 -0
  163. package/dist/slack-upload.d.ts +49 -0
  164. package/dist/slack-upload.js +224 -0
  165. package/dist/task-assignments.d.ts +45 -0
  166. package/dist/task-assignments.js +527 -0
  167. package/dist/thread-confirmations.d.ts +22 -0
  168. package/dist/thread-confirmations.js +139 -0
  169. package/dist/tool-registration-runtime.d.ts +13 -0
  170. package/dist/tool-registration-runtime.js +13 -0
  171. package/dist/ttl-cache.d.ts +47 -0
  172. package/dist/ttl-cache.js +105 -0
  173. package/manifest.yaml +57 -0
  174. package/package.json +60 -0
  175. package/skills/pinet-skin-creator/SKILL.md +109 -0
  176. package/skills/pinet-skin-creator/references/descriptor-format.md +99 -0
  177. package/skills/pinet-skin-creator/references/safety-checklist.md +56 -0
  178. package/skills/pinet-skin-creator/templates/pinet-skin-descriptor.json +77 -0
  179. package/skills/slack-bridge/SKILL.md +360 -0
@@ -0,0 +1,101 @@
1
+ import { probeGitBranch } from "./git-metadata.js";
2
+ import { publishSlackHomeTab, renderBrokerControlPlaneHomeTabView, renderStandalonePinetHomeTabView, } from "./home-tab.js";
3
+ function buildHomeTabPublishFailureMessage(formatError, err) {
4
+ return `Pinet Home tab publish failed: ${formatError(err)}`;
5
+ }
6
+ function getConnectedState(deps) {
7
+ const mode = deps.getRuntimeMode();
8
+ const brokerHomeTabs = deps.getBrokerHomeTabs();
9
+ if (mode === "broker") {
10
+ return brokerHomeTabs.isConnected();
11
+ }
12
+ if (mode === "follower") {
13
+ return deps.isFollowerConnected();
14
+ }
15
+ if (mode === "single") {
16
+ return deps.isSinglePlayerConnected();
17
+ }
18
+ return false;
19
+ }
20
+ export function createPinetHomeTabs(deps) {
21
+ function reportHomeTabPublishFailure(ctx, err) {
22
+ const brokerHomeTabs = deps.getBrokerHomeTabs();
23
+ const homeTabMessage = buildHomeTabPublishFailureMessage(deps.formatError, err);
24
+ if (homeTabMessage !== brokerHomeTabs.getLastHomeTabError()) {
25
+ ctx.ui.notify(homeTabMessage, "warning");
26
+ }
27
+ brokerHomeTabs.setLastHomeTabError(homeTabMessage);
28
+ }
29
+ async function refreshBrokerControlPlaneHomeTabs(ctx, snapshot, refreshedAt, userIds = deps.getBrokerHomeTabs().getHomeTabViewerIds()) {
30
+ const botToken = deps.getBotToken();
31
+ if (!botToken || userIds.length === 0) {
32
+ return;
33
+ }
34
+ const brokerHomeTabs = deps.getBrokerHomeTabs();
35
+ brokerHomeTabs.setLastHomeTabSnapshot(snapshot);
36
+ let hadError = false;
37
+ for (const userId of userIds) {
38
+ try {
39
+ await publishSlackHomeTab({
40
+ slack: deps.slack,
41
+ token: botToken,
42
+ userId,
43
+ view: renderBrokerControlPlaneHomeTabView(snapshot),
44
+ });
45
+ }
46
+ catch (err) {
47
+ hadError = true;
48
+ reportHomeTabPublishFailure(ctx, err);
49
+ }
50
+ }
51
+ if (!hadError) {
52
+ brokerHomeTabs.setLastHomeTabError(null);
53
+ }
54
+ brokerHomeTabs.setLastHomeTabRefreshAt(refreshedAt);
55
+ }
56
+ async function publishCurrentPinetHomeTab(userId, ctx, openedAt = new Date().toISOString()) {
57
+ const botToken = deps.getBotToken();
58
+ if (!botToken) {
59
+ return;
60
+ }
61
+ const brokerHomeTabs = deps.getBrokerHomeTabs();
62
+ if (brokerHomeTabs.isConnected() && deps.getBrokerRole() === "broker") {
63
+ if (await brokerHomeTabs.publishCurrentHomeTabSafely(userId, ctx, openedAt)) {
64
+ return;
65
+ }
66
+ }
67
+ const currentBranch = deps.getCurrentBranch
68
+ ? await deps.getCurrentBranch()
69
+ : ((await probeGitBranch(process.cwd())) ?? null);
70
+ await publishSlackHomeTab({
71
+ slack: deps.slack,
72
+ token: botToken,
73
+ userId,
74
+ view: renderStandalonePinetHomeTabView({
75
+ agentName: deps.getAgentName(),
76
+ agentEmoji: deps.getAgentEmoji(),
77
+ connected: getConnectedState(deps),
78
+ mode: deps.getRuntimeMode(),
79
+ activeThreads: deps.getActiveThreads(),
80
+ pendingInbox: deps.getPendingInboxCount(),
81
+ currentBranch,
82
+ defaultChannel: deps.getDefaultChannel() ?? null,
83
+ }),
84
+ });
85
+ brokerHomeTabs.setLastHomeTabError(null);
86
+ }
87
+ async function publishCurrentPinetHomeTabSafely(userId, ctx, openedAt = new Date().toISOString()) {
88
+ try {
89
+ await publishCurrentPinetHomeTab(userId, ctx, openedAt);
90
+ }
91
+ catch (err) {
92
+ reportHomeTabPublishFailure(ctx, err);
93
+ }
94
+ }
95
+ return {
96
+ refreshBrokerControlPlaneHomeTabs,
97
+ reportHomeTabPublishFailure,
98
+ publishCurrentPinetHomeTab,
99
+ publishCurrentPinetHomeTabSafely,
100
+ };
101
+ }
@@ -0,0 +1,22 @@
1
+ export interface PinetMaintenanceDeliveryAgentRecord {
2
+ id: string;
3
+ name?: string;
4
+ emoji?: string;
5
+ }
6
+ export interface PinetMaintenanceDeliveryBrokerDbPort {
7
+ getAgentById: (agentId: string) => PinetMaintenanceDeliveryAgentRecord | null;
8
+ getThread: (threadId: string) => unknown;
9
+ createThread: (threadId: string, source: string, channel: string, owner: string) => void;
10
+ insertMessage: (threadId: string, source: string, direction: "outbound" | "inbound", sender: string, body: string, recipients: string[], metadata?: Record<string, unknown>) => void;
11
+ }
12
+ export interface PinetMaintenanceDeliveryDeps {
13
+ getActiveBrokerDb: () => PinetMaintenanceDeliveryBrokerDbPort | null;
14
+ getActiveBrokerSelfId: () => string | null;
15
+ isIdle: () => boolean;
16
+ sendUserMessage: (body: string) => void;
17
+ }
18
+ export interface PinetMaintenanceDelivery {
19
+ sendBrokerMaintenanceMessage: (targetAgentId: string, body: string) => void;
20
+ trySendBrokerFollowUp: (body: string, onDelivered: () => void) => void;
21
+ }
22
+ export declare function createPinetMaintenanceDelivery(deps: PinetMaintenanceDeliveryDeps): PinetMaintenanceDelivery;
@@ -0,0 +1,45 @@
1
+ function formatBrokerOnlyMaintenanceMessage(target, body) {
2
+ const label = [target.emoji, target.name].filter(Boolean).join(" ") || target.id;
3
+ return [
4
+ `RALPH broker-only maintenance for ${label} (${target.id}):`,
5
+ "",
6
+ "Original worker-directed maintenance note (not delivered to the worker/follower Pi queue):",
7
+ body,
8
+ ].join("\n");
9
+ }
10
+ export function createPinetMaintenanceDelivery(deps) {
11
+ function sendBrokerMaintenanceMessage(targetAgentId, body) {
12
+ const db = deps.getActiveBrokerDb();
13
+ const selfId = deps.getActiveBrokerSelfId();
14
+ if (!db || !selfId)
15
+ return;
16
+ const target = db.getAgentById(targetAgentId);
17
+ if (!target)
18
+ return;
19
+ const threadId = `a2a:${selfId}:${selfId}`;
20
+ if (!db.getThread(threadId)) {
21
+ db.createThread(threadId, "agent", "", selfId);
22
+ }
23
+ db.insertMessage(threadId, "agent", "outbound", selfId, formatBrokerOnlyMaintenanceMessage(target, body), [selfId], {
24
+ kind: "ralph_loop_nudge",
25
+ targetAgentId: target.id,
26
+ brokerOnly: true,
27
+ });
28
+ }
29
+ function trySendBrokerFollowUp(body, onDelivered) {
30
+ if (!deps.isIdle()) {
31
+ return;
32
+ }
33
+ try {
34
+ deps.sendUserMessage(body);
35
+ onDelivered();
36
+ }
37
+ catch {
38
+ /* best effort */
39
+ }
40
+ }
41
+ return {
42
+ sendBrokerMaintenanceMessage,
43
+ trySendBrokerFollowUp,
44
+ };
45
+ }
@@ -0,0 +1,97 @@
1
+ import { type AgentMessageStorage } from "./broker/agent-messaging.js";
2
+ import type { AgentInfo, TaskAssignmentInfo, TaskAssignmentKind } from "./broker/types.js";
3
+ import type { ActivityLogEntry } from "./activity-log.js";
4
+ export interface PinetMeshOpsAgentRecord {
5
+ emoji: string;
6
+ name: string;
7
+ id: string;
8
+ pid?: number;
9
+ status: "working" | "idle";
10
+ metadata: Record<string, unknown> | null;
11
+ lastHeartbeat: string;
12
+ lastSeen?: string;
13
+ disconnectedAt?: string | null;
14
+ resumableUntil?: string | null;
15
+ outboundCount?: number;
16
+ pendingInboxCount?: number;
17
+ }
18
+ export interface PinetMeshOpsRecordedAssignment {
19
+ issueNumber: number;
20
+ branch: string | null;
21
+ }
22
+ export interface PinetMeshOpsTransferableThread {
23
+ threadId: string;
24
+ source?: string;
25
+ channel?: string;
26
+ }
27
+ export interface PinetMeshOpsBrokerDbPort extends AgentMessageStorage {
28
+ getAllAgents: () => AgentInfo[];
29
+ getPendingInboxCount: (agentId: string) => number;
30
+ getThread: (threadId: string) => PinetMeshOpsTransferableThread | null;
31
+ transferThreadOwnership: (threadId: string, ownerAgent: string) => {
32
+ reassignedInboxCount: number;
33
+ updatedMessageCount: number;
34
+ };
35
+ recordTaskAssignment: (agentId: string, issueNumber: number, branch: string | null, threadId: string, sourceMessageId: number, options?: {
36
+ repoOwner?: string | null;
37
+ repoName?: string | null;
38
+ repoRoot?: string | null;
39
+ taskKind?: TaskAssignmentKind;
40
+ }) => TaskAssignmentInfo;
41
+ scheduleWakeup: (agentId: string, message: string, fireAt: string) => {
42
+ id: number;
43
+ fireAt: string;
44
+ } | Promise<{
45
+ id: number;
46
+ fireAt: string;
47
+ }>;
48
+ }
49
+ export interface PinetMeshOpsFollowerAgentRecord extends Omit<PinetMeshOpsAgentRecord, "status"> {
50
+ status?: PinetMeshOpsAgentRecord["status"] | null;
51
+ }
52
+ export interface PinetMeshOpsFollowerClientPort {
53
+ sendAgentMessage: (target: string, body: string, metadata?: Record<string, unknown>) => Promise<number>;
54
+ scheduleWakeup: (fireAt: string, message: string) => Promise<{
55
+ id: number;
56
+ fireAt: string;
57
+ }>;
58
+ listAgents: (includeGhosts: boolean) => Promise<PinetMeshOpsFollowerAgentRecord[]>;
59
+ }
60
+ export interface PinetMeshOpsDeps {
61
+ getPinetEnabled: () => boolean;
62
+ getBrokerRole: () => "broker" | "follower" | null;
63
+ getActiveBrokerDb: () => PinetMeshOpsBrokerDbPort | null;
64
+ getActiveBrokerSelfId: () => string | null;
65
+ getAgentName: () => string;
66
+ getFollowerClient: () => PinetMeshOpsFollowerClientPort | null;
67
+ formatTrackedAgent: (agentId: string) => string;
68
+ logActivity: (entry: ActivityLogEntry) => void;
69
+ }
70
+ export interface PinetMeshOps {
71
+ sendPinetAgentMessage: (target: string, body: string, metadata?: Record<string, unknown>) => Promise<{
72
+ messageId: number;
73
+ target: string;
74
+ transferredThreadId?: string;
75
+ transferredThreadChannel?: string;
76
+ }>;
77
+ sendPinetBroadcastMessage: (channel: string, body: string) => {
78
+ channel: string;
79
+ messageIds: number[];
80
+ recipients: string[];
81
+ };
82
+ scheduleBrokerWakeup: (fireAt: string, message: string) => Promise<{
83
+ id: number;
84
+ fireAt: string;
85
+ }>;
86
+ scheduleFollowerWakeup: (fireAt: string, message: string) => Promise<{
87
+ id: number;
88
+ fireAt: string;
89
+ }>;
90
+ listBrokerAgents: () => PinetMeshOpsAgentRecord[];
91
+ listFollowerAgents: (includeGhosts: boolean) => Promise<PinetMeshOpsAgentRecord[]>;
92
+ }
93
+ export declare function parseGitHubRemoteRepo(remoteUrl: string): {
94
+ repoOwner: string;
95
+ repoName: string;
96
+ } | null;
97
+ export declare function createPinetMeshOps(deps: PinetMeshOpsDeps): PinetMeshOps;
@@ -0,0 +1,266 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { normalizeOutgoingPinetControlMessage } from "./helpers.js";
4
+ import { dispatchBroadcastAgentMessage, dispatchDirectAgentMessage, isBroadcastChannelTarget, } from "./broker/agent-messaging.js";
5
+ import { extractTaskAssignmentsFromMessage } from "./task-assignments.js";
6
+ const execFileAsync = promisify(execFile);
7
+ function prepareOutgoingPinetAgentMessage(body, metadata) {
8
+ const control = normalizeOutgoingPinetControlMessage(body, metadata);
9
+ if (control) {
10
+ return {
11
+ body: control.body,
12
+ metadata: control.metadata,
13
+ };
14
+ }
15
+ return { body, metadata };
16
+ }
17
+ function getThreadOwnershipTransferMetadata(metadata) {
18
+ const transfer = metadata?.threadOwnershipTransfer;
19
+ return transfer && typeof transfer === "object" && !Array.isArray(transfer)
20
+ ? transfer
21
+ : null;
22
+ }
23
+ function getThreadOwnershipTransferId(metadata) {
24
+ const threadId = getThreadOwnershipTransferMetadata(metadata)?.threadId;
25
+ return typeof threadId === "string" && threadId.trim().length > 0 ? threadId.trim() : null;
26
+ }
27
+ function appendSlackThreadTransferNotice(body, threadId, channel) {
28
+ return [
29
+ body,
30
+ "",
31
+ "Transferred Slack thread context:",
32
+ `- thread_ts: ${threadId}`,
33
+ `- channel: ${channel}`,
34
+ `- To report directly in the transferred Slack thread, use slack_send with thread_ts ${threadId}; the channel is already recorded in Pinet.`,
35
+ "- If slack_send says the thread is already owned by another agent, ask the broker to inspect ownership and transfer it again.",
36
+ ].join("\n");
37
+ }
38
+ export function parseGitHubRemoteRepo(remoteUrl) {
39
+ const match = remoteUrl
40
+ .trim()
41
+ .match(/^(?:(?:https?:\/\/|ssh:\/\/)(?:[^@/\s]+@)?|(?:[^@/\s]+@)?)github(?:\.com|[-.][^/:]+)?[:/]([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?\/?$/i);
42
+ if (!match?.[1] || !match[2]) {
43
+ return null;
44
+ }
45
+ return { repoOwner: match[1], repoName: match[2] };
46
+ }
47
+ async function resolveCurrentGitHubRepo() {
48
+ try {
49
+ const [rootResult, remoteResult] = await Promise.all([
50
+ execFileAsync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf-8" }),
51
+ execFileAsync("git", ["remote", "get-url", "origin"], { encoding: "utf-8" }),
52
+ ]);
53
+ const remoteRepo = parseGitHubRemoteRepo(String(remoteResult.stdout).trim());
54
+ return {
55
+ repoOwner: remoteRepo?.repoOwner ?? null,
56
+ repoName: remoteRepo?.repoName ?? null,
57
+ repoRoot: String(rootResult.stdout).trim() || null,
58
+ };
59
+ }
60
+ catch {
61
+ return { repoOwner: null, repoName: null, repoRoot: null };
62
+ }
63
+ }
64
+ export function createPinetMeshOps(deps) {
65
+ async function sendPinetAgentMessage(targetRef, body, metadata) {
66
+ if (!deps.getPinetEnabled()) {
67
+ throw new Error("Pinet is not running. Use /pinet start or /pinet follow first.");
68
+ }
69
+ if (isBroadcastChannelTarget(targetRef)) {
70
+ throw new Error("Broadcast channels are broker-only. Send the request to the broker instead.");
71
+ }
72
+ const outgoing = prepareOutgoingPinetAgentMessage(body, metadata);
73
+ const finalBody = outgoing.body;
74
+ const finalMetadata = outgoing.metadata;
75
+ if (deps.getBrokerRole() === "broker") {
76
+ const db = deps.getActiveBrokerDb();
77
+ const selfId = deps.getActiveBrokerSelfId();
78
+ if (!db || !selfId) {
79
+ throw new Error("Broker agent identity is unavailable.");
80
+ }
81
+ const transferThreadId = getThreadOwnershipTransferId(finalMetadata);
82
+ const transferThread = transferThreadId ? db.getThread(transferThreadId) : null;
83
+ if (transferThreadId && !transferThread) {
84
+ throw new Error(`Cannot transfer unknown thread ${transferThreadId}.`);
85
+ }
86
+ if (transferThread && (transferThread.source !== "slack" || !transferThread.channel)) {
87
+ throw new Error(`Thread ${transferThreadId} is not a transferable Slack thread.`);
88
+ }
89
+ const transferThreadChannel = transferThread?.channel;
90
+ const transferMetadata = getThreadOwnershipTransferMetadata(finalMetadata);
91
+ const dispatchMetadata = transferThreadId
92
+ ? {
93
+ ...(finalMetadata ?? {}),
94
+ threadOwnershipTransfer: {
95
+ ...(transferMetadata ?? {}),
96
+ mode: "transfer",
97
+ threadId: transferThreadId,
98
+ source: "slack",
99
+ channel: transferThreadChannel,
100
+ },
101
+ }
102
+ : finalMetadata;
103
+ const dispatchBody = transferThreadId && transferThreadChannel
104
+ ? appendSlackThreadTransferNotice(finalBody, transferThreadId, transferThreadChannel)
105
+ : finalBody;
106
+ const result = dispatchDirectAgentMessage(db, {
107
+ senderAgentId: selfId,
108
+ senderAgentName: deps.getAgentName(),
109
+ target: targetRef,
110
+ body: dispatchBody,
111
+ metadata: dispatchMetadata,
112
+ });
113
+ if (transferThreadId) {
114
+ const transfer = db.transferThreadOwnership(transferThreadId, result.target.id);
115
+ deps.logActivity({
116
+ kind: "thread_transfer",
117
+ level: "actions",
118
+ title: "Thread ownership transferred",
119
+ summary: `Transferred ${transferThreadId} to ${deps.formatTrackedAgent(result.target.id)}.`,
120
+ fields: [
121
+ { label: "Worker", value: deps.formatTrackedAgent(result.target.id) },
122
+ { label: "Thread", value: transferThreadId },
123
+ { label: "A2A message", value: result.messageId },
124
+ { label: "Reassigned inbox rows", value: transfer.reassignedInboxCount },
125
+ ],
126
+ tone: "info",
127
+ });
128
+ }
129
+ const parsedAssignments = extractTaskAssignmentsFromMessage(body);
130
+ const fallbackRepo = parsedAssignments.some((assignment) => !assignment.repoOwner || !assignment.repoName)
131
+ ? await resolveCurrentGitHubRepo()
132
+ : { repoOwner: null, repoName: null, repoRoot: null };
133
+ const recordedAssignments = [];
134
+ for (const assignment of parsedAssignments) {
135
+ const repoOwner = assignment.repoOwner ?? fallbackRepo.repoOwner;
136
+ const repoName = assignment.repoName ?? fallbackRepo.repoName;
137
+ const usesFallbackRepoIdentity = !assignment.repoOwner || !assignment.repoName;
138
+ const fallbackMatchesAssignmentRepo = assignment.repoOwner?.toLowerCase() === fallbackRepo.repoOwner?.toLowerCase() &&
139
+ assignment.repoName?.toLowerCase() === fallbackRepo.repoName?.toLowerCase();
140
+ const repoRoot = usesFallbackRepoIdentity || fallbackMatchesAssignmentRepo ? fallbackRepo.repoRoot : null;
141
+ const tracked = db.recordTaskAssignment(result.target.id, assignment.issueNumber, assignment.branch, result.threadId, result.messageId, {
142
+ repoOwner,
143
+ repoName,
144
+ repoRoot,
145
+ taskKind: assignment.taskKind,
146
+ });
147
+ recordedAssignments.push({ issueNumber: tracked.issueNumber, branch: tracked.branch });
148
+ }
149
+ if (recordedAssignments.length > 0) {
150
+ deps.logActivity({
151
+ kind: "task_assignment",
152
+ level: "actions",
153
+ title: recordedAssignments.length === 1 ? "Task assigned" : "Tasks assigned",
154
+ summary: `Assigned ${recordedAssignments.map((assignment) => `#${assignment.issueNumber}`).join(", ")} to ${deps.formatTrackedAgent(result.target.id)}.`,
155
+ details: recordedAssignments.map((assignment) => assignment.branch
156
+ ? `#${assignment.issueNumber} on \`${assignment.branch}\``
157
+ : `#${assignment.issueNumber}`),
158
+ fields: [
159
+ { label: "Worker", value: deps.formatTrackedAgent(result.target.id) },
160
+ { label: "Thread", value: result.threadId },
161
+ { label: "Message", value: result.messageId },
162
+ ],
163
+ tone: "info",
164
+ });
165
+ }
166
+ return {
167
+ messageId: result.messageId,
168
+ target: result.target.name,
169
+ ...(transferThreadId ? { transferredThreadId: transferThreadId } : {}),
170
+ ...(transferThreadChannel ? { transferredThreadChannel: transferThreadChannel } : {}),
171
+ };
172
+ }
173
+ if (deps.getBrokerRole() === "follower") {
174
+ const client = deps.getFollowerClient();
175
+ if (!client) {
176
+ throw new Error("Pinet is in an unexpected state.");
177
+ }
178
+ const messageId = await client.sendAgentMessage(targetRef, finalBody, finalMetadata);
179
+ return { messageId, target: targetRef };
180
+ }
181
+ throw new Error("Pinet is in an unexpected state.");
182
+ }
183
+ function sendPinetBroadcastMessage(channel, body) {
184
+ const db = deps.getActiveBrokerDb();
185
+ const selfId = deps.getActiveBrokerSelfId();
186
+ if (!db || !selfId) {
187
+ throw new Error("Broker agent identity is unavailable.");
188
+ }
189
+ const outgoing = prepareOutgoingPinetAgentMessage(body);
190
+ const result = dispatchBroadcastAgentMessage(db, {
191
+ senderAgentId: selfId,
192
+ senderAgentName: deps.getAgentName(),
193
+ channel,
194
+ body: outgoing.body,
195
+ ...(outgoing.metadata ? { metadata: outgoing.metadata } : {}),
196
+ });
197
+ return {
198
+ channel: result.channel,
199
+ messageIds: result.messageIds,
200
+ recipients: result.targets.map((target) => target.name),
201
+ };
202
+ }
203
+ async function scheduleBrokerWakeup(fireAt, message) {
204
+ const db = deps.getActiveBrokerDb();
205
+ const selfId = deps.getActiveBrokerSelfId();
206
+ if (!db || !selfId) {
207
+ throw new Error("Broker agent identity is unavailable.");
208
+ }
209
+ return await db.scheduleWakeup(selfId, message, fireAt);
210
+ }
211
+ async function scheduleFollowerWakeup(fireAt, message) {
212
+ const client = deps.getFollowerClient();
213
+ if (!client) {
214
+ throw new Error("Pinet is in an unexpected state.");
215
+ }
216
+ return await client.scheduleWakeup(fireAt, message);
217
+ }
218
+ function listBrokerAgents() {
219
+ const db = deps.getActiveBrokerDb();
220
+ if (!db) {
221
+ throw new Error("Broker agent identity is unavailable.");
222
+ }
223
+ return db.getAllAgents().map((agent) => ({
224
+ emoji: agent.emoji,
225
+ name: agent.name,
226
+ id: agent.id,
227
+ pid: agent.pid,
228
+ status: agent.status,
229
+ metadata: agent.metadata,
230
+ lastHeartbeat: agent.lastHeartbeat,
231
+ lastSeen: agent.lastSeen,
232
+ disconnectedAt: agent.disconnectedAt,
233
+ resumableUntil: agent.resumableUntil,
234
+ outboundCount: agent.outboundCount,
235
+ pendingInboxCount: db.getPendingInboxCount(agent.id),
236
+ }));
237
+ }
238
+ async function listFollowerAgents(includeGhosts) {
239
+ const client = deps.getFollowerClient();
240
+ if (!client) {
241
+ throw new Error("Pinet is in an unexpected state.");
242
+ }
243
+ return (await client.listAgents(includeGhosts)).map((agent) => ({
244
+ emoji: agent.emoji,
245
+ name: agent.name,
246
+ id: agent.id,
247
+ pid: agent.pid,
248
+ status: agent.status ?? "idle",
249
+ metadata: agent.metadata,
250
+ lastHeartbeat: agent.lastHeartbeat,
251
+ lastSeen: agent.lastSeen,
252
+ disconnectedAt: agent.disconnectedAt,
253
+ resumableUntil: agent.resumableUntil,
254
+ outboundCount: agent.outboundCount,
255
+ pendingInboxCount: agent.pendingInboxCount,
256
+ }));
257
+ }
258
+ return {
259
+ sendPinetAgentMessage,
260
+ sendPinetBroadcastMessage,
261
+ scheduleBrokerWakeup,
262
+ scheduleFollowerWakeup,
263
+ listBrokerAgents,
264
+ listFollowerAgents,
265
+ };
266
+ }
@@ -0,0 +1,14 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ export interface PinetRegistrationGateDeps {
3
+ getArgv?: () => string[];
4
+ getStdinIsTTY?: () => boolean | undefined;
5
+ getStdoutIsTTY?: () => boolean | undefined;
6
+ }
7
+ export interface PinetRegistrationGate {
8
+ isBlocked: () => boolean;
9
+ getBlockReason: () => string;
10
+ evaluateSessionStart: (ctx: ExtensionContext) => boolean;
11
+ assertCanRegister: () => void;
12
+ reset: () => void;
13
+ }
14
+ export declare function createPinetRegistrationGate(deps?: PinetRegistrationGateDeps): PinetRegistrationGate;
@@ -0,0 +1,42 @@
1
+ import { isLikelyLocalSubagentContext } from "./helpers.js";
2
+ const PINET_REGISTRATION_BLOCK_REASON = "Pinet is disabled in local subagent sessions to avoid polluting the agent mesh.";
3
+ export function createPinetRegistrationGate(deps = {}) {
4
+ const getArgv = deps.getArgv ?? (() => process.argv.slice(2));
5
+ const getStdinIsTTY = deps.getStdinIsTTY ?? (() => process.stdin.isTTY);
6
+ const getStdoutIsTTY = deps.getStdoutIsTTY ?? (() => process.stdout.isTTY);
7
+ let pinetRegistrationBlocked = false;
8
+ function isBlocked() {
9
+ return pinetRegistrationBlocked;
10
+ }
11
+ function getBlockReason() {
12
+ return PINET_REGISTRATION_BLOCK_REASON;
13
+ }
14
+ function evaluateSessionStart(ctx) {
15
+ const sessionHeader = ctx.sessionManager.getHeader?.();
16
+ pinetRegistrationBlocked = isLikelyLocalSubagentContext({
17
+ sessionHeader,
18
+ sessionFile: ctx.sessionManager.getSessionFile(),
19
+ leafId: ctx.sessionManager.getLeafId(),
20
+ argv: getArgv(),
21
+ hasUI: ctx.hasUI,
22
+ stdinIsTTY: getStdinIsTTY(),
23
+ stdoutIsTTY: getStdoutIsTTY(),
24
+ });
25
+ return pinetRegistrationBlocked;
26
+ }
27
+ function assertCanRegister() {
28
+ if (pinetRegistrationBlocked) {
29
+ throw new Error(getBlockReason());
30
+ }
31
+ }
32
+ function reset() {
33
+ pinetRegistrationBlocked = false;
34
+ }
35
+ return {
36
+ isBlocked,
37
+ getBlockReason,
38
+ evaluateSessionStart,
39
+ assertCanRegister,
40
+ reset,
41
+ };
42
+ }
@@ -0,0 +1,16 @@
1
+ import type { PinetControlCommand } from "./helpers.js";
2
+ export interface PinetRemoteControlAcksDeps {
3
+ queueBrokerInboxIds: (inboxIds: Iterable<number>) => void;
4
+ isBrokerConnected: () => boolean;
5
+ markBrokerInboxIdsDelivered: (inboxIds: number[]) => void;
6
+ queueFollowerInboxIds: (inboxIds: Iterable<number>) => void;
7
+ markFollowerInboxIdsDelivered: (inboxIds: Iterable<number>) => void;
8
+ flushDeliveredFollowerAcks: () => void | Promise<void>;
9
+ }
10
+ export interface PinetRemoteControlAcks {
11
+ resetPendingRemoteControlAcks: () => void;
12
+ deferBrokerControlAck: (command: PinetControlCommand, inboxId: number) => void;
13
+ deferFollowerControlAck: (command: PinetControlCommand, inboxId: number) => void;
14
+ flushDeferredRemoteControlAcks: (command: PinetControlCommand) => void;
15
+ }
16
+ export declare function createPinetRemoteControlAcks(deps: PinetRemoteControlAcksDeps): PinetRemoteControlAcks;
@@ -0,0 +1,46 @@
1
+ function createAckBuckets() {
2
+ return {
3
+ interrupt: new Set(),
4
+ reload: new Set(),
5
+ exit: new Set(),
6
+ };
7
+ }
8
+ export function createPinetRemoteControlAcks(deps) {
9
+ const pendingBrokerControlInboxIds = createAckBuckets();
10
+ const pendingFollowerControlInboxIds = createAckBuckets();
11
+ function resetPendingRemoteControlAcks() {
12
+ pendingBrokerControlInboxIds.interrupt.clear();
13
+ pendingBrokerControlInboxIds.reload.clear();
14
+ pendingBrokerControlInboxIds.exit.clear();
15
+ pendingFollowerControlInboxIds.interrupt.clear();
16
+ pendingFollowerControlInboxIds.reload.clear();
17
+ pendingFollowerControlInboxIds.exit.clear();
18
+ }
19
+ function deferBrokerControlAck(command, inboxId) {
20
+ pendingBrokerControlInboxIds[command].add(inboxId);
21
+ deps.queueBrokerInboxIds([inboxId]);
22
+ }
23
+ function deferFollowerControlAck(command, inboxId) {
24
+ pendingFollowerControlInboxIds[command].add(inboxId);
25
+ deps.queueFollowerInboxIds([inboxId]);
26
+ }
27
+ function flushDeferredRemoteControlAcks(command) {
28
+ const brokerIds = [...pendingBrokerControlInboxIds[command]];
29
+ if (brokerIds.length > 0 && deps.isBrokerConnected()) {
30
+ deps.markBrokerInboxIdsDelivered(brokerIds);
31
+ pendingBrokerControlInboxIds[command].clear();
32
+ }
33
+ const followerIds = [...pendingFollowerControlInboxIds[command]];
34
+ if (followerIds.length > 0) {
35
+ deps.markFollowerInboxIdsDelivered(followerIds);
36
+ pendingFollowerControlInboxIds[command].clear();
37
+ void deps.flushDeliveredFollowerAcks();
38
+ }
39
+ }
40
+ return {
41
+ resetPendingRemoteControlAcks,
42
+ deferBrokerControlAck,
43
+ deferFollowerControlAck,
44
+ flushDeferredRemoteControlAcks,
45
+ };
46
+ }
@@ -0,0 +1,17 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { type PinetControlCommand, type PinetRemoteControlRequestResult } from "./helpers.js";
3
+ export type PinetRuntimeControlContext = ExtensionContext & {
4
+ abort?: () => void;
5
+ shutdown?: () => void;
6
+ };
7
+ export interface PinetRemoteControlDeps {
8
+ flushDeferredRemoteControlAcks: (command: PinetControlCommand) => void;
9
+ reloadPinetRuntime: (ctx: ExtensionContext) => Promise<void>;
10
+ formatError: (error: unknown) => string;
11
+ }
12
+ export interface PinetRemoteControl {
13
+ requestRemoteControl: (command: PinetControlCommand, ctx: ExtensionContext) => PinetRemoteControlRequestResult;
14
+ runRemoteControl: (command: PinetControlCommand, ctx: ExtensionContext) => void;
15
+ resetRemoteControlState: () => void;
16
+ }
17
+ export declare function createPinetRemoteControl(deps: PinetRemoteControlDeps): PinetRemoteControl;