@pinet/slack-bridge 0.2.1 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,157 +1,185 @@
1
1
  # slack-bridge (Pinet)
2
2
 
3
- Slack assistant integration for [pi](https://github.com/badlogic/pi-mono) multi-agent broker, thread routing, and inbox tools powered by Socket Mode.
3
+ Connect pi coding agents to Slack. Pinet provides multi-agent coordination, thread routing, and inbox tools through Socket Mode.
4
4
 
5
- ## Install
5
+ ## Install Pinet
6
6
 
7
- Install the latest Pinet Slack bridge pi package:
7
+ Install the latest version:
8
8
 
9
9
  ```bash
10
10
  pi install npm:@pinet/slack-bridge
11
11
  ```
12
12
 
13
- Or pin an exact published version for reproducible installs:
13
+ Pin a specific version:
14
14
 
15
15
  ```bash
16
- pi install npm:@pinet/slack-bridge@0.1.0
16
+ pi install npm:@pinet/slack-bridge@0.2.2
17
17
  ```
18
18
 
19
- For package managers or local inspection, the npm package is also installable directly:
19
+ For direct npm installation:
20
20
 
21
21
  ```bash
22
22
  npm install @pinet/slack-bridge
23
23
  ```
24
24
 
25
- ## Package metadata and publishing
25
+ ## What you need
26
26
 
27
- This package declares pi package/gallery metadata in [`package.json`](./package.json):
27
+ - a Slack workspace where you can install apps
28
+ - Node.js 22 or later
29
+ - pi installed on your system
28
30
 
29
- - `keywords` includes `pi-package` for gallery discovery.
30
- - `pi.extensions` points at the built extension entrypoint, `./dist/index.js`.
31
- - `pi.skills` points at the bundled skill directory, `./skills`.
32
- - No `pi.image` or `pi.video` preview is declared yet because this package does
33
- not currently include a reviewed gallery image/video asset.
31
+ ## Set up your Slack app
34
32
 
35
- The published tarball is expected to include the package metadata, README,
36
- Slack app manifest, built `dist/` files, bundled `skills/`, and LICENSE. Verify
37
- that locally with:
33
+ ### Create the app
38
34
 
39
- ```bash
40
- cd slack-bridge
41
- npm pack --dry-run
42
- ```
43
-
44
- This package is included in the full npm publish set tracked in
45
- [`../plans/npm-publish.md`](../plans/npm-publish.md). Use the GitHub Actions
46
- workflow's default dry-run/readiness path for validation; do not publish, tag, or
47
- bump versions without explicit maintainer release approval.
48
-
49
- ## Prerequisites
50
-
51
- - A Slack workspace where you have permission to install apps
52
- - Node.js 22+ (uses native `fetch` and `WebSocket`)
53
- - [pi](https://github.com/badlogic/pi-mono) installed
54
-
55
- ## Slack App Setup
35
+ 1. Go to [api.slack.com/apps](https://api.slack.com/apps)
36
+ 2. Select 'Create New App'
37
+ 3. Choose 'From a manifest'
38
+ 4. Select your workspace
39
+ 5. Paste the contents of [`manifest.yaml`](./manifest.yaml)
40
+ 6. If you want a different Slack command, change `features.slash_commands[0].command` before creating and set `slackCommandName` or `slackCommandNames` in `settings.json`
41
+ 7. Select 'Create'
56
42
 
57
- ### 1. Create the app
43
+ The manifest configures Socket Mode, the assistant view, bot scopes, event subscriptions, and slash commands automatically.
58
44
 
59
- 1. Go to [api.slack.com/apps](https://api.slack.com/apps) → **Create New App**
60
- 2. Choose **From a manifest**
61
- 3. Select your workspace
62
- 4. Paste the contents of [`manifest.yaml`](./manifest.yaml) from this directory
63
- 5. If the Slack app is not named Pinet, change `features.slash_commands[0].command` before creating the app (for example, Oathgate uses `/oathgate` instead of the packaged `/pinet` default)
64
- 6. Click **Create**
45
+ ### Get your tokens
65
46
 
66
- The manifest configures Socket Mode, the assistant view, all required bot scopes, event subscriptions, and the packaged Pinet slash-command default automatically.
47
+ Generate two tokens:
67
48
 
68
- ### 2. Generate tokens
49
+ | Token | Where to find it | Format |
50
+ | --------------- | ------------------------------------------------------------------------------- | ------------ |
51
+ | App-Level Token | Basic Information → App-Level Tokens → Generate (add `connections:write` scope) | `xapp-1-...` |
52
+ | Bot Token | OAuth & Permissions → Install to Workspace → Bot User OAuth Token | `xoxb-...` |
69
53
 
70
- You need two tokens:
54
+ ### Required bot scopes
71
55
 
72
- | Token | Where to find it | Looks like |
73
- | ------------------- | -------------------------------------------------------------------------------- | ------------ |
74
- | **App-Level Token** | Basic Information → App-Level Tokens → Generate (with `connections:write` scope) | `xapp-1-...` |
75
- | **Bot Token** | OAuth & Permissions → Install to Workspace → Bot User OAuth Token | `xoxb-...` |
76
-
77
- ### 3. Required bot scopes
78
-
79
- These are included in the manifest, but for reference:
56
+ The manifest includes these scopes:
80
57
 
81
58
  ```
82
59
  app_mentions:read assistant:write bookmarks:read
83
60
  bookmarks:write canvases:read canvases:write
84
61
  channels:history channels:read chat:write
85
- commands files:read files:write groups:history
86
- groups:read im:history im:read
87
- im:write pins:read pins:write
88
- reactions:read reactions:write users:read
62
+ commands files:read files:write
63
+ groups:history groups:read im:history
64
+ im:read im:write pins:read
65
+ pins:write reactions:read reactions:write
66
+ users:read
89
67
  ```
90
68
 
91
- `commands` is required for the Slack slash-command surface (`/<app> agents list [all]`). `files:read` is required because Slack exposes canvas comment pagination through `files.info`, even when the target is first validated via canvas-specific APIs.
69
+ The `commands` scope enables slash commands. The `files:read` scope is needed because Slack uses `files.info` for canvas comment pagination.
92
70
 
93
- Slack thread shimmer/status updates use `assistant.threads.setStatus`; Slack's 2026 scope update allows this method with the existing `chat:write` bot scope, so no new `assistant:write` scope is needed for status-only support.
71
+ ## Configure Pinet
94
72
 
95
- ## Configuration
96
-
97
- Add your tokens to `~/.pi/agent/settings.json`:
73
+ Add tokens, runtime mode, and access rules to `~/.pi/agent/settings.json`:
98
74
 
99
75
  ```json
100
76
  {
101
77
  "slack-bridge": {
102
78
  "botToken": "xoxb-your-bot-token",
103
- "appToken": "xapp-your-app-token"
79
+ "appToken": "xapp-your-app-token",
80
+ "runtimeMode": "single",
81
+ "allowedUsers": ["U_YOUR_USER_ID"]
104
82
  }
105
83
  }
106
84
  ```
107
85
 
108
- That's it for a minimal setup. Start pi and Pinet appears in Slack's sidebar.
86
+ Pinet stays off unless you set `runtimeMode`, `autoConnect`, or `autoFollow`. Start pi after you configure access.
109
87
 
110
- ### Environment variables (alternative)
88
+ ### Use environment variables instead
111
89
 
112
90
  ```bash
113
91
  export SLACK_BOT_TOKEN="xoxb-..."
114
92
  export SLACK_APP_TOKEN="xapp-..."
115
93
  ```
116
94
 
117
- Settings in `settings.json` take priority over env vars.
95
+ Settings in `settings.json` override environment variables.
118
96
 
119
- ### Optional Pinet mesh auth
97
+ ## Control who can use Pinet
120
98
 
121
- Shared-secret mesh auth is **optional**. You can configure it with either settings keys or environment variables:
99
+ Slack access is default-deny. Configure one of these:
100
+
101
+ - `allowedUsers`: list specific Slack user IDs
102
+ - `allowAllWorkspaceUsers: true`: allow everyone in the workspace
103
+
104
+ Example with specific users:
122
105
 
123
106
  ```json
124
107
  {
125
108
  "slack-bridge": {
126
- "meshSecret": "shared-secret"
109
+ "botToken": "xoxb-...",
110
+ "appToken": "xapp-...",
111
+ "allowedUsers": ["U_USER_ID_1", "U_USER_ID_2"]
112
+ }
113
+ }
114
+ ```
115
+
116
+ Find user IDs by selecting a user's profile in Slack and choosing 'Copy member ID'.
117
+
118
+ ## Optional settings
119
+
120
+ ### Mesh authentication
121
+
122
+ Shared-secret authentication is optional. Configure it with settings or environment variables:
123
+
124
+ Settings:
125
+
126
+ ```json
127
+ {
128
+ "slack-bridge": {
129
+ "meshSecret": "your-shared-secret"
127
130
  }
128
131
  }
129
132
  ```
130
133
 
134
+ Or use a file:
135
+
131
136
  ```json
132
137
  {
133
138
  "slack-bridge": {
134
- "meshSecretPath": "/Users/alice/.config/pi/pinet.secret"
139
+ "meshSecretPath": "/path/to/secret.txt"
135
140
  }
136
141
  }
137
142
  ```
138
143
 
144
+ Environment variables:
145
+
139
146
  ```bash
140
- export PINET_MESH_SECRET="shared-secret"
147
+ export PINET_MESH_SECRET="your-shared-secret"
141
148
  # or
142
- export PINET_MESH_SECRET_PATH="$HOME/.config/pi/pinet.secret"
149
+ export PINET_MESH_SECRET_PATH="/path/to/secret.txt"
143
150
  ```
144
151
 
145
- Behavior and precedence:
152
+ How it works:
153
+
154
+ - settings override environment variables
155
+ - inline secrets override file paths
156
+ - if nothing is set, mesh auth is disabled
157
+ - brokers create the secret file if it does not exist
158
+ - followers need an existing file or will show an error
146
159
 
147
- - `slack-bridge.meshSecret` and `slack-bridge.meshSecretPath` override the environment fallbacks.
148
- - Inline secrets win over secret paths. If `meshSecret` or `PINET_MESH_SECRET` is set, the corresponding `*Path` value is ignored.
149
- - If all four values are unset, broker/follower mesh auth is disabled.
150
- - A broker started with `meshSecretPath` creates the secret file if it does not exist yet.
151
- - A follower started with `meshSecretPath` does **not** create the file. If the configured file is missing, follow fails with a clear error telling you to point at an existing file, provide `meshSecret` directly, or leave both unset to disable shared-secret auth.
152
- - A follower configured for mesh auth will fail closed against an older/no-auth broker with a clear compatibility error. It will **not** silently retry as an unauthenticated follower.
160
+ ### Require mentions in channels
153
161
 
154
- ### Full settings reference
162
+ Make Pinet respond only when mentioned in specific channels:
163
+
164
+ ```json
165
+ {
166
+ "slack-bridge": {
167
+ "ingressGuard": {
168
+ "requireMention": {
169
+ "channels": ["C_CHANNEL_ID"],
170
+ "mixedParticipantThreads": {
171
+ "enabled": true,
172
+ "trustedUsers": ["U_TRUSTED_USER"]
173
+ }
174
+ }
175
+ }
176
+ }
177
+ }
178
+ ```
179
+
180
+ This is separate from `allowedUsers`. Authorization decides who can use Pinet. The guard decides when a mention is needed.
181
+
182
+ ### All configuration options
155
183
 
156
184
  ```json
157
185
  {
@@ -159,16 +187,32 @@ Behavior and precedence:
159
187
  "botToken": "xoxb-...",
160
188
  "appToken": "xapp-...",
161
189
  "runtimeMode": "single",
162
- "allowedUsers": ["U_EXAMPLE_MEMBER_ID"],
163
- "defaultChannel": "C_EXAMPLE_CHANNEL_ID",
190
+ "allowedUsers": ["U_USER_ID"],
191
+ "allowAllWorkspaceUsers": false,
192
+ "ingressGuard": {
193
+ "requireMention": {
194
+ "channels": ["C_CHANNEL_ID"],
195
+ "mixedParticipantThreads": {
196
+ "enabled": true,
197
+ "trustedUsers": ["U_USER_ID"]
198
+ }
199
+ }
200
+ },
201
+ "defaultChannel": "C_CHANNEL_ID",
164
202
  "logChannel": "#pinet-logs",
165
203
  "logLevel": "actions",
166
- "autoFollow": true,
204
+ "autoConnect": false,
205
+ "autoFollow": false,
167
206
  "ralphLoopIntervalMs": 300000,
168
207
  "ralphSnoozeAfterEmptyCycles": 0,
169
208
  "ralphSnoozeDurationMs": 1800000,
170
- "meshSecretPath": "/Users/alice/.config/pi/pinet.secret",
171
- "suggestedPrompts": [{ "title": "Status", "message": "What are you working on?" }],
209
+ "meshSecretPath": "/path/to/secret",
210
+ "suggestedPrompts": [
211
+ {
212
+ "title": "Status",
213
+ "message": "What are you working on?"
214
+ }
215
+ ],
172
216
  "security": {
173
217
  "readOnly": false,
174
218
  "requireConfirmation": ["slack:create_channel"],
@@ -178,445 +222,255 @@ Behavior and precedence:
178
222
  }
179
223
  ```
180
224
 
181
- Slack access is now **default-deny** unless you configure one of these explicitly:
182
-
183
- - `allowedUsers` / `SLACK_ALLOWED_USERS` allow only specific Slack user IDs
184
- - `allowAllWorkspaceUsers: true` / `SLACK_ALLOW_ALL_WORKSPACE_USERS=true` explicit workspace-wide opt-in
185
-
186
- | Key | Required | Description |
187
- | ------------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------- |
188
- | `botToken` | **yes** | Bot User OAuth Token (`xoxb-...`) |
189
- | `appToken` | **yes** | App-Level Token for Socket Mode (`xapp-...`) |
190
- | `allowedUsers` | no | Slack user IDs that can interact; when unset, access is denied unless `allowAllWorkspaceUsers` is true |
191
- | `allowAllWorkspaceUsers` | no | Explicit opt-in for workspace-wide Slack access when you do not want a user allowlist |
192
- | `defaultChannel` | no | Default channel for the `slack` dispatcher `post_channel` action |
193
- | `logChannel` | no | Channel for broker activity logs |
194
- | `logLevel` | no | `"errors"`, `"actions"` (default), or `"verbose"` |
195
- | `runtimeMode` | no | Explicit startup mode: `"off"`, `"single"`, `"broker"`, or `"follower"` |
196
- | `autoConnect` | no | Legacy compatibility alias for `runtimeMode: "single"` |
197
- | `autoFollow` | no | Legacy compatibility alias for follower startup when a broker socket exists |
198
- | `ralphLoopIntervalMs` | no | Broker RALPH maintenance cadence in milliseconds; defaults to `300000` (5 minutes), valid range `1000`-`2147483647` |
199
- | `ralphSnoozeAfterEmptyCycles` | no | Broker RALPH auto-snooze trigger after N empty cycles; defaults to `0` (disabled), valid range `0`-`100` |
200
- | `ralphSnoozeDurationMs` | no | Broker RALPH auto-snooze duration in milliseconds; defaults to `1800000` (30 minutes), valid range `60000`-`86400000` |
201
- | `skinTheme` | no | Pinet presentation skin selected at broker startup/reload (`default`, `foundation`, `cosmere`, or free-form) |
202
- | `slackCommandName` | no | Slack web app slash command name for `agents list`; defaults to `/pinet`, or `/oathgate` for Oathgate/Cosmere skins |
203
- | `slackCommandNames` | no | Optional list of accepted/deployed Slack slash command aliases when one app needs multiple command names |
204
- | `meshSecret` | no | Optional inline Pinet shared secret; overrides `meshSecretPath` and env fallbacks |
205
- | `meshSecretPath` | no | Optional path to a shared-secret file; broker creates it if missing, followers require an existing file |
206
- | `suggestedPrompts` | no | Prompts shown when a user opens a new conversation |
207
- | `security.readOnly` | no | Runtime-block write-capable tools for Slack-triggered turns, including core tools like `bash`, `edit`, and `write` |
208
- | `security.requireConfirmation` | no | Runtime-require Slack approval before matching tools execute; core tools need a specific Slack thread context |
209
- | `security.blockedTools` | no | Runtime-block matching tools for Slack-triggered turns, including core tools |
210
-
211
- ## Scope carrier model (compatibility-first)
212
-
213
- Slack/Pinet now threads a first-class runtime `scope` carrier through shared message contracts and runtime metadata.
214
-
215
- For this first slice:
216
-
217
- - **workspace/install scope** is carried as compatibility-first metadata for Slack
218
- - **instance scope** is also carried as a first-class compatibility carrier
219
- - today’s single-workspace deployments use one default compatibility scope
220
- - a missing or empty Slack `teamId` stays **unknown** — the bridge does not invent a fake workspace ID
221
- - these carriers are metadata only in this slice; enforcement and multi-install behavior land later in `#547` / `#550`
222
-
223
- ## Usage
224
-
225
- Once configured, Pinet appears in Slack's sidebar. Users open it, type a message, and the pi agent responds.
225
+ | Setting | Description | Default |
226
+ | ------------------------ | ------------------------------------------------------ | -------------------- |
227
+ | `botToken` | Bot User OAuth Token (required) | none |
228
+ | `appToken` | App-Level Token for Socket Mode (required) | none |
229
+ | `runtimeMode` | How Pinet runs (`off`, `single`, `broker`, `follower`) | `off` |
230
+ | `allowedUsers` | Slack user IDs who can use Pinet | none |
231
+ | `allowAllWorkspaceUsers` | Allow all workspace members | `false` |
232
+ | `defaultChannel` | Where to post updates | none |
233
+ | `logChannel` | Where to post logs | none |
234
+ | `logLevel` | What to log (`errors`, `actions`, `verbose`) | `actions` |
235
+ | `autoConnect` | Start as a single instance when `runtimeMode` is unset | `false` |
236
+ | `autoFollow` | Start as follower if broker exists | `false` |
237
+ | `ralphLoopIntervalMs` | How often to check for stalls (milliseconds) | `300000` (5 minutes) |
238
+ | `meshSecret` | Shared secret for mesh auth | none |
239
+ | `meshSecretPath` | File containing shared secret | none |
226
240
 
227
- ```
228
- User opens Pinet in Slack sidebar
229
- └─► types a message
230
- └─► 👀 reaction appears (thinking)
231
- └─► message queued for pi agent
232
- └─► agent responds via slack_send
233
- └─► 👀 removed, reply appears in thread
234
- ```
241
+ ## Using Pinet
235
242
 
236
- Messages queue while the agent is busy. When the agent finishes, it automatically drains the inbox and responds.
237
-
238
- ### Reaction triggers
239
-
240
- Slack emoji reactions are ignored by default: they do not enqueue Pinet work, trigger reviews, steer agents, interrupt owners, or cause broker/worker replies. To opt in deliberately, configure `reactionCommands` for the exact emoji aliases that should become structured Pinet requests from the reacted-to Slack message. Even configured reactions are accepted only inside an already authorized Pinet thread (for example a thread with a current Pinet owner, or persisted Slack assistant-thread context). Reaction authorization is deny-by-default: it requires an explicit broker-backed authorization gate, and a thread the adapter has merely seen or cached never qualifies on its own. Reactions in ordinary, uninvoked Slack channel threads remain no-op — even from authorized users — and they do not enqueue work, persist thread state, claim ownership, or receive a Slack ACK. Messages and interactive events from users outside the allowlist also never mint known-thread state that could later admit reactions or replies. Pinet adds ✅ only when it accepts an opt-in reaction-triggered request. If it cannot process an accepted opted-in reaction, it adds ❌; check broker logs for the underlying Slack/API error. When Slack cannot return the reacted message text, Pinet can still route configured reactions when the message timestamp itself identifies an already authorized thread; otherwise it ignores the reaction safely.
241
-
242
- ### Available tools
243
-
244
- Slack-bridge uses progressive disclosure to keep the per-turn tool surface
245
- small:
246
-
247
- | Tool | Description |
248
- | ------------- | --------------------------------------------------------------------------- |
249
- | `slack_inbox` | Hot-path inbox drain for pending incoming Slack messages |
250
- | `slack_send` | Hot-path reply tool for Slack assistant threads |
251
- | `slack` | Dispatcher for all non-hot Slack actions; call `action: "help"` for schemas |
252
-
253
- Cold Slack actions live behind the `slack` dispatcher:
254
-
255
- | Dispatcher action | Description |
256
- | ---------------------- | --------------------------------------------------------------------------------- |
257
- | `react` | Add an emoji reaction to a message |
258
- | `read` | Read messages from a thread |
259
- | `upload` | Upload files, snippets, or diffs into Slack |
260
- | `file` | Download Slack-hosted files to a controlled local temp cache by file ID |
261
- | `schedule` | Schedule a message for later delivery |
262
- | `post_channel` | Post to a channel (by name or ID) |
263
- | `delete` | Delete a bot-posted message or an entire thread |
264
- | `read_channel` | Read channel history or a thread in a channel |
265
- | `create_channel` | Create a new Slack channel |
266
- | `project_create` | Create a project channel + RFC canvas + bot invite in one call |
267
- | `pin` | Pin or unpin a message |
268
- | `bookmark` | Add, list, or remove channel bookmarks |
269
- | `export` | Export a thread as markdown, plain text, or JSON |
270
- | `presence` | Check if users are active, away, or in DND |
271
- | `canvas_comments_read` | Read comments attached to a verified canvas by canvas ID or channel canvas lookup |
272
- | `canvas_create` | Create a standalone or channel canvas |
273
- | `canvas_update` | Append, prepend, or replace canvas content |
274
- | `modal_open` | Open a modal from a trigger interaction |
275
- | `modal_push` | Push a new step onto a modal stack |
276
- | `modal_update` | Update an existing open modal |
277
- | `confirm_action` | Request user confirmation before a dangerous action |
278
-
279
- Use `slack` with `action: "help"` for the action catalogue, or
280
- `action: "help", args: { "topic": "canvas_update" }` for a specific JSON
281
- schema and example invocations. Dispatcher responses use a consistent
282
- `{ "status", "data", "errors", "warnings" }` envelope. Guardrails match
283
- cold Slack actions as `slack:<action>` (for example `slack:upload` or
284
- `slack:canvas_update`); legacy `slack_<action>` patterns are accepted during
285
- migration.
286
-
287
- #### Tool and workflow usage notes
288
-
289
- - **Reply where the work arrived.** Use `slack_send` for assistant-thread
290
- replies. If a task was delivered in a Slack thread, acknowledge briefly,
291
- do the work, report blockers immediately, and finish with the outcome. If
292
- you know only a channel/thread pair, use dispatcher action `post_channel`
293
- with `channel` and optional `thread_ts` instead.
294
- - **Channel posting is explicit.** `post_channel` posts to a named channel or
295
- channel ID. When `channel` is omitted, it first resolves a provided
296
- `thread_ts` to a tracked thread channel, then falls back to `defaultChannel`
297
- from settings. `slack_send` is intentionally narrower and resolves the
298
- current tracked assistant thread/DM context.
299
- - **Rich messages use Block Kit JSON.** Pass `blocks` directly to
300
- `slack_send` or `post_channel`; keep `text` as the notification/fallback.
301
- Block Kit builder tools are not registered by this package. Load the bundled
302
- `slack-bridge` skill for copyable status-report, button, code, and diff
303
- templates. The package also bundles `pinet-skin-creator` for safely drafting
304
- or reviewing curated Pinet skin descriptors and character/status-vocabulary
305
- pools before changing runtime skin wiring.
306
- - **Modal helpers are patterns, not hot tools.** Use dispatcher actions
307
- `modal_open`, `modal_push`, and `modal_update` with Slack view JSON. Open or
308
- push immediately after receiving a fresh `trigger_id`; Slack trigger IDs
309
- expire quickly. Include `thread_ts` when submissions should route back to an
310
- original assistant thread.
311
- - **Uploads are for bulky artifacts.** Use `upload` for logs, screenshots,
312
- long diffs, and generated files instead of large inline messages. Inline
313
- uploads require `filename`; path uploads are guarded and must stay within the
314
- current working directory or system temp directory. `slack_send` also accepts
315
- `files: [{ path, filename?, title?, filetype? }]` so one assistant-thread
316
- reply can contain both text and local binary attachments in the same Slack
317
- file upload message. Slack external file uploads cannot include Block Kit in
318
- that same message, so omit `blocks` when sending files or send a separate
319
- block-only reply.
320
- - **Inbound Slack files are fetched explicitly.** Incoming file-share messages
321
- preserve safe `slackFiles` metadata such as file ID, name, type, size, and
322
- permalink, but private Slack download URLs are not exposed in normal tool
323
- output. To inspect raw content, call dispatcher action `file` with
324
- `op: "download"`, `file_id`, and optionally `thread_ts`, `message_ts`, and
325
- `channel`. The bot fetches the file with Slack bot auth, stores it under the
326
- system temp `pi-slack-files` cache with best-effort TTL cleanup, and returns a
327
- descriptor containing the local path, filename, type, size, SHA-256, expiry,
328
- and residual privacy risks.
329
- - **Upload host egress note.** The second upload leg goes to Slack file upload
330
- hosts (`files.slack.com`/`uploads.slack.com`) for the raw payload. In
331
- environments with restricted egress this can fail with `403` (proxy
332
- allowlist) or DNS errors after `files.getUploadURLExternal`; verify the proxy
333
- allowlist first, or route through an environment that can reach those hosts.
334
- - **Upload metadata note.** Slack snippet uploads attempt to use inferred
335
- `snippet_type` values for inline content and retry with plain upload metadata
336
- when Slack returns `invalid_arguments`, preserving syntax highlighting for
337
- supported types while avoiding hard failures on unsupported snippet types.
338
- - **Canvases are long-lived docs.** `canvas_create` creates standalone or
339
- channel canvases. If Slack rejects channel tab creation with
340
- `canvas_tab_creation_failed`, it falls back to a standalone canvas attached to
341
- the channel, attempts to bookmark the canvas URL, and returns the fallback
342
- `canvas_id` for future `canvas_update` calls. `canvas_update` can append,
343
- prepend, replace the whole canvas, or replace a matched section;
344
- `canvas_comments_read` is read-only and limited to verified canvas targets.
345
- - **Scheduling, pins, and bookmarks are durable affordances.** Use `schedule`
346
- for delayed reminders instead of waiting; use `pin` for important thread
347
- messages; use `bookmark` for persistent channel-header links to repos,
348
- dashboards, docs, or runbooks.
349
- - **Presence helps choose timing.** Use `presence` before pinging humans when
350
- active/away/DND status affects routing or whether to schedule a follow-up.
351
- - **Destructive actions stay constrained.** `delete` can remove only messages
352
- posted by the current bot and every delete call requires `confirm: true`.
353
- Whole-thread deletion additionally requires `thread: true` and succeeds only
354
- when every message in the target thread belongs to the current bot. Prefer
355
- asking for explicit approval before destructive cleanup.
356
- - **Confirm guarded actions in the same thread.** If guardrails require
357
- confirmation, call `confirm_action` with the target `thread_ts`, exact tool
358
- name, and the exact action string required by the guarded tool. The safest
359
- flow is: attempt the guarded call, copy the `requires confirmation for action
360
- ...` string from the error, request confirmation, wait for the user's approval
361
- via `slack_inbox`, then retry the guarded call unchanged. Batched
362
- multi-thread Slack turns cannot satisfy a single-thread confirmation.
363
- - **Plain emoji reactions are not tasks.** Slack emoji reactions are ignored
364
- unless `reactionCommands` explicitly opts that emoji into structured
365
- reaction-trigger handling and the reacted message belongs to an already
366
- authorized Pinet thread. If an opt-in reaction-triggered request or a Block
367
- Kit/modal interaction payload arrives through `slack_inbox` with metadata,
368
- treat it as a user instruction tied to the referenced Slack thread or
369
- message.
370
-
371
- #### Common dispatcher examples
372
-
373
- Reply in the current Slack assistant thread with Block Kit:
243
+ ### In Slack
374
244
 
375
- ```json
376
- {
377
- "text": "Deploy complete — branch main, checks passed.",
378
- "blocks": [
379
- {
380
- "type": "section",
381
- "fields": [
382
- { "type": "mrkdwn", "text": "*Branch*\n`main`" },
383
- { "type": "mrkdwn", "text": "*Checks*\n✅ lint/typecheck/test" }
384
- ]
385
- }
386
- ]
387
- }
388
- ```
245
+ Talk to Pinet:
389
246
 
390
- Post a channel/thread update through the dispatcher:
247
+ - Direct message: open a DM with Pinet
248
+ - In channels: mention `@pinet` (or your app name)
249
+ - Slack slash command: type `/pinet agents list` or `/pinet agents list all`
391
250
 
392
- ```json
393
- {
394
- "action": "post_channel",
395
- "args": {
396
- "channel": "#pinet-logs",
397
- "thread_ts": "1712345678.000100",
398
- "text": "PR #123 is ready for review."
399
- }
400
- }
401
- ```
251
+ ### Pi commands
402
252
 
403
- Upload a generated diff snippet:
253
+ Run these inside pi.
404
254
 
405
- ```json
406
- {
407
- "action": "upload",
408
- "args": {
409
- "content": "diff --git a/README.md b/README.md\n...",
410
- "filename": "docs.diff",
411
- "filetype": "diff",
412
- "title": "Docs changes",
413
- "thread_ts": "1712345678.000100"
414
- }
415
- }
416
- ```
255
+ Main commands:
256
+
257
+ - `/pinet` - show available commands
258
+ - `/pinet status` - show current Pinet status
259
+ - `/pinet logs` - show recent broker activity logs
260
+ - `/pinet rename [name]` - rename this agent
261
+ - `/pinet free` - mark this agent idle
262
+
263
+ Coordinator commands:
417
264
 
418
- Request confirmation before a guarded destructive action after copying the
419
- exact action string from the guardrail error:
265
+ - `/pinet start` or `/pinet broker` - become the broker
266
+ - `/pinet follow` - become a follower
267
+ - `/pinet unfollow` - disconnect from broker
268
+ - `/pinet reload <agent>` - ask another agent to reload
269
+ - `/pinet exit <agent>` - ask another agent to exit
270
+ - `/pinet snooze [duration|off|status]` - quiet empty RALPH cycles
271
+ - `/pinet subtree [start|status|spawn|stop]` - manage subtree broker mode
272
+
273
+ ### From pi
274
+
275
+ Use the Pinet dispatcher for agent coordination:
420
276
 
421
277
  ```json
422
278
  {
423
- "action": "confirm_action",
279
+ "action": "send",
424
280
  "args": {
425
- "thread_ts": "1712345678.000100",
426
- "tool": "slack:delete",
427
- "action": "channel=#pinet-logs | thread_ts=1712345678.000100 | ts=1712345678.000200 | thread=false"
281
+ "to": "@worker",
282
+ "message": "Please review PR #123"
428
283
  }
429
284
  }
430
285
  ```
431
286
 
432
- #### Canvas comment inspection
433
-
434
- The `canvas_comments_read` dispatcher action is intentionally narrow:
435
-
436
- - it validates the target with `canvases.sections.lookup` before reading comment pages via `files.info`
437
- - it needs `files:read` because Slack exposes canvas comments through the file API surface
438
- - it will **not** inspect generic Slack files, non-canvas file comments, or full canvas body/history
287
+ Common actions:
439
288
 
440
- ### Slash commands
289
+ - `send` - send a message to an agent or broker-only channel
290
+ - `read` - read this agent's inbox
291
+ - `schedule` - schedule a future wake-up
292
+ - `free` - mark this agent idle
293
+ - `help` - discover actions and schemas
441
294
 
442
- | Command | Description |
443
- | -------------------------- | ---------------------------------------------------------- |
444
- | `/pinet <action>` | Unified Pinet command surface; run `/pinet help` for usage |
445
- | `/pinet status` | Show connection status, threads, and agent identity |
446
- | `/pinet rename` | Change the agent's display name |
447
- | `/pinet logs` | Show recent broker activity log entries |
448
- | `/<app> agents list [all]` | Slack-native broker roster, workload, task, and lane view |
295
+ Use `slack_send` for hot-path Slack replies. Use the `slack` dispatcher for uploads, canvases, pins, bookmarks, and other Slack actions.
449
296
 
450
- ## Runtime modes
297
+ ## Architecture
451
298
 
452
- `slack-bridge` now treats runtime mode as an explicit concept:
299
+ ### Broker and followers
453
300
 
454
- | Mode | Meaning |
455
- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
456
- | `off` | Slack bridge is loaded, but **no Slack Socket Mode ingress** and no coordination runtime are started. |
457
- | `single` | One local Pi session owns Slack ingress and local thread/inbox ownership only. No broker DB/socket/client, no RALPH/control plane, no mesh auth, no multi-agent surface. |
458
- | `broker` | The session runs the broker coordination runtime. |
459
- | `follower` | The session connects to an existing broker as a worker runtime. |
301
+ Pinet can run as:
460
302
 
461
- Startup selection:
303
+ - single - one instance handles everything
304
+ - broker - coordinates and routes messages
305
+ - follower - receives work from the broker
462
306
 
463
- - `runtimeMode` is the explicit startup selector.
464
- - `autoConnect` is a legacy compatibility alias for `runtimeMode: "single"`.
465
- - `autoFollow` is a legacy compatibility alias for `runtimeMode: "follower"` when a broker socket is available.
466
- - explicit `runtimeMode` wins over the legacy flags.
467
- - `/pinet start` and `/pinet follow` still switch the live session into broker/follower runtimes explicitly.
307
+ The broker:
468
308
 
469
- ## Scope carriers (compatibility-first)
309
+ - watches Slack for messages
310
+ - assigns work to agents
311
+ - tracks who owns what
312
+ - syncs state across followers
470
313
 
471
- `slack-bridge` now emits first-class runtime scope carriers in shared transport contracts and agent runtime metadata.
314
+ Followers:
472
315
 
473
- - `scope.workspace` models the current Slack install/workspace scope.
474
- - `scope.instance` models the current broker/runtime instance scope.
475
- - in the first slice, both stay **compatibility-first**: today’s singleton runtime gets one default compatibility scope
476
- - if Slack omits `team_id`, the carrier keeps the workspace id **unknown** instead of inventing a fake one
477
- - this slice is metadata/plumbing only: it does **not** change routing, enforcement, or multi-install orchestration yet
316
+ - connect to the broker
317
+ - receive assigned work
318
+ - stay in sync automatically
478
319
 
479
- ## Pinet (Multi-Agent Mode)
320
+ ### RALPH maintenance loop
480
321
 
481
- Pinet supports a broker/follower architecture for coordinating multiple pi agents over Slack.
322
+ RALPH keeps broker state healthy. It:
482
323
 
483
- ### Runtime composition boundary
324
+ - runs every 5 minutes by default
325
+ - checks worker presence
326
+ - releases stale claims held by unavailable workers
327
+ - observes pending backlog while broker maintenance handles assignment
328
+ - triggers wake-ups
484
329
 
485
- Broker startup is composed as Pinet core plus injected transport adapter factories. `broker-runtime.ts` starts the broker DB/socket/router and skin/agent state, then calls `createAdapterBindings` to attach transports. The packaged Slack bridge passes `createSlackPinetRuntimeAdapterFactory(...)`, while tests use an in-memory non-Slack adapter to demonstrate the same boundary without Slack tokens or Slack-specific metadata. Adapter factories return `MessageAdapter` bindings; the core wires inbound delivery, registers adapters on the broker, and connects them.
330
+ Configure RALPH:
486
331
 
487
- ### Quick start
488
-
489
- **Broker** (one per mesh — coordinates routing and health):
490
-
491
- ```
492
- /pinet start
332
+ ```json
333
+ {
334
+ "slack-bridge": {
335
+ "ralphLoopIntervalMs": 120000,
336
+ "ralphSnoozeAfterEmptyCycles": 3,
337
+ "ralphSnoozeDurationMs": 1800000
338
+ }
339
+ }
493
340
  ```
494
341
 
495
- **Follower** (workers that connect to the broker):
496
-
497
- ```
498
- /pinet follow
499
- ```
342
+ ### Inbox and threading
500
343
 
501
- Or set `"runtimeMode": "follower"` in settings (or the legacy `"autoFollow": true`) to auto-connect when a broker is running.
344
+ Pinet maintains an inbox for each agent. Messages are:
502
345
 
503
- ### Broker prompt MD
346
+ - routed based on thread ownership
347
+ - queued when agents are busy
348
+ - marked read when processed
349
+ - preserved across restarts
504
350
 
505
- Broker coordination policy is loaded from Markdown. Configure `slack-bridge.brokerPrompt` to choose a packaged prompt preset such as `tmux` or to point at a custom Markdown file path. Relative paths resolve under the current repo/worktree root; `~/...` paths resolve under the user home directory. When no setting is present, the broker scans for the first valid prompt in this order:
351
+ Thread ownership ensures continuity. Once an agent owns a thread, it keeps receiving those messages.
506
352
 
507
- 1. workspace override: `.pi/slack-bridge/tmux.md` under the current repo/worktree root
508
- 2. user-local override: `~/.pi/agent/slack-bridge/tmux.md`
509
- 3. packaged default: `dist/prompts/broker/tmux.md`
353
+ ## Troubleshooting
510
354
 
511
- Invalid higher-priority files (unsafe symlink/path escape, unreadable file, oversized content, invalid UTF-8/binary-looking content, or empty file) emit a concise warning and fall through to lower-priority candidates. Warnings identify only the candidate kind and reason; prompt bodies and private paths are not echoed.
355
+ ### Socket Mode connection issues
512
356
 
513
- The packaged `tmux.md` captures the default fully autonomous / unchained broker operating policy: the broker coordinates and never implements, delegates to repo-scoped workers, starts fresh tmux-backed workers on the Mac mini for new repo-scoped tasks/lanes unless a maintainer explicitly asks for reuse, marks broker-launched followers with `PINET_BROKER_MANAGED=1 PINET_BROKER_AGENT_ID=<current-broker-agent-id> PINET_LAUNCH_SOURCE=broker-tmux PINET_TMUX_SESSION=<session>` so PID ownership is inspectable, records tmux session/socket and repo/worktree metadata in durable lane state, keeps completed workers available for a one-hour follow-up grace period, routes follow-up back to the same Pi instance when possible, asks grace-expired healthy idle broker-managed workers to exit only when inspectable Pinet signals or the worker confirm they are free, fails closed by reporting ambiguous cleanup candidates, prunes old broker-managed tmux capacity instead of recycling stale context into new lanes, observes RALPH loop maintenance expectations, handles Slack thread ownership/reporting caveats, and describes GitHub/secret handling without exposing secrets.
357
+ If you see 'WebSocket error' or connection failures:
514
358
 
515
- Only broker prompt content is replaceable. Broker runtime/tool restrictions remain code-owned and are appended after the loaded MD prompt, including the forbidden local `Agent` path and broker `edit`/`write` blocking. Followers keep append-only worker guidance and do not load broker prompt MD. Prompt changes are picked up on `/pinet start` / runtime restart; this slice does not hot-reload per turn.
359
+ 1. Check your app token is valid
360
+ 2. Verify Socket Mode is enabled in your Slack app
361
+ 3. Check network connectivity
362
+ 4. Look for rate limiting (Slack allows 10 connections per app)
516
363
 
517
- ### Multi-agent tools
364
+ ### Permission errors
518
365
 
519
- | Tool | Description |
520
- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
521
- | `pinet` | Pinet dispatcher with token-efficient `action`-based routing (`help`, `send`, `read`, `free`, `snooze`, `schedule`, `agents`, `lanes`, `ports`, `spawn`, `reload`, `exit`) |
366
+ If Pinet cannot perform actions:
522
367
 
523
- Use the dispatcher for Pinet tool actions: `pinet action=send`, `pinet action=read`, `pinet action=free`, `pinet action=snooze`, `pinet action=schedule`, `pinet action=agents`, `pinet action=lanes`, `pinet action=ports`, `pinet action=spawn`, `pinet action=reload`, and `pinet action=exit`. Use slash commands for UI lifecycle transitions: `/pinet start`, `/pinet follow`, `/pinet unfollow`, and `/pinet subtree start`. Dedicated direct Pinet tools (`pinet_message`, `pinet_read`, `pinet_agents`, `pinet_free`, `pinet_schedule`) are no longer registered. Legacy `pinet_*` guardrail patterns still match dispatcher action names, and legacy send policies such as `pinet_send` or `pinet_message` also cover `pinet action=send`, so existing security configs fail closed during migration.
368
+ 1. Check the bot is in the channel (invite with `/invite @pinet`)
369
+ 2. Verify bot scopes match the manifest
370
+ 3. Reinstall the app to update permissions
371
+ 4. Check `allowedUsers` includes the right user IDs
524
372
 
525
- Worker-owned subtree brokers let a follower worker supervise its own child mesh without registering those children in the central broker. Run `/pinet subtree start` (alias: `/pinet subbroker start`) from a follower worker. The worker remains connected to the central broker as a normal worker, and it also starts a separate broker socket/database under `~/.pi/pinet-subtrees/<worker>/`. Child workers launched by this worker receive `PINET_SOCKET_PATH`, `PINET_PARENT_AGENT_ID`, `PINET_ROOT_AGENT_ID`, `PINET_LAUNCH_ID`, `PINET_SUBTREE_ROLE`, and related metadata, so they follow the worker's subtree broker instead of the central Pinet broker.
373
+ ### Messages not received
526
374
 
527
- Use `pinet action=spawn args.repo=<repo> args.task=<task> [args.role=<role>] [args.lane_id=<lane>]` or `/pinet subtree spawn repo=<repo> [role=<role>] [lane=<lane>] <task>` to launch a tmux-backed child worker, wait for it to register in the subtree broker, and deliver the task over private Pinet A2A. Use `pinet action=agents args.scope=subtree args.full=true` from the supervising worker to list subtree children, `pinet action=send args.to=<child> args.message=<message>` to reply/control them, `pinet action=read` to read child reports, and `pinet action=exit args.target=<child>` or `/pinet subtree stop` to clean them up. The central broker sees only the supervising worker; the subtree DB contains the child roster and messages.
375
+ If Pinet does not respond:
528
376
 
529
- Dispatcher content defaults to terse CLI-style confirmations/summaries for noisy reads, sends, and agent lists. Bulky read/agent payloads are compacted in `data.details` by default, including when `args.format="json"` (or `args.f` / `args["-f"]`) renders the dispatcher envelope in content. Use `args.full=true` / `args["--full"]=true` only when you need verbose text and full structured debug details such as exact message bodies or agent metadata.
377
+ 1. Check Socket Mode shows 'Connected' in Slack app settings
378
+ 2. Verify event subscriptions are enabled
379
+ 3. Check `allowedUsers` or `allowAllWorkspaceUsers`
380
+ 4. Look in the log channel for errors
381
+ 5. Try `/pinet status` to check if Pinet is running
530
382
 
531
- Durable Pinet inbox notifications are classified as `steering`, `fwup`, or `maintenance/context` from explicit metadata or message cues. Follower prompts receive compact pointers such as `pinet action=read args.thread_id=...` instead of the full durable message body; agents use `pinet action=read` to retrieve the actual context. Delivery, read/ack state, and mail classification remain separate.
383
+ ### Stalled agents
532
384
 
533
- Scheduled Pinet wake-ups use the same durable read surface: due wake-ups are persisted/stamped as Pinet follow-up mail and surfaced through compact `pinet action=read` pointers rather than direct reminder-body prompts. Wake-up bodies and metadata are treated as mail content only; they do not trigger Pinet remote-control commands such as `/exit`, `/reload`, or structured `pinet:control` JSON.
385
+ If work gets stuck:
534
386
 
535
- Durable lane metadata is stored in SQLite and can be inspected/updated with `pinet action=lanes`. PM-mode lanes can record the accountable follower/PM, implementation lead, participant roles (`pm`, `lead`, `implementer`, `reviewer`, `second_pass_reviewer`, etc.), linked issue/PR, state, and summary. The `detached` lane state means a lane is manually supervised by a human; broker/RALPH/status surfaces keep it visible but should not treat it as normal auto-reassignment work without explicit human/broker action.
387
+ 1. Check `/pinet status` for current state.
388
+ 2. Check `/pinet logs` for repeated failures.
389
+ 3. Wait for RALPH to run automatically.
390
+ 4. Reduce `ralphLoopIntervalMs` for faster recovery if needed.
536
391
 
537
- Durable local port leases are stored in SQLite and can be managed with `pinet action=ports`. Use `op=acquire` with `purpose` and `ttl_ms` to reserve either a requested `port` (for example `3000`) or the first free port in `min_port..max_port` (default `49152..65535`, host default `127.0.0.1`). Use `op=renew` with `lease_id` and `ttl_ms`, `op=release`, `op=status`, `op=list`, or `op=expire`. Follower RPC access is scoped to the caller-owned leases; broker-local maintenance can still expire all stale leases. Active leases are unique by `(host, port)`; broker maintenance expires stale leases conservatively, and process-kill behavior should be layered on explicit cleanup hooks rather than hidden in lease acquisition.
392
+ ## Package information
538
393
 
539
- Broker-mode ghost cleanup is deliberately conservative. The broker stores follower PIDs in the agent registry, but it only sends real process signals for ghosts that registered with broker-managed launch metadata (`PINET_BROKER_MANAGED=1`) and still verify as Pi follower processes. Reaping sends `SIGTERM` first and schedules a bounded `SIGKILL` only if the same verified broker-managed process remains; unmarked or mismatched PIDs are never killed.
394
+ ### Publishing metadata
540
395
 
541
- RALPH snooze quiets non-urgent empty maintenance cycles without disabling human-triggered routing. Use `/pinet snooze 30m no work available` or `pinet action=snooze args.op=set args.duration=30m` to quiet the broker manually, `/pinet snooze off` or `op=clear` to wake it, and `/pinet status` / Home tab to inspect snooze state. An empty cycle means no active live workers, no active tracked assignments, no visible RALPH anomalies, no pending backlog, no assigned backlog from broker maintenance, no maintenance anomalies, no pending task-assignment report, and no tracked task progress change. If active work, anomalies, or task progress appears during snooze, RALPH wakes and reports normally. Auto-snooze is opt-in via `ralphSnoozeAfterEmptyCycles`; the default is disabled.
396
+ The package declares pi metadata in [`package.json`](./package.json):
542
397
 
543
- ### Pinet command surface
398
+ - `keywords` includes `pi-package` for gallery discovery
399
+ - `pi.extensions` points to `./dist/index.js`
400
+ - `pi.skills` points to bundled skills
401
+ - No preview assets yet
544
402
 
545
- Use `/pinet <action> [args]` for mesh lifecycle and broker operations. In the Slack web app, use `/<app> agents list [all]` for the Slack-native broker roster/current-work view: `/pinet agents list` for the Pinet app, or `/oathgate agents list` for an Oathgate-named app. Set `slackCommandName` (or `slackCommandNames`) in `slack-bridge` settings before deploying the manifest when the Slack command should match a non-default app name.
403
+ Check the package contents:
546
404
 
547
- | Command | Description |
548
- | ------------------------------------------ | ----------------------------------------------------------------------------- |
549
- | `/pinet start` | Start as the mesh broker |
550
- | `/pinet follow` | Connect as a follower worker |
551
- | `/pinet unfollow` | Disconnect from the broker |
552
- | `/pinet reload <agent>` | Ask another agent to reload |
553
- | `/pinet exit <agent>` | Ask another agent to exit |
554
- | `/pinet free` | Mark this agent as idle |
555
- | `/pinet snooze [duration/off/status]` | Quiet empty RALPH cycles while preserving human-triggered wake/route behavior |
556
- | `/pinet subtree [start/status/spawn/stop]` | Run this worker as a local subtree broker for child followers |
405
+ ```bash
406
+ cd slack-bridge
407
+ npm pack --dry-run
408
+ ```
557
409
 
558
- ### Pinet skins
410
+ ### Development
559
411
 
560
- Pinet skin selection is configuration-driven. Set `skinTheme` under the `slack-bridge` settings object (for example, `"skinTheme": "foundation"`) and restart/reload the broker; broker startup applies the configured presentation to broker and follower registrations. Skin selection updates mesh presentation only: names, emoji palette, persona/tone guidance, and optional display vocabulary for statuses. Core roles and states stay skin-neutral (`broker`, `worker`, `idle`, `working`, routing, repo, and guardrails are not redefined by skins).
412
+ Build the package:
561
413
 
562
- Built-in skins:
414
+ ```bash
415
+ cd slack-bridge
416
+ pnpm build
417
+ ```
563
418
 
564
- - `default` / `classic` — preserves the current whimsical animal names, animal emoji palette, and playful-but-focused persona.
565
- - `foundation` / `foundation/space` / `space` — JSON descriptor with curated institutional sci-fi characters, full-name aliases, and archive, relay, frontier, and crisis-room flavor.
566
- - `cosmere` / `cosmere-inspired` / `oathgate` — JSON descriptor with curated/prebaked 1–3 word identities, static emoji, and whimsical Mistborn/Stormlight/Emberdark-inspired agents, spren, artifacts, places, and jokes while avoiding exact third-party character names.
419
+ Run tests:
567
420
 
568
- Free-form themes are still accepted as deterministic legacy/custom presentation themes. Shipped non-default skins live in `skins/*.json`; use the bundled `pinet-skin-creator` skill to author and review curated character/name/persona/status-vocabulary pools before adding runtime descriptors.
421
+ ```bash
422
+ pnpm test
423
+ ```
569
424
 
570
- ### How it works
425
+ Deploy the Slack manifest:
571
426
 
572
- - The **broker** runs Slack Socket Mode, routes messages to agents, and monitors health via the RALPH loop. The loop defaults to every 5 minutes and can be configured with `ralphLoopIntervalMs` under `slack-bridge` settings.
573
- - **Followers** connect to the broker over a local Unix socket, poll for work, and report results
574
- - Agents can optionally authenticate using a shared local secret (`meshSecret` or `meshSecretPath`); when both are unset, mesh auth is disabled
575
- - Thread ownership is first-responder-wins — the first agent to reply claims the thread
427
+ ```bash
428
+ pnpm deploy:slack
429
+ ```
576
430
 
577
431
  ## Security
578
432
 
579
- - **User access**: Slack access is default-deny. Set `allowedUsers` for a narrow allowlist, or `allowAllWorkspaceUsers: true` only if you explicitly want workspace-wide access
580
- - **Tool guardrails**: `security.readOnly`, `security.requireConfirmation`, and `security.blockedTools` are runtime-enforced for Slack-triggered turns, including core tools such as `bash`, `edit`, and `write`
581
- - **Guardrail posture**: If Slack/Pinet access is enabled for admitted users and `security.readOnly`, `security.blockedTools`, and `security.requireConfirmation` are all effectively empty (`readOnly !== true` and both arrays are absent or empty), the bridge emits a startup/runtime warning and `/pinet status` shows `Guardrails: empty (warn-first posture; behavior unchanged)`. This is visibility-only: it does **not** auto-enable `readOnly`, block startup, or require an acknowledgement flow.
582
- - **Mesh authentication**: Optional. Configure `meshSecret` or `meshSecretPath` (or `PINET_MESH_SECRET` / `PINET_MESH_SECRET_PATH`) to require a shared secret; leave them unset to disable shared-secret auth. Configured followers fail closed on missing secret files or older/no-auth brokers rather than silently downgrading.
583
-
584
- Find Slack user IDs: click a user's profile → **More** → **Copy member ID**.
433
+ ### Default-deny access
585
434
 
586
- ---
435
+ Pinet requires explicit configuration to allow users. Without `allowedUsers` or `allowAllWorkspaceUsers`, nobody can use it.
587
436
 
588
- ## Development
589
-
590
- ### Build
591
-
592
- ```bash
593
- pnpm run build
594
- ```
437
+ ### Token safety
595
438
 
596
- ### Lint / Typecheck / Test
439
+ - Never commit tokens to git
440
+ - Use environment variables in production
441
+ - Rotate tokens regularly
442
+ - Use separate apps for development and production
597
443
 
598
- ```bash
599
- pnpm lint
600
- pnpm typecheck
601
- pnpm test
602
- ```
444
+ ### Confirmation for dangerous actions
603
445
 
604
- ### Deploy manifest to Slack
446
+ Configure confirmation for sensitive operations:
605
447
 
606
- ```bash
607
- pnpm deploy:slack
448
+ ```json
449
+ {
450
+ "slack-bridge": {
451
+ "security": {
452
+ "requireConfirmation": ["slack:create_channel", "slack:upload", "slack:delete"]
453
+ }
454
+ }
455
+ }
608
456
  ```
609
457
 
610
- Requires `appId` and `appConfigToken` in settings (or `SLACK_APP_ID` / `SLACK_APP_CONFIG_TOKEN` env vars). The deploy path rewrites `features.slash_commands` from `slackCommandName` / `slackCommandNames` (or the configured `skinTheme`) before validating and uploading, so set `slackCommandName: "/oathgate"` for an Oathgate app and leave it unset for the packaged Pinet `/pinet` default.
458
+ ### Read-only mode
611
459
 
612
- ### Architecture
460
+ Prevent all modifications:
613
461
 
614
- - **Socket Mode** — outbound WebSocket, no public URL needed
615
- - **Zero runtime npm deps** — native `fetch`, `WebSocket`, `node:sqlite` (Node 22+)
616
- - **Hybrid inbox** — queue when busy, auto-drain when idle
617
- - **Reactions** — 👀 as a lightweight "thinking" indicator
618
- - **Thread persistence** — thread state survives `/reload`
462
+ ```json
463
+ {
464
+ "slack-bridge": {
465
+ "security": {
466
+ "readOnly": true
467
+ }
468
+ }
469
+ }
470
+ ```
619
471
 
620
- ## License
472
+ ## Support
621
473
 
622
- MIT. See [`LICENSE`](./LICENSE).
474
+ - [GitHub repository](https://github.com/gugu91/extensions)
475
+ - [Architecture documentation](../plans/)
476
+ - Check the log channel in Slack for runtime issues