@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,171 @@
1
+ const REACTION_ALIASES = {
2
+ "📝": "memo",
3
+ memo: "memo",
4
+ "🐛": "bug",
5
+ bug: "bug",
6
+ "🔍": "mag",
7
+ mag: "mag",
8
+ mag_right: "mag_right",
9
+ "📌": "pushpin",
10
+ pushpin: "pushpin",
11
+ "🌐": "globe_with_meridians",
12
+ globe_with_meridians: "globe_with_meridians",
13
+ "🔄": "repeat",
14
+ repeat: "repeat",
15
+ "👀": "eyes",
16
+ eyes: "eyes",
17
+ "✅": "white_check_mark",
18
+ white_check_mark: "white_check_mark",
19
+ "⬆️": "arrow_up",
20
+ "⬆": "arrow_up",
21
+ arrow_up: "arrow_up",
22
+ "🛑": "octagonal_sign",
23
+ octagonal_sign: "octagonal_sign",
24
+ };
25
+ const REACTION_DISPLAY = {
26
+ memo: "📝",
27
+ bug: "🐛",
28
+ mag: "🔍",
29
+ mag_right: "🔎",
30
+ pushpin: "📌",
31
+ globe_with_meridians: "🌐",
32
+ repeat: "🔄",
33
+ eyes: "👀",
34
+ white_check_mark: "✅",
35
+ arrow_up: "⬆️",
36
+ octagonal_sign: "🛑",
37
+ };
38
+ export const DEFAULT_REACTION_COMMANDS = {
39
+ memo: {
40
+ action: "summarize",
41
+ prompt: "Summarize the reacted-to message and the surrounding thread. Focus on key decisions, action items, and open questions.",
42
+ },
43
+ bug: {
44
+ action: "file-issue",
45
+ prompt: "Turn the reacted-to message or thread into a GitHub issue. Capture the problem, context, reproduction clues, and a sensible next step.",
46
+ },
47
+ eyes: {
48
+ action: "review",
49
+ prompt: "Review the referenced message, code, or work item and report what needs attention. If the context points to a PR or diff, review that artifact.",
50
+ },
51
+ white_check_mark: {
52
+ action: "approve",
53
+ prompt: "Evaluate whether the referenced work should be approved. If it looks good, say so clearly and note any final caveats.",
54
+ },
55
+ repeat: {
56
+ action: "retry",
57
+ prompt: "Retry or regenerate the requested work using the reacted message and surrounding thread as the source of truth.",
58
+ },
59
+ arrow_up: {
60
+ action: "steer",
61
+ prompt: "Treat the reacted-to message as steering. Read the durable message context, then prioritize it as an explicit operator instruction if it is relevant and safe.",
62
+ },
63
+ octagonal_sign: {
64
+ action: "interrupt",
65
+ prompt: "Interrupt the current owner process for the reacted Slack thread. Stop the active turn safely, then read the durable context before deciding whether more work is needed.",
66
+ },
67
+ mag: {
68
+ action: "search",
69
+ prompt: "Search the codebase for code related to the reacted message and report the most relevant files or findings.",
70
+ },
71
+ pushpin: {
72
+ action: "track",
73
+ prompt: "Treat the reacted message as something to preserve and track. Pin or record it in the most appropriate project artifact, such as a Slack canvas.",
74
+ },
75
+ globe_with_meridians: {
76
+ action: "fetch-url",
77
+ prompt: "If the reacted message contains a URL, fetch the linked page and summarize the important information for the user.",
78
+ },
79
+ };
80
+ function normalizeReactionNameOrNull(input) {
81
+ const trimmed = input.trim();
82
+ if (!trimmed)
83
+ return null;
84
+ const withoutColons = trimmed.replace(/^:+|:+$/g, "").toLowerCase();
85
+ return (REACTION_ALIASES[trimmed] ??
86
+ REACTION_ALIASES[withoutColons] ??
87
+ (/^[a-z0-9_+-]+$/.test(withoutColons) ? withoutColons : null));
88
+ }
89
+ export function normalizeReactionName(input) {
90
+ const normalized = normalizeReactionNameOrNull(input);
91
+ if (!normalized) {
92
+ throw new Error(`Unsupported reaction ${JSON.stringify(input)}. Use a Slack reaction name like "eyes" or a supported emoji such as 👀, ✅, 🔄, 📝, 🐛, ⬆️, or 🛑.`);
93
+ }
94
+ return normalized;
95
+ }
96
+ function buildDefaultPromptForAction(action) {
97
+ switch (action) {
98
+ case "summarize":
99
+ return DEFAULT_REACTION_COMMANDS.memo.prompt;
100
+ case "file-issue":
101
+ return DEFAULT_REACTION_COMMANDS.bug.prompt;
102
+ case "review":
103
+ return DEFAULT_REACTION_COMMANDS.eyes.prompt;
104
+ case "approve":
105
+ return DEFAULT_REACTION_COMMANDS.white_check_mark.prompt;
106
+ case "retry":
107
+ return DEFAULT_REACTION_COMMANDS.repeat.prompt;
108
+ case "steer":
109
+ return DEFAULT_REACTION_COMMANDS.arrow_up.prompt;
110
+ case "interrupt":
111
+ return DEFAULT_REACTION_COMMANDS.octagonal_sign.prompt;
112
+ case "search":
113
+ return DEFAULT_REACTION_COMMANDS.mag.prompt;
114
+ case "track":
115
+ return DEFAULT_REACTION_COMMANDS.pushpin.prompt;
116
+ case "fetch-url":
117
+ return DEFAULT_REACTION_COMMANDS.globe_with_meridians.prompt;
118
+ default:
119
+ return `Carry out the "${action}" action for the reacted Slack message or thread, using the included context before you decide what to do.`;
120
+ }
121
+ }
122
+ export function resolveReactionCommands(settings) {
123
+ const resolved = new Map(Object.entries(DEFAULT_REACTION_COMMANDS));
124
+ if (!settings) {
125
+ return resolved;
126
+ }
127
+ for (const [rawReaction, config] of Object.entries(settings)) {
128
+ const normalizedReaction = normalizeReactionNameOrNull(rawReaction);
129
+ if (!normalizedReaction)
130
+ continue;
131
+ if (typeof config === "string") {
132
+ resolved.set(normalizedReaction, {
133
+ action: config,
134
+ prompt: buildDefaultPromptForAction(config),
135
+ });
136
+ continue;
137
+ }
138
+ if (!config || typeof config !== "object")
139
+ continue;
140
+ const action = config.action?.trim() || resolved.get(normalizedReaction)?.action || normalizedReaction;
141
+ const prompt = config.prompt?.trim() || buildDefaultPromptForAction(action);
142
+ resolved.set(normalizedReaction, { action, prompt });
143
+ }
144
+ return resolved;
145
+ }
146
+ export function formatReactionDisplay(reactionName) {
147
+ return `${REACTION_DISPLAY[reactionName] ?? `:${reactionName}:`} (:${reactionName}:)`;
148
+ }
149
+ export function buildReactionTriggerMessage(input) {
150
+ const reactedMessageText = input.reactedMessageText.trim() || "(no text)";
151
+ return [
152
+ "Reaction trigger from Slack:",
153
+ `- reaction: ${formatReactionDisplay(input.reactionName)}`,
154
+ `- action: ${input.command.action}`,
155
+ `- reactor: ${input.reactorName}`,
156
+ `- channel: <#${input.channel}>`,
157
+ `- thread_ts: ${input.threadTs}`,
158
+ `- message_ts: ${input.messageTs}`,
159
+ `- reacted_message_author: ${input.reactedMessageAuthor}`,
160
+ `- reacted_message_text: ${reactedMessageText}`,
161
+ "",
162
+ `Requested action: ${input.command.prompt}`,
163
+ "Treat this reaction as an explicit user request, but still verify context before acting.",
164
+ ].join("\n");
165
+ }
166
+ export function buildReactionPromptGuidelines() {
167
+ return [
168
+ "Reaction-triggered requests may appear in your Slack inbox as structured 'Reaction trigger from Slack:' messages.",
169
+ "Treat them as explicit user requests keyed off emoji reactions. Use the included action, reacted message text, and thread context to decide what to do.",
170
+ ];
171
+ }
@@ -0,0 +1,20 @@
1
+ import { type SecurityGuardrails } from "./guardrails.js";
2
+ import type { SlackToolPolicyTurn } from "./core-tool-guardrails.js";
3
+ export declare const GUARDED_REPO_TOOLS: Set<string>;
4
+ export declare function isGuardedRepoTool(toolName: string): boolean;
5
+ export declare function summarizeRepoToolAction(event: {
6
+ toolName: string;
7
+ input: Record<string, unknown>;
8
+ }): string;
9
+ export declare function evaluateSlackOriginRepoToolPolicy(options: {
10
+ turn: SlackToolPolicyTurn | null;
11
+ toolName: string;
12
+ input: Record<string, unknown>;
13
+ guardrails: SecurityGuardrails;
14
+ requireToolPolicy: (toolName: string, threadTs: string | undefined, action: string) => void;
15
+ formatAction: (action: string) => string;
16
+ formatError: (error: unknown) => string;
17
+ }): {
18
+ block: true;
19
+ reason: string;
20
+ } | undefined;
@@ -0,0 +1,81 @@
1
+ import { isToolBlocked, toolNeedsConfirmation } from "./guardrails.js";
2
+ export const GUARDED_REPO_TOOLS = new Set([
3
+ "open_in_editor",
4
+ "comment_add",
5
+ "comment_list",
6
+ "comment_wipe_all",
7
+ "psql",
8
+ ]);
9
+ export function isGuardedRepoTool(toolName) {
10
+ return GUARDED_REPO_TOOLS.has(toolName);
11
+ }
12
+ function normalizeWhitespace(value) {
13
+ return value.replace(/\s+/g, " ").trim();
14
+ }
15
+ function clipValue(value, maxLength = 120) {
16
+ if (value.length <= maxLength) {
17
+ return value;
18
+ }
19
+ return `${value.slice(0, maxLength - 1).trimEnd()}…`;
20
+ }
21
+ export function summarizeRepoToolAction(event) {
22
+ const input = event.input;
23
+ switch (event.toolName) {
24
+ case "open_in_editor":
25
+ return `file=${String(input.file ?? "")} | line=${String(input.line ?? "")}`;
26
+ case "comment_add": {
27
+ const comment = typeof input.comment === "string" ? input.comment : "";
28
+ return [
29
+ `thread_id=${String(input.thread_id ?? "")}`,
30
+ `file=${String(input.file ?? "")}`,
31
+ `start_line=${String(input.start_line ?? "")}`,
32
+ `end_line=${String(input.end_line ?? "")}`,
33
+ `comment_length=${comment.length}`,
34
+ ].join(" | ");
35
+ }
36
+ case "comment_list":
37
+ return `thread_id=${String(input.thread_id ?? "")} | limit=${String(input.limit ?? "")}`;
38
+ case "comment_wipe_all":
39
+ return "scope=current_repo";
40
+ case "psql": {
41
+ const query = typeof input.query === "string" ? normalizeWhitespace(input.query) : "";
42
+ return `format=${String(input.format ?? "table")} | query=${clipValue(query)}`;
43
+ }
44
+ default:
45
+ return JSON.stringify(input);
46
+ }
47
+ }
48
+ export function evaluateSlackOriginRepoToolPolicy(options) {
49
+ const { turn, toolName, input, guardrails, requireToolPolicy, formatAction, formatError } = options;
50
+ if (!turn || !isGuardedRepoTool(toolName)) {
51
+ return undefined;
52
+ }
53
+ if (isToolBlocked(toolName, guardrails)) {
54
+ return {
55
+ block: true,
56
+ reason: `Tool "${toolName}" is blocked by Slack security guardrails.`,
57
+ };
58
+ }
59
+ if (!toolNeedsConfirmation(toolName, guardrails)) {
60
+ return undefined;
61
+ }
62
+ const action = summarizeRepoToolAction({ toolName, input });
63
+ if (!turn.threadTs) {
64
+ return {
65
+ block: true,
66
+ reason: turn.threadCount > 1
67
+ ? `Tool "${toolName}" requires Slack confirmation for action ${formatAction(action)}, but this Slack-triggered turn currently batches ${turn.threadCount} threads. Process one Slack thread at a time before using that tool.`
68
+ : `Tool "${toolName}" requires Slack confirmation for action ${formatAction(action)}, but there is no tracked Slack thread available for this turn. Retry from a specific Slack thread and call slack with action "confirm_action" there first.`,
69
+ };
70
+ }
71
+ try {
72
+ requireToolPolicy(toolName, turn.threadTs, action);
73
+ return undefined;
74
+ }
75
+ catch (error) {
76
+ return {
77
+ block: true,
78
+ reason: formatError(error),
79
+ };
80
+ }
81
+ }
@@ -0,0 +1,93 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { type GitContext, type GitDynamicState } from "./git-metadata.js";
3
+ import { type PinetSkinStatusVocabulary, type SlackBridgeSettings } from "./helpers.js";
4
+ import { type SecurityGuardrails } from "./guardrails.js";
5
+ import type { SinglePlayerThreadInfo } from "./single-player-runtime.js";
6
+ export type RuntimeAgentRole = "broker" | "worker";
7
+ export type ReactionCommandMap = Map<string, {
8
+ action: string;
9
+ prompt: string;
10
+ }>;
11
+ export interface ReloadableRuntimeSnapshot {
12
+ settings: SlackBridgeSettings;
13
+ botToken: string | undefined;
14
+ appToken: string | undefined;
15
+ allowedUsers: Set<string> | null;
16
+ guardrails: SecurityGuardrails;
17
+ reactionCommands: ReactionCommandMap;
18
+ securityPrompt: string;
19
+ agentName: string;
20
+ agentEmoji: string;
21
+ activeSkinTheme: string | null;
22
+ agentPersonality: string | null;
23
+ agentAliases: string[];
24
+ }
25
+ export interface RuntimeAgentContextDeps {
26
+ cwd: string;
27
+ getSettings: () => SlackBridgeSettings;
28
+ setSettings: (settings: SlackBridgeSettings) => void;
29
+ getBotToken: () => string | undefined;
30
+ setBotToken: (token: string | undefined) => void;
31
+ getAppToken: () => string | undefined;
32
+ setAppToken: (token: string | undefined) => void;
33
+ getAllowedUsers: () => Set<string> | null;
34
+ setAllowedUsers: (users: Set<string> | null) => void;
35
+ getGuardrails: () => SecurityGuardrails;
36
+ setGuardrails: (guardrails: SecurityGuardrails) => void;
37
+ getReactionCommands: () => ReactionCommandMap;
38
+ setReactionCommands: (commands: ReactionCommandMap) => void;
39
+ getSecurityPrompt: () => string;
40
+ setSecurityPrompt: (prompt: string) => void;
41
+ getAgentName: () => string;
42
+ setAgentName: (name: string) => void;
43
+ getAgentEmoji: () => string;
44
+ setAgentEmoji: (emoji: string) => void;
45
+ getAgentStableId: () => string;
46
+ getBrokerStableId: () => string;
47
+ getBrokerRole: () => "broker" | "follower" | null;
48
+ getAgentOwnerToken: () => string;
49
+ setAgentOwnerToken: (ownerToken: string) => void;
50
+ getActiveSkinTheme: () => string | null;
51
+ setActiveSkinTheme: (theme: string | null) => void;
52
+ getAgentPersonality: () => string | null;
53
+ setAgentPersonality: (personality: string | null) => void;
54
+ getAgentAliases: () => Set<string>;
55
+ getThreads: () => Map<string, SinglePlayerThreadInfo>;
56
+ getExtensionContext: () => ExtensionContext | null;
57
+ persistState: () => void;
58
+ updateBadge: () => void;
59
+ getGitContext: () => Promise<GitContext>;
60
+ /** Optional test hook for live branch/dirty probing. */
61
+ getDynamicGitState?: (cwd: string) => Promise<GitDynamicState>;
62
+ }
63
+ export interface RuntimeAgentContext {
64
+ isUserAllowed: (userId: string) => boolean;
65
+ maybeWarnSlackUserAccess: (ctx?: ExtensionContext) => void;
66
+ maybeWarnSlackGuardrailPosture: (ctx?: ExtensionContext) => void;
67
+ getStableIdForRole: (role: RuntimeAgentRole) => string;
68
+ getIdentitySeedForRole: (role: RuntimeAgentRole, sessionFile?: string) => string;
69
+ getSkinSeed: (preferredSeed?: string) => string;
70
+ rememberAgentAlias: (name: string | undefined) => void;
71
+ resolveSkinAssignment: (role: RuntimeAgentRole, seed?: string) => {
72
+ name: string;
73
+ emoji: string;
74
+ personality: string;
75
+ statusVocabulary?: PinetSkinStatusVocabulary;
76
+ } | null;
77
+ applyLocalAgentIdentity: (nextName: string, nextEmoji: string, nextPersonality: string | null) => void;
78
+ refreshSettings: () => void;
79
+ snapshotReloadableRuntime: () => ReloadableRuntimeSnapshot;
80
+ restoreReloadableRuntime: (snapshot: ReloadableRuntimeSnapshot) => void;
81
+ detectProjectTools: (repoRoot: string, cwd: string) => string[];
82
+ getAgentMetadata: (role?: RuntimeAgentRole) => Promise<Record<string, unknown>>;
83
+ asStringValue: (value: unknown) => string | undefined;
84
+ getMeshRoleFromMetadata: (metadata: Record<string, unknown> | undefined, fallback?: RuntimeAgentRole) => RuntimeAgentRole;
85
+ buildSkinMetadata: (metadata: Record<string, unknown> | undefined, personality: string, statusVocabulary?: PinetSkinStatusVocabulary) => Record<string, unknown>;
86
+ getIdentityGuidelines: () => [string, string, string];
87
+ applyRegistrationIdentity: (registration: {
88
+ name: string;
89
+ emoji: string;
90
+ metadata?: Record<string, unknown> | null;
91
+ }) => void;
92
+ }
93
+ export declare function createRuntimeAgentContext(deps: RuntimeAgentContextDeps): RuntimeAgentContext;
@@ -0,0 +1,302 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { probeGitDynamic } from "./git-metadata.js";
5
+ import { buildAllowlist, buildIdentityReplyGuidelines, buildPinetOwnerToken, buildPinetSkinAssignment, buildSlackCompatibilityScope, getSlackUserAccessWarning, isUserAllowed as checkUserAllowed, loadSettings as loadSettingsFromFile, normalizeOwnedThreads, normalizePinetSkinTheme, resolveRuntimeAgentIdentity, shortenPath, } from "./helpers.js";
6
+ import { buildSecurityPrompt, getEmptyRuntimeGuardrailsWarning, } from "./guardrails.js";
7
+ import { resolveReactionCommands } from "./reaction-triggers.js";
8
+ export function createRuntimeAgentContext(deps) {
9
+ let lastSlackUserAccessWarning = "";
10
+ let lastSlackGuardrailPostureWarning = "";
11
+ const selfLocation = `${shortenPath(deps.cwd, os.homedir())}@${os.hostname()}`;
12
+ function isUserAllowed(userId) {
13
+ return checkUserAllowed(deps.getAllowedUsers(), userId);
14
+ }
15
+ function maybeWarnSlackUserAccess(ctx) {
16
+ const warning = getSlackUserAccessWarning(deps.getAllowedUsers());
17
+ if (!warning) {
18
+ lastSlackUserAccessWarning = "";
19
+ return;
20
+ }
21
+ if (warning === lastSlackUserAccessWarning) {
22
+ return;
23
+ }
24
+ lastSlackUserAccessWarning = warning;
25
+ console.warn(`[slack-bridge] ${warning}`);
26
+ ctx?.ui.notify(warning, "warning");
27
+ }
28
+ function maybeWarnSlackGuardrailPosture(ctx) {
29
+ const botToken = deps.getBotToken()?.trim();
30
+ const appToken = deps.getAppToken()?.trim();
31
+ const allowlist = deps.getAllowedUsers();
32
+ const hasAdmittedUsers = allowlist === null || allowlist.size > 0;
33
+ const warning = botToken && appToken && hasAdmittedUsers
34
+ ? getEmptyRuntimeGuardrailsWarning(deps.getGuardrails())
35
+ : null;
36
+ if (!warning) {
37
+ lastSlackGuardrailPostureWarning = "";
38
+ return;
39
+ }
40
+ if (warning === lastSlackGuardrailPostureWarning) {
41
+ return;
42
+ }
43
+ lastSlackGuardrailPostureWarning = warning;
44
+ console.warn(`[slack-bridge] ${warning}`);
45
+ ctx?.ui.notify(warning, "warning");
46
+ }
47
+ function getStableIdForRole(role) {
48
+ return role === "broker" ? deps.getBrokerStableId() : deps.getAgentStableId();
49
+ }
50
+ function getIdentitySeedForRole(role, sessionFile = deps.getExtensionContext()?.sessionManager.getSessionFile() ?? undefined) {
51
+ return role === "broker" ? deps.getBrokerStableId() : (sessionFile ?? deps.getAgentStableId());
52
+ }
53
+ function getSkinSeed(preferredSeed) {
54
+ return (preferredSeed?.trim() ||
55
+ getStableIdForRole(deps.getBrokerRole() === "broker" ? "broker" : "worker"));
56
+ }
57
+ function rememberAgentAlias(name) {
58
+ const trimmed = name?.trim();
59
+ if (!trimmed || trimmed === deps.getAgentName()) {
60
+ return;
61
+ }
62
+ const agentAliases = deps.getAgentAliases();
63
+ agentAliases.add(trimmed);
64
+ while (agentAliases.size > 24) {
65
+ const oldest = agentAliases.values().next().value;
66
+ if (!oldest) {
67
+ break;
68
+ }
69
+ agentAliases.delete(oldest);
70
+ }
71
+ }
72
+ function resolveSkinAssignment(role, seed = getSkinSeed()) {
73
+ const activeSkinTheme = deps.getActiveSkinTheme();
74
+ if (!activeSkinTheme) {
75
+ return null;
76
+ }
77
+ const assignment = buildPinetSkinAssignment({ theme: activeSkinTheme, role, seed });
78
+ return {
79
+ name: assignment.name,
80
+ emoji: assignment.emoji,
81
+ personality: assignment.personality,
82
+ statusVocabulary: assignment.statusVocabulary,
83
+ };
84
+ }
85
+ function applyLocalAgentIdentity(nextName, nextEmoji, nextPersonality) {
86
+ const previousName = deps.getAgentName();
87
+ if (previousName === nextName &&
88
+ deps.getAgentEmoji() === nextEmoji &&
89
+ (deps.getAgentPersonality() ?? null) === (nextPersonality ?? null)) {
90
+ return;
91
+ }
92
+ deps.setAgentName(nextName);
93
+ deps.setAgentEmoji(nextEmoji);
94
+ deps.setAgentPersonality(nextPersonality ?? null);
95
+ rememberAgentAlias(previousName);
96
+ normalizeOwnedThreads(deps.getThreads().values(), deps.getAgentName(), deps.getAgentOwnerToken(), deps.getAgentAliases());
97
+ deps.persistState();
98
+ deps.updateBadge();
99
+ }
100
+ function refreshSettings() {
101
+ const settings = loadSettingsFromFile();
102
+ deps.setSettings(settings);
103
+ deps.setBotToken(settings.botToken ?? process.env.SLACK_BOT_TOKEN);
104
+ deps.setAppToken(settings.appToken ?? process.env.SLACK_APP_TOKEN);
105
+ deps.setAllowedUsers(buildAllowlist(settings, process.env.SLACK_ALLOWED_USERS, process.env.SLACK_ALLOW_ALL_WORKSPACE_USERS));
106
+ const guardrails = settings.security ?? {};
107
+ deps.setGuardrails(guardrails);
108
+ deps.setReactionCommands(resolveReactionCommands(settings.reactionCommands));
109
+ deps.setSecurityPrompt(buildSecurityPrompt(guardrails));
110
+ const configuredSkinTheme = normalizePinetSkinTheme(settings.skinTheme);
111
+ deps.setActiveSkinTheme(configuredSkinTheme);
112
+ const role = deps.getBrokerRole() === "broker" ? "broker" : "worker";
113
+ const identitySeed = getIdentitySeedForRole(role);
114
+ const skinIdentity = resolveSkinAssignment(role, identitySeed);
115
+ if (skinIdentity) {
116
+ deps.setAgentName(skinIdentity.name);
117
+ deps.setAgentEmoji(skinIdentity.emoji);
118
+ deps.setAgentPersonality(skinIdentity.personality);
119
+ return;
120
+ }
121
+ const refreshedIdentity = resolveRuntimeAgentIdentity({ name: deps.getAgentName(), emoji: deps.getAgentEmoji() }, settings, process.env.PI_NICKNAME, identitySeed, role);
122
+ deps.setAgentName(refreshedIdentity.name);
123
+ deps.setAgentEmoji(refreshedIdentity.emoji);
124
+ deps.setAgentPersonality(null);
125
+ }
126
+ function snapshotReloadableRuntime() {
127
+ return {
128
+ settings: structuredClone(deps.getSettings()),
129
+ botToken: deps.getBotToken(),
130
+ appToken: deps.getAppToken(),
131
+ allowedUsers: deps.getAllowedUsers() ? new Set(deps.getAllowedUsers()) : null,
132
+ guardrails: structuredClone(deps.getGuardrails()),
133
+ reactionCommands: new Map(deps.getReactionCommands()),
134
+ securityPrompt: deps.getSecurityPrompt(),
135
+ agentName: deps.getAgentName(),
136
+ agentEmoji: deps.getAgentEmoji(),
137
+ activeSkinTheme: deps.getActiveSkinTheme(),
138
+ agentPersonality: deps.getAgentPersonality(),
139
+ agentAliases: [...deps.getAgentAliases()],
140
+ };
141
+ }
142
+ function restoreReloadableRuntime(snapshot) {
143
+ deps.setSettings(structuredClone(snapshot.settings));
144
+ deps.setBotToken(snapshot.botToken);
145
+ deps.setAppToken(snapshot.appToken);
146
+ deps.setAllowedUsers(snapshot.allowedUsers ? new Set(snapshot.allowedUsers) : null);
147
+ deps.setGuardrails(structuredClone(snapshot.guardrails));
148
+ deps.setReactionCommands(new Map(snapshot.reactionCommands));
149
+ deps.setSecurityPrompt(snapshot.securityPrompt);
150
+ deps.setAgentName(snapshot.agentName);
151
+ deps.setAgentEmoji(snapshot.agentEmoji);
152
+ deps.setActiveSkinTheme(snapshot.activeSkinTheme);
153
+ deps.setAgentPersonality(snapshot.agentPersonality);
154
+ deps.setAgentOwnerToken(buildPinetOwnerToken(getStableIdForRole(deps.getBrokerRole() === "broker" ? "broker" : "worker")));
155
+ const agentAliases = deps.getAgentAliases();
156
+ agentAliases.clear();
157
+ for (const alias of snapshot.agentAliases) {
158
+ if (alias && alias !== deps.getAgentName()) {
159
+ agentAliases.add(alias);
160
+ }
161
+ }
162
+ }
163
+ function detectProjectTools(repoRoot, cwd) {
164
+ const tools = new Set();
165
+ for (const candidate of [path.join(cwd, "package.json"), path.join(repoRoot, "package.json")]) {
166
+ try {
167
+ if (!fs.existsSync(candidate)) {
168
+ continue;
169
+ }
170
+ const parsed = JSON.parse(fs.readFileSync(candidate, "utf-8"));
171
+ const scripts = parsed.scripts ?? {};
172
+ if (scripts.test)
173
+ tools.add("test");
174
+ if (scripts.lint)
175
+ tools.add("lint");
176
+ if (scripts.typecheck)
177
+ tools.add("typecheck");
178
+ if (scripts.build)
179
+ tools.add("build");
180
+ }
181
+ catch {
182
+ // Ignore unreadable package.json files.
183
+ }
184
+ }
185
+ tools.add("git");
186
+ return [...tools].sort();
187
+ }
188
+ const dynamicGitProbe = deps.getDynamicGitState ?? ((cwd) => probeGitDynamic(cwd));
189
+ async function getAgentMetadata(role = "worker") {
190
+ const gitContext = await deps.getGitContext();
191
+ const { cwd, repo, repoRoot } = gitContext;
192
+ const resolvedRepoRoot = repoRoot ?? cwd;
193
+ const tools = detectProjectTools(resolvedRepoRoot, cwd);
194
+ const scope = buildSlackCompatibilityScope();
195
+ let dynamic;
196
+ try {
197
+ dynamic = await dynamicGitProbe(cwd);
198
+ }
199
+ catch {
200
+ dynamic = { probeFailed: true };
201
+ }
202
+ const branch = dynamic.branch;
203
+ const workdirDirty = dynamic.dirty;
204
+ const gitProbeFailed = dynamic.probeFailed;
205
+ const tags = [
206
+ `role:${role}`,
207
+ `repo:${repo}`,
208
+ ...(branch ? [`branch:${branch}`] : []),
209
+ ...(workdirDirty === true ? ["workdir:dirty"] : []),
210
+ ...(gitProbeFailed ? ["git-probe-failed"] : []),
211
+ ...(scope.workspace?.provider ? [`scope-provider:${scope.workspace.provider}`] : []),
212
+ ...(scope.workspace?.compatibilityKey ? [`scope:${scope.workspace.compatibilityKey}`] : []),
213
+ ...tools.map((tool) => `tool:${tool}`),
214
+ ];
215
+ const skinAssignment = resolveSkinAssignment(role, getIdentitySeedForRole(role));
216
+ const brokerManaged = role === "worker" && process.env.PINET_BROKER_MANAGED === "1";
217
+ const brokerManagedMetadata = brokerManaged
218
+ ? {
219
+ brokerManaged: true,
220
+ brokerManagedBy: process.env.PINET_BROKER_AGENT_ID?.trim() || undefined,
221
+ launchSource: process.env.PINET_LAUNCH_SOURCE?.trim() || "broker-tmux",
222
+ tmuxSession: process.env.PINET_TMUX_SESSION?.trim() || undefined,
223
+ brokerManagedAt: new Date().toISOString(),
224
+ }
225
+ : {};
226
+ return {
227
+ cwd,
228
+ ...(branch ? { branch } : {}),
229
+ ...(typeof workdirDirty === "boolean" ? { workdirDirty } : {}),
230
+ ...(typeof dynamic.dirtyFileCount === "number"
231
+ ? { workdirDirtyFileCount: dynamic.dirtyFileCount }
232
+ : {}),
233
+ ...(gitProbeFailed ? { gitProbeFailed: true } : {}),
234
+ gitProbedAt: new Date().toISOString(),
235
+ host: os.hostname(),
236
+ role,
237
+ repo,
238
+ repoRoot,
239
+ scope,
240
+ ...brokerManagedMetadata,
241
+ ...(deps.getActiveSkinTheme() ? { skinTheme: deps.getActiveSkinTheme() } : {}),
242
+ ...(deps.getAgentPersonality() ? { personality: deps.getAgentPersonality() } : {}),
243
+ ...(skinAssignment?.statusVocabulary
244
+ ? { skinStatusVocabulary: skinAssignment.statusVocabulary }
245
+ : {}),
246
+ capabilities: {
247
+ repo,
248
+ repoRoot,
249
+ ...(branch ? { branch } : {}),
250
+ ...(typeof workdirDirty === "boolean" ? { workdirDirty } : {}),
251
+ role,
252
+ tools,
253
+ tags,
254
+ scope,
255
+ },
256
+ };
257
+ }
258
+ function asStringValue(value) {
259
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
260
+ }
261
+ function getMeshRoleFromMetadata(metadata, fallback = "worker") {
262
+ return asStringValue(metadata?.role) === "broker" ? "broker" : fallback;
263
+ }
264
+ function buildSkinMetadata(metadata, personality, statusVocabulary) {
265
+ const baseMetadata = { ...(metadata ?? {}) };
266
+ delete baseMetadata.skinStatusVocabulary;
267
+ return {
268
+ ...baseMetadata,
269
+ ...(deps.getActiveSkinTheme() ? { skinTheme: deps.getActiveSkinTheme() } : {}),
270
+ personality,
271
+ ...(statusVocabulary ? { skinStatusVocabulary: statusVocabulary } : {}),
272
+ };
273
+ }
274
+ function getIdentityGuidelines() {
275
+ return buildIdentityReplyGuidelines(deps.getAgentEmoji(), deps.getAgentName(), selfLocation);
276
+ }
277
+ function applyRegistrationIdentity(registration) {
278
+ deps.setActiveSkinTheme(asStringValue(registration.metadata?.skinTheme) ?? null);
279
+ applyLocalAgentIdentity(registration.name, registration.emoji, asStringValue(registration.metadata?.personality) ?? null);
280
+ }
281
+ return {
282
+ isUserAllowed,
283
+ maybeWarnSlackUserAccess,
284
+ maybeWarnSlackGuardrailPosture,
285
+ getStableIdForRole,
286
+ getIdentitySeedForRole,
287
+ getSkinSeed,
288
+ rememberAgentAlias,
289
+ resolveSkinAssignment,
290
+ applyLocalAgentIdentity,
291
+ refreshSettings,
292
+ snapshotReloadableRuntime,
293
+ restoreReloadableRuntime,
294
+ detectProjectTools,
295
+ getAgentMetadata,
296
+ asStringValue,
297
+ getMeshRoleFromMetadata,
298
+ buildSkinMetadata,
299
+ getIdentityGuidelines,
300
+ applyRegistrationIdentity,
301
+ };
302
+ }
@@ -0,0 +1,10 @@
1
+ import type { SlackBridgeSettings } from "./helpers.js";
2
+ export type SlackBridgeRuntimeMode = "off" | "single" | "broker" | "follower";
3
+ export declare function normalizeSlackBridgeRuntimeMode(value: string | null | undefined): SlackBridgeRuntimeMode | null;
4
+ export declare function isPinetRuntimeMode(mode: SlackBridgeRuntimeMode): boolean;
5
+ export interface ResolveSlackBridgeStartupRuntimeModeOptions {
6
+ brokerSocketExists?: boolean;
7
+ brokerManagedFollowerLaunch?: boolean;
8
+ }
9
+ export declare function isBrokerManagedFollowerLaunch(env?: NodeJS.ProcessEnv): boolean;
10
+ export declare function resolveSlackBridgeStartupRuntimeMode(settings: Pick<SlackBridgeSettings, "runtimeMode" | "autoConnect" | "autoFollow">, options?: ResolveSlackBridgeStartupRuntimeModeOptions): SlackBridgeRuntimeMode;