hivecode-mcp 0.14.12 → 0.14.13

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.
@@ -175,9 +175,15 @@ short job on each event and every 10 minutes, with the token it gives the job.
175
175
  `npx -y hivecode-mcp@latest branch <branch>` then `git push origin <branch>`
176
176
  2. Add the room's link as a repository secret named `HIVE_ROOM`: Settings → Secrets and variables →
177
177
  Actions → New repository secret.
178
- 3. Add the workflow file. `npx -y hivecode-mcp@latest github action --workflow --branch <branch>`
179
- prints it; save it as `.github/workflows/hivecode.yml`.
180
- 4. The agents join the room. They can all run in the cloud.
178
+ 3. Allow GitHub Actions to create pull requests: Settings → Actions → General → Workflow permissions →
179
+ select "Allow GitHub Actions to create and approve pull requests" and save. Without this, GitHub
180
+ refuses the pull request (HTTP 403 / Q574) when the work lands.
181
+ 4. Add the workflow file. `npx -y hivecode-mcp@latest github action --workflow --branch <branch>`
182
+ detects your repository's package manager and lockfile (`package-lock.json`, `pnpm-lock.yaml`, or
183
+ `yarn.lock`) to insert the dependency installation step (`npm ci`, `pnpm install`, or `yarn install`),
184
+ then prints the workflow; save it as `.github/workflows/hivecode.yml`. For other package managers
185
+ or languages, add your dependency installation step before the Hivecode step.
186
+ 5. The agents join the room. They can all run in the cloud.
181
187
 
182
188
  ### What happens
183
189
 
@@ -190,6 +196,8 @@ short job on each event and every 10 minutes, with the token it gives the job.
190
196
  | The room fixes it | The next run pushes the fix to the same pull request. |
191
197
  | The pull request is merged or closed | The room is told, the label comes off, and the next labelled issue comes in. |
192
198
  | The issue is closed or unlabelled mid-work | The room is told to stop. Labelling it again sends it again, as a new attempt. |
199
+ | Tests fail during landing due to missing dependencies | The landing is refused (Q576); the issue gets a comment naming the fix (`Add an install step` in `.github/workflows/hivecode.yml`). Once the workflow file is updated on the default branch, the next run tries landing again. |
200
+ | The work is pushed to the branch, but GitHub refuses to open the PR (HTTP 403) | The issue is told the work is pushed to the branch, names the missing permission (Settings → Actions → General → Workflow permissions → "Allow GitHub Actions to create and approve pull requests"), and offers opening the PR manually. Once the setting is enabled, the next run opens the PR automatically without changes in the room. |
193
201
 
194
202
  ### No state file
195
203
 
@@ -1,195 +1,150 @@
1
- # Slack Setup (Under 10 Minutes)
1
+ # Slack Setup
2
2
 
3
- Hivecode posts to Slack using **one thread per task** (for a live room) or **one thread per GitHub issue** (for the GitHub watcher). Only the opening message and the final outcome appear in the main channel; intermediate claims, gate checks, CI runs, and untasked room chatter stay inside threads so your channel is never flooded.
3
+ Hivecode connects Slack and your swarm rooms so humans and agents collaborate where they already work.
4
+
5
+ There are two ways to connect:
6
+ 1. **Relay Connection (`slack connect`) — Recommended (2 minutes):** Zero processes running on anyone's machine. The relay securely routes events between Slack and the room.
7
+ 2. **Self-Hosted Bridge (`slack start`):** Runs a local bridge process on your machine using Slack Socket Mode (or HTTPS polling).
4
8
 
5
9
  ---
6
10
 
7
- ## 1. Create the Slack App from the Manifest (2 minutes)
11
+ ## 1. Fast Setup with `slack connect` (Under 2 Minutes, Zero Local Processes)
8
12
 
9
- 1. Open [api.slack.com/apps](https://api.slack.com/apps) and click **Create New App** → **From a manifest**.
10
- 2. Pick your Slack workspace and switch the tab to **JSON**.
11
- 3. Paste the contents of [`slack-app-manifest.json`](../slack-app-manifest.json):
13
+ With `slack connect`, you don't need to keep a terminal running or worry about laptops sleeping. The Hivecode relay handles event routing directly between Slack webhooks and room state.
12
14
 
13
- ```json
14
- {
15
- "display_information": {
16
- "name": "Hivecode",
17
- "description": "One Slack thread per task or GitHub issue for Hivecode rooms.",
18
- "background_color": "#111318"
19
- },
20
- "features": {
21
- "bot_user": {
22
- "display_name": "Hivecode",
23
- "always_online": true
24
- }
25
- },
26
- "oauth_config": {
27
- "scopes": {
28
- "bot": [
29
- "chat:write",
30
- "channels:read",
31
- "groups:read"
32
- ]
33
- }
34
- },
35
- "settings": {
36
- "org_deploy_enabled": false,
37
- "socket_mode_enabled": false,
38
- "token_rotation_enabled": false
39
- }
40
- }
15
+ ### Step 1: Run `slack connect` & Create the App from the Manifest
16
+ Run the connect command with your room link:
17
+ ```bash
18
+ npx -y hivecode-mcp@latest slack connect "wss://livecode-xoss.onrender.com|room-<id>"
41
19
  ```
20
+ *(On Windows PowerShell, quote the room link as shown).*
42
21
 
43
- 4. Click **Next** → **Create**, then click **Install to Workspace** and **Allow**.
44
- 5. In the left sidebar under **OAuth & Permissions**, copy the **Bot User OAuth Token** (`xoxb-…`). (After inviting `@Hivecode` to your channel in Step 2, run `SLACK_BOT_TOKEN=xoxb-… npx -y hivecode-mcp@latest slack check --channel C0123456789` to verify the app end to end.)
45
-
46
- ---
47
-
48
- ## 2. Invite `@Hivecode` to Your Channel & Copy the Channel ID (1 minute)
49
-
50
- 1. In Slack, open the channel where you want threads posted and invite the bot:
51
- ```text
52
- /invite @Hivecode
53
- ```
54
- 2. Right-click the channel name (or click the channel header at the top) → **View channel details**.
55
- 3. Scroll to the bottom of the dialog and copy the **Channel ID** (looks like `C0123456789`).
56
- 4. Run **`slack check`** to prove your token, workspace, channel membership, scopes, and (when `SLACK_APP_TOKEN` is set) inbound Socket Mode event subscriptions work end to end before starting any workflow:
57
- ```bash
58
- SLACK_BOT_TOKEN=xoxb-… [SLACK_APP_TOKEN=xapp-…] npx -y hivecode-mcp@latest slack check --channel C0123456789
59
- ```
60
- *(This posts one message in `#your-channel` — `"Hivecode check: reply anything in this thread"` — and one reply in its thread once your reply arrives.)*
22
+ The CLI contacts the relay (`POST /slack/prepare`), generates a dedicated connection ID and event URL (`https://<relay>/slack/events/<id>`), and outputs a ready-to-paste Slack application manifest:
23
+ 1. Open [api.slack.com/apps](https://api.slack.com/apps) and click **Create New App** → **From a manifest**.
24
+ 2. Select your Slack workspace and switch to the **JSON** tab.
25
+ 3. Paste the JSON manifest printed in your terminal and click **Next** → **Create**.
26
+ 4. Click **Install to Workspace** and authorize the bot.
27
+
28
+ The manifest configures:
29
+ - **Bot Scopes**: `chat:write`, `channels:read`, `groups:read`, `channels:history`, `groups:history`, `users:read`.
30
+ - **Event Subscriptions**: `message.channels`, `message.groups` pointing directly to your relay's `/slack/events/<id>` URL.
31
+ - **No Socket Mode required** (no `xapp-…` token needed).
32
+
33
+ ### Step 2: Invite `@Hivecode` to Your Channel
34
+ In Slack, open the channel you want to bridge and invite the newly created bot:
35
+ ```text
36
+ /invite @Hivecode
37
+ ```
61
38
 
62
- ---
39
+ ### Step 3: Enter Tokens & Select Channel
40
+ Now return to your terminal. The CLI securely prompts for your credentials (input is hidden with no terminal echo):
41
+ 1. **Bot User OAuth Token (`xoxb-…`)**: In Slack app settings under **OAuth & Permissions**, copy the Bot User OAuth Token.
42
+ 2. **Signing Secret**: Under **Basic Information → App Credentials**, copy the Signing Secret.
43
+ *(Alternatively, you can provide them via environment variables `SLACK_BOT_TOKEN` and `SLACK_SIGNING_SECRET`).*
63
44
 
64
- ## 3. Run `slack check` First, Then Start
45
+ 3. **Choose Channel**:
46
+ The CLI queries the channels the bot belongs to. Since you invited the bot in Step 2, your channel appears immediately. If running in an interactive terminal, it lists them for you to pick. In non-interactive environments, pass `--channel <id>` (or if the bot is in only one channel, that channel is selected automatically).
65
47
 
66
- Before starting a room bridge or the GitHub watcher, run **`slack check`** once. It verifies each piece of your Slack app in order (`token`, `workspace`, `channel`, `scopes`, `Socket Mode`), then posts `"Hivecode check: reply anything in this thread"` in `#your-channel` and waits up to 2 minutes for your reply so you know outbound posting **and** inbound event subscriptions actually work end to end.
48
+ ### Step 4: Done!
49
+ The relay verifies the bot token with `auth.test`, validates channel membership, stores the credentials encrypted (`AES-256-GCM` using `HIVE_SLACK_KEY`), and posts:
50
+ `✓ Slack is connected to this room (#channel)` in both the Slack channel and the room.
67
51
 
68
- **macOS / Linux (bash, zsh):**
52
+ ### Disconnecting
53
+ To remove the connection and wipe the stored credentials from the relay:
69
54
  ```bash
70
- # Outbound only (3-scope manifest):
71
- SLACK_BOT_TOKEN=xoxb-… npx -y hivecode-mcp@latest slack check --channel C0123456789
72
-
73
- # Outbound + inbound stop/retry/replies (Socket Mode manifest):
74
- SLACK_BOT_TOKEN=xoxb-… SLACK_APP_TOKEN=xapp-… npx -y hivecode-mcp@latest slack check --channel C0123456789
75
-
76
- # Slack AI Agent surface + native stop button (Paid Slack plan manifest):
77
- SLACK_BOT_TOKEN=xoxb-… SLACK_APP_TOKEN=xapp-… npx -y hivecode-mcp@latest slack check --channel D0123456789 --agent
55
+ npx -y hivecode-mcp@latest slack disconnect "wss://livecode-xoss.onrender.com|room-<id>"
78
56
  ```
57
+ Only the holder of the same `SLACK_SIGNING_SECRET` can disconnect the room.
79
58
 
80
- **Windows (PowerShell):**
81
- ```powershell
82
- $env:SLACK_BOT_TOKEN="xoxb-…"
83
- $env:SLACK_APP_TOKEN="xapp-…" # optional: include when using inbound stop/retry/replies or --agent
84
- npx -y hivecode-mcp@latest slack check --channel C0123456789
85
- ```
59
+ ---
60
+
61
+ ## 2. What Gets Posted (and What Is Scrubbed)
86
62
 
87
- **What Each `slack check` Line Means & the One Fix When It Fails:**
63
+ Hivecode posts room activity into Slack and forwards Slack messages into the room:
88
64
 
89
- | Failing Step / Line | Cause | The One Fix |
65
+ | Source | Posting Model | Description |
90
66
  | :--- | :--- | :--- |
91
- | **`Q630 SLACK_BOT_TOKEN is missing` / `rejected by Slack (invalid_auth)`** | `SLACK_BOT_TOKEN` is unset, pasted from the wrong field, or revoked. | Copy the **Bot User OAuth Token** (`xoxb-…`) from **OAuth & Permissions** in your Slack app settings and export `SLACK_BOT_TOKEN` in the same terminal. |
92
- | **`Q634 Slack auth.test did not return a workspace team_id`** | Slack did not associate the token with a workspace. | Reinstall the app to your workspace under **OAuth & Permissions → Reinstall to Workspace**. |
93
- | **`Q631 @Hivecode is not a member of #channel`** | The bot has not been invited to the target channel. | Open `#channel` in Slack and run `/invite @Hivecode`. |
94
- | **`Q631 Slack channel … could not be accessed (missing_scope: needs groups:read)`** | Target channel is private, or the bot lacks the channel read scope (`needed` scope named in error). | Add the named scope (`groups:read` / `channels:read`) under **OAuth & Permissions → Bot Token Scopes** and click **Reinstall to Workspace**. |
95
- | **`Q634 SLACK_BOT_TOKEN is missing required inbound scope(s): users:read, channels:history`** | `SLACK_APP_TOKEN` is set for inbound `stop`/`retry`, but the app was created from the outbound-only manifest. | Recreate or update the app's manifest from [`slack-app-manifest-inbound.json`](../slack-app-manifest-inbound.json) (Section 5 below) and click **Reinstall to Workspace**. |
96
- | **`Q634 SLACK_BOT_TOKEN is missing required agent scope(s): assistant:write, im:history`** | `--agent` (or `hive-slack-agent.js`) was started with a token from the outbound or inbound-only manifest. | Update the app's manifest from [`slack-app-manifest-agent.json`](../slack-app-manifest-agent.json) (Section 5 below) and click **Reinstall to Workspace**. |
97
- | **`Q634 SLACK_APP_TOKEN was rejected` / `Socket Mode did not open (no hello frame)`** | Socket Mode is disabled in the Slack app, or `SLACK_APP_TOKEN` (`xapp-…`) lacks `connections:write`. | Enable **Socket Mode** in your Slack app settings and generate a new App-Level Token (`xapp-…`) with `connections:write` under **Basic Information → App-Level Tokens**. |
98
- | **`Q634 Slack agents.sessions.setStatus returned warning missing_agent_session_stopped_event_subscription`** | `--agent` checked `agents.sessions.setStatus`, and Slack reported that `agent_session_stopped` is not subscribed (so the native stop button is not rendered). | Subscribe to `agent_session_stopped` by updating your app's manifest from [`slack-app-manifest-agent.json`](../slack-app-manifest-agent.json) and reinstalling. |
99
- | **`Q634 Slack agents.sessions.setStatus failed (feature_disabled)`** | The workspace is on a Free Slack plan or Agents & Assistants is disabled for the workspace. | Use [`slack-app-manifest-inbound.json`](../slack-app-manifest-inbound.json) for typed `stop` / `retry` in channel threads (works on all Slack plans), or enable Agents & Assistants on a paid Slack plan. |
100
- | **`Q634 Slack agents.sessions.setStatus failed (thread_ts_required)`** | Slack rejected `agents.sessions.setStatus` because the DM session requires a valid `thread_ts`. | Run `slack check --channel <D…> --agent` on a DM or channel where `@Hivecode` can post a thread root message first. |
101
- | **`Q634 replies did not arrive. Event subscriptions are probably missing`** | You replied in the check thread, but Slack never sent the event (`message.channels` / `message.groups` / `message.im` not subscribed). | Update your app's manifest with [`slack-app-manifest-inbound.json`](../slack-app-manifest-inbound.json) (or [`slack-app-manifest-agent.json`](../slack-app-manifest-agent.json) for `--agent`) and reinstall the app. |
102
- | **`Q635 Another process is already connected to Slack Socket Mode`** | Another running terminal or service is connected with the same `SLACK_APP_TOKEN` (e.g. `slack start`, `github start --slack-allow`, or `hive-slack-agent`). | Use a separate Slack app per process that reads Slack. If you recently stopped a bridge or watcher, wait a minute for Slack to release the socket and retry. |
103
-
104
- Once `slack check` passes, you can also run `--dry-run` on `slack start` or `github start` before launching:
105
-
106
- ### Option A — Bridge a Live Hivecode Room (`slack start`)
107
-
108
- > **Where do I get the `"wss://…|room-…"` link?** Running `npx -y hivecode-mcp@latest branch <name>` or `npx -y hivecode-mcp@latest setup` prints your room's `wss://…|room-…` link.
109
-
110
- **macOS / Linux (bash, zsh):**
111
- ```bash
112
- # 1. Verify token and channel membership first
113
- SLACK_BOT_TOKEN=xoxb-… npx -y hivecode-mcp@latest slack start "wss://livecode-xoss.onrender.com|room-abc123" --channel C0123456789 --dry-run
67
+ | **Relay Connection (`slack connect`)** | Direct channel messages | Each room chat line is posted directly into the channel with `chat.postMessage`, and messages posted in Slack are added to room chat. |
68
+ | **Room Bridge (`slack start`)** | One thread per task | Dedicated threads for tasks (`task-open` and `task-done` broadcast to channel), plus a shared room thread for untasked chatter. |
69
+ | **GitHub Watcher (`--slack-channel`)** | One thread per issue | Issue threads with broadcast replies for PR ready, waiting on human, and merged/closed. |
114
70
 
115
- # 2. Start the live bridge
116
- SLACK_BOT_TOKEN=xoxb-… npx -y hivecode-mcp@latest slack start "wss://livecode-xoss.onrender.com|room-abc123" --channel C0123456789
117
- ```
71
+ - **Security & Scrubbing**: Before lines leave the relay, API keys and tokens are scrubbed, and the room ID is replaced with `[room]`.
72
+ - **Deduplication & Deadlines**: The relay acknowledges Slack event webhooks (`HTTP 200`) immediately to satisfy Slack's 3-second deadline, deduplicates `X-Slack-Retry-Num` and `event_id`, and avoids echoing bot or Slack-origin messages.
73
+ - **Attribution**: Messages originating from Slack are attributed in room chat as `slack:<handle>` (e.g. `slack:ganesh`), preventing impersonation of local room participants.
118
74
 
119
- **Windows (PowerShell):**
120
- ```powershell
121
- $env:SLACK_BOT_TOKEN="xoxb-…"
122
- npx -y hivecode-mcp@latest slack start "wss://livecode-xoss.onrender.com|room-abc123" --channel C0123456789 --dry-run
123
- npx -y hivecode-mcp@latest slack start "wss://livecode-xoss.onrender.com|room-abc123" --channel C0123456789
124
- ```
75
+ ---
125
76
 
126
- **Windows (Command Prompt / `cmd.exe`):**
127
- ```cmd
128
- set SLACK_BOT_TOKEN=xoxb-…
129
- npx -y hivecode-mcp@latest slack start "wss://livecode-xoss.onrender.com|room-abc123" --channel C0123456789 --dry-run
130
- npx -y hivecode-mcp@latest slack start "wss://livecode-xoss.onrender.com|room-abc123" --channel C0123456789
131
- ```
77
+ ## 3. Controlling Agents from Slack
78
+
79
+ Full workspace members can control agents directly from Slack:
80
+ - **`stop` / `pause` / `halt`** (or `stop all`, `stop <AgentName>`): Pauses agents on their clients immediately.
81
+ - **`resume` / `unpause`** (or `resume all`, `resume <AgentName>`): Resumes paused agents.
132
82
 
133
83
  ---
134
84
 
135
- ### Option B — One Slack Thread per GitHub Issue (`github start --slack-channel`)
85
+ ## 4. Self-Hosted Fallback (`slack start`)
136
86
 
137
- When running the GitHub issue watcher (`docs/GITHUB_WORKFLOW.md`), pass `--slack-channel <id>` with `SLACK_BOT_TOKEN` set in your environment. Each labelled issue opens one Slack thread; PR ready for review, waiting on a person, and merged/closed are posted in the thread and broadcast to the channel (`reply_broadcast`), while run start, CI failures, the tester's verdict, fixes pushed to the PR, resumed, and stopped stay inside the thread only.
87
+ If you run your own private relay or prefer running a dedicated local bridge process on your machine, use `slack start`:
138
88
 
139
- If you also set `SLACK_APP_TOKEN=xapp-…` (see Section 5), replying `stop` in an issue's Slack thread stops that issue's run (and posts `/hivecode stop` on GitHub), and replying `retry` re-runs it for Slack user IDs listed in `--slack-allow <U123,U456>`.
89
+ ### Setup Steps:
90
+ 1. Create a Slack app using [`slack-app-manifest-inbound.json`](../slack-app-manifest-inbound.json) with Socket Mode enabled.
91
+ 2. Under **Basic Information → App-Level Tokens**, create an app token with `connections:write` scope (`SLACK_APP_TOKEN=xapp-…`).
92
+ 3. Under **OAuth & Permissions**, copy `SLACK_BOT_TOKEN=xoxb-…`.
93
+ 4. Invite `@Hivecode` to your channel (`/invite @Hivecode`).
94
+ 5. Run the bridge:
140
95
 
141
- **macOS / Linux (bash, zsh):**
96
+ **macOS / Linux:**
142
97
  ```bash
143
- # 1. Dry-run first (checks gh auth, repo, and Slack token/channel)
144
- SLACK_BOT_TOKEN=xoxb-… npx -y hivecode-mcp@latest github start --agent "claude -p --output-format json --allowedTools Bash Edit Write Read Glob Grep mcp__hivecode" --slack-channel C0123456789 --dry-run
145
-
146
- # 2. Start watching issues
147
- SLACK_BOT_TOKEN=xoxb-… npx -y hivecode-mcp@latest github start --agent "claude -p --output-format json --allowedTools Bash Edit Write Read Glob Grep mcp__hivecode" --slack-channel C0123456789
98
+ SLACK_BOT_TOKEN=xoxb-… SLACK_APP_TOKEN=xapp-… npx -y hivecode-mcp@latest slack start "wss://relay|room-…" --channel C0123456789
148
99
  ```
149
100
 
150
101
  **Windows (PowerShell):**
151
102
  ```powershell
152
103
  $env:SLACK_BOT_TOKEN="xoxb-…"
153
- npx -y hivecode-mcp@latest github start --agent 'claude -p --output-format json --allowedTools Bash Edit Write Read Glob Grep mcp__hivecode' --slack-channel C0123456789 --dry-run
154
- npx -y hivecode-mcp@latest github start --agent 'claude -p --output-format json --allowedTools Bash Edit Write Read Glob Grep mcp__hivecode' --slack-channel C0123456789
104
+ $env:SLACK_APP_TOKEN="xapp-…"
105
+ npx -y hivecode-mcp@latest slack start "wss://relay|room-…" --channel C0123456789
155
106
  ```
156
107
 
157
- **Windows (Command Prompt / `cmd.exe`):**
158
- ```cmd
159
- set SLACK_BOT_TOKEN=xoxb-…
160
- npx -y hivecode-mcp@latest github start --agent "claude -p --output-format json --allowedTools Bash Edit Write Read Glob Grep mcp__hivecode" --slack-channel C0123456789 --dry-run
161
- npx -y hivecode-mcp@latest github start --agent "claude -p --output-format json --allowedTools Bash Edit Write Read Glob Grep mcp__hivecode" --slack-channel C0123456789
108
+ ### Preflight Check (`slack check`)
109
+ You can test your tokens, channel permissions, and Socket Mode connectivity anytime:
110
+ ```bash
111
+ SLACK_BOT_TOKEN=xoxb-… npx -y hivecode-mcp@latest slack check --channel C0123456789
162
112
  ```
163
113
 
164
- ---
165
-
166
- ## 4. What Gets Posted (and What Is Scrubbed)
167
-
168
- | Source | Shown in Channel | Inside the Thread Only |
169
- | :--- | :--- | :--- |
170
- | **GitHub Watcher (`--slack-channel`)** | Thread opening post (`#12 — Title`), plus thread replies broadcast to the channel (`reply_broadcast`): PR ready for review, waiting on a person (`hivecode:needs-human`), merged / closed | Run started, CI failed, the tester's verdict, resumed, stopped |
171
- | **Room Bridge (`slack start`)** | `task-open` and `task-done` | Gate pass/fail (`gate-pass`, `gate-fail`), task chat, and all untasked room chat (grouped inside one shared room thread so chatter never floods the channel) |
172
-
173
- - **What `slack start` announces, and when**: it watches the room's task list and gate results as well as its chat. A new task opens its thread (and appears in the channel); when the assignee completes it, the "done" reply is also broadcast to the channel. A gate pass or fail is posted inside the shared room thread. The first time a bridge ever attaches to a room it only records what is already there, so starting it never replays the room's history; what it has announced is kept in room state (`slack_seen`), so a restart does not repeat itself. A forced gate release is not posted as a gate result: it is already in room chat.
174
- - **Secret & Room Link Scrubbing**: Every message passes through `scrubSlack()` before leaving your machine. API keys, tokens, bearer headers, room capability links (`wss://…|room-…`), and room IDs (`room-…`) are stripped automatically.
175
- - **Rate Limits & Retries**: Slack's ~1 message/second per-channel pacing is enforced by a single outbound queue. HTTP `429` (`ratelimited`) responses are retried up to 5 times honouring `Retry-After`.
176
- - **Durable Thread State**: Thread timestamps (`thread_ts`) are stored in room state (`slack_threads` map) for `slack start` and in `.git/hivecode/github.json` for `github start --slack-channel`, so restarting the process continues posting into the existing Slack thread.
177
-
178
- ---
179
-
180
- ## 5. Stopping Agents & Replying from Slack (Optional — Socket Mode)
181
-
182
- By default, `slack-app-manifest.json` requests only the three outbound scopes (`chat:write`, `channels:read`, `groups:read`). If you also want to **stop/resume agents** or **reply in task threads** from Slack (with zero public webhook URL), enable **Socket Mode**:
183
-
184
- ### Privacy & Scope Notice
185
- Inbound Socket Mode requires adding `channels:history`, `groups:history`, and `users:read` to **Bot Token Scopes**, plus an **App-Level Token** (`SLACK_APP_TOKEN=xapp-…`) with `connections:write` and Event Subscriptions for `message.channels` and `message.groups`.
186
- > **Plain-spoken scope warning:** `channels:history` and `groups:history` let the bot read every message posted in the channels you invite it to. If you only want outbound thread feeds, keep the default 3-scope manifest above.
187
-
188
- <details>
189
- <summary><strong>Optional Inbound Manifest (Socket Mode enabled — <code>slack-app-manifest-inbound.json</code>)</strong></summary>
114
+ ### Self-Hosted Manifest Templates
190
115
 
191
- Paste the contents of [`slack-app-manifest-inbound.json`](../slack-app-manifest-inbound.json):
116
+ #### 1. Outbound Manifest (`slack-app-manifest.json`)
117
+ ```json
118
+ {
119
+ "display_information": {
120
+ "name": "Hivecode",
121
+ "description": "One Slack thread per task or GitHub issue for Hivecode rooms.",
122
+ "background_color": "#111318"
123
+ },
124
+ "features": {
125
+ "bot_user": {
126
+ "display_name": "Hivecode",
127
+ "always_online": true
128
+ }
129
+ },
130
+ "oauth_config": {
131
+ "scopes": {
132
+ "bot": [
133
+ "chat:write",
134
+ "channels:read",
135
+ "groups:read"
136
+ ]
137
+ }
138
+ },
139
+ "settings": {
140
+ "org_deploy_enabled": false,
141
+ "socket_mode_enabled": false,
142
+ "token_rotation_enabled": false
143
+ }
144
+ }
145
+ ```
192
146
 
147
+ #### 2. Inbound Socket Mode Manifest (`slack-app-manifest-inbound.json`)
193
148
  ```json
194
149
  {
195
150
  "display_information": {
@@ -228,77 +183,8 @@ Paste the contents of [`slack-app-manifest-inbound.json`](../slack-app-manifest-
228
183
  }
229
184
  }
230
185
  ```
231
- </details>
232
-
233
- ### Generating the App-Level Token (`SLACK_APP_TOKEN`)
234
- 1. In your Slack app settings, go to **Basic Information** and scroll down to **App-Level Tokens**.
235
- 2. Click **Generate Token and Scopes**.
236
- 3. Enter `socket-mode` as the **Token Name** (required by Slack), click **Add Scope**, select `connections:write`, and click **Generate**.
237
- 4. Copy the resulting `xapp-…` token and set it as `SLACK_APP_TOKEN`.
238
-
239
- ### Networks That Refuse WebSockets (Cloud VMs): `--inbound`
240
- Socket Mode is a WebSocket. Some networks refuse every WebSocket upgrade. One example is Antigravity's cloud VM, whose proxy answers `403 WebSocket upgrades are not allowed`. On such a network the bridge reads Slack over plain HTTPS instead: `conversations.history` for the channel and `conversations.replies` for the threads it tracks.
241
-
242
- | `--inbound` (or `HIVE_SLACK_INBOUND`) | What it does |
243
- | --- | --- |
244
- | `auto` (default) | Socket Mode when `SLACK_APP_TOKEN` is set. If the network refuses the upgrade with an HTTP status, it switches to HTTPS polling and prints `[hivecode] Slack replies are read over HTTPS every 3 s (...)`. Without `SLACK_APP_TOKEN`, it is outbound only, as before. |
245
- | `poll` | HTTPS polling only. **Needs no `SLACK_APP_TOKEN`**, just a bot token from [`slack-app-manifest-inbound.json`](../slack-app-manifest-inbound.json) (`channels:history` or `groups:history`, plus `users:read`). |
246
- | `socket` | Socket Mode only. Never polls. |
247
-
248
- ```bash
249
- SLACK_BOT_TOKEN=xoxb-… npx -y hivecode-mcp@latest slack check --channel C0123456789 --inbound poll
250
- SLACK_BOT_TOKEN=xoxb-… npx -y hivecode-mcp@latest slack start "<relay|room>" --channel C0123456789 --allow U123 --inbound poll
251
- ```
252
-
253
- What to know about polling:
254
- - **Speed:** a reply reaches the room within about `HIVE_SLACK_POLL_MS` (default 3000 ms). Socket Mode is near-instant.
255
- - **Restarts:** where it stopped reading is kept in the room and on disk, so a restarted bridge neither replays old messages nor skips new ones. Two bridges on one room claim each message before acting, so a `stop` is acted on once.
256
- - **Downtime:** after the bridge has been offline, messages older than `HIVE_SLACK_CATCHUP_MS` (default 30 minutes) are **not acted on**. A two-hour-old `resume` should not fire. The bridge posts once in the room's thread with the offline window and the count, so nobody assumes they were handled. Send it again if it still matters.
257
- - **Rate limits:** two reads per poll, more while tracked threads are active. A `429` is honoured with Slack's `Retry-After`.
258
- - `--agent` (the native stop button) needs Socket Mode and cannot poll.
259
-
260
- ### Strict or Secured Rooms: the Bridge Needs a Human Token
261
- `stop` and `resume` from Slack change the room's controls, and the relay lets only a human change controls: an AI's write is reverted. The bridge acts for a named Slack person, so it joins as a human. On an open room this works with no token. On a **strict or secured room**, start the bridge with a human (or admin/maintainer) token in the link or `HIVE_TOKEN`. Otherwise the relay reverts the stop, so check the agents' state in the room after your first `stop`.
262
-
263
- ### Who May Act (`--allow` / `--slack-allow`) & No Impersonation
264
- - **STOP is fail-safe**: Any full member of your Slack workspace may type `stop`, `stop all`, or `stop <AgentName>` in a thread to pause agents (or stop an active GitHub issue run) immediately. If Slack's `users.info` endpoint fails during an outage, `stop` still succeeds and notes `(stopped; I could not confirm who you are)`.
265
- - **RESUME, RETRY, and thread replies require the allowlist (`--allow U123,U456` on `slack start`, or `--slack-allow U123,U456` on `github start`)**: Only Slack user IDs explicitly listed in the allowlist (find yours in Slack under **Profile → `⋯` → Copy member ID**) may resume agents (`resume`, `resume all`, `resume <AgentName>`), retry a GitHub issue run (`retry`), or send replies into the room. If the allowlist is omitted, inbound is **stop-only**.
266
- - **Guests & Slack Connect users are refused**: Single-channel/multi-channel guests (`is_restricted` / `is_ultra_restricted`) and external Slack Connect users from other workspaces (`team_id` mismatch via `users.info`) are refused for all actions (including `stop`) and receive a refusal reply in the thread.
267
- - **One Slack app per process (Socket Mode multi-connection guard)**: Each process that reads Slack (`slack start`, `github start --slack-allow`, or `hive-slack-agent`) requires its own dedicated Slack app and App-Level Token (`xapp-…`). Slack Socket Mode supports up to 10 connections per token and arbitrarily distributes incoming events across them without channel routing. Sharing an app token across two processes causes events (including `stop all` and thread replies) to split between the two processes (`Q635`).
268
- - **No impersonation (`slack:<handle>`)**: Every message delivered from Slack into the room is attributed as `slack:<handle>` (for example, `slack:ganesh`), never as a bare room member name (`Ganesh`), so a Slack display name cannot impersonate the room owner.
269
-
270
- ### Free vs Paid Slack Plans & the Slack AI Agent Surface (`hive-slack-agent.js`)
271
- - **Typed `stop` works on every Slack plan (including Free)**: Replying `stop`, `stop all`, `stop <AgentName>`, `resume <AgentName>`, or `retry` in a channel thread requires only standard channel events (`slack-app-manifest-inbound.json`) and works on **all Slack plans**. We keep `slack-app-manifest-inbound.json` separate from `slack-app-manifest-agent.json` so Free-plan workspaces can install inbound `stop`/`retry` without hitting `feature_disabled`.
272
- - **Native AI stop button & streamed plan cards (`hive-slack-agent.js`) require a paid Slack plan**: `hive-slack-agent.js` renders live room tasks as a streamed plan (`chat.startStream` / `chat.appendStream` / `chat.stopStream` with `task_display_mode: "plan"`) and enables Slack's native stop button via `agents.sessions.setStatus` (`status: "processing"` / `"active"`) and `agent_session_stopped`.
273
- - **Who may act on the agent surface (same rules as `slack start`)**: `hive-slack-agent.js` checks every sender with the same `makeSlackActorCheck` rules. Any full workspace member may `stop` / `stop all` (or press Slack's native stop button); if Slack's `users.info` is down it still stops, and says `(stopped; I could not confirm who you are)`. `resume` needs the sender's member ID in `--allow <U123,U456>` (or `SLACK_ALLOW`). Guests and users from other workspaces are refused for everything, `status` included, and are told so in the thread; the room's tasks are never streamed to them. At startup it needs your workspace id from `auth.test` (refused with `Q634` if Slack omits it). A typed message counts only in the configured `--channel` or in a DM with the app (each user's own assistant DM is answered in that DM); messages from any other channel the bot is in are ignored.
274
- - **Official Slack requirements for `hive-slack-agent.js`**:
275
- - **Bot Token Scopes**: `chat:write` ([`chat.startStream`](https://docs.slack.dev/reference/methods/chat.startStream)), `assistant:write` ([`agents.sessions.setStatus`](https://docs.slack.dev/reference/methods/agents.sessions.setStatus) and [`agent_session_stopped`](https://docs.slack.dev/reference/events/agent_session_stopped)), `im:history`, `im:read`, `im:write` ([Developing AI Apps](https://docs.slack.dev/ai/developing-ai-apps)), plus `channels:read`, `groups:read`, `channels:history`, `groups:history`, `users:read`.
276
- - **Bot Events**: `agent_session_stopped` ([`agent_session_stopped` event](https://docs.slack.dev/reference/events/agent_session_stopped)), `assistant_thread_started`, `assistant_thread_context_changed`, `message.im`, `message.channels`, `message.groups`.
277
- - **App Settings**: `features.assistant_view` (`assistant_description` and `suggested_prompts`) and `settings.socket_mode_enabled: true` ([Agent Sessions](https://docs.slack.dev/ai/agent-sessions)).
278
- - **Startup & `slack check --agent` verification**:
279
- - At startup, `hive-slack-agent.js` (`preflightSlackAgent`) checks `x-oauth-scopes` from `auth.test` via `checkAgentScopes` and refuses with `Q634` (naming missing scopes such as `assistant:write, im:history` and `slack-app-manifest-agent.json`) before opening Socket Mode.
280
- - Running `SLACK_BOT_TOKEN=xoxb-… SLACK_APP_TOKEN=xapp-… npx -y hivecode-mcp@latest slack check --channel D0123456789 --agent` verifies those scopes, posts a check message (`checkTs`), and calls `agents.sessions.setStatus({ status: 'processing', channel_id, thread_ts: checkTs })`. Per Slack's [`agent_session_stopped` docs](https://docs.slack.dev/reference/events/agent_session_stopped), if your app is not subscribed to `agent_session_stopped`, Slack returns a `missing_agent_session_stopped_event_subscription` warning (checked in both top-level `warning` and `response_metadata.warnings[]`) and renders a non-interactive spinner instead of the stop button; `slack check --agent` catches that warning (as well as `feature_disabled` and `thread_ts_required`) and refuses with `Q634`. On every exit path (pass, fail, timeout, or `Ctrl+C`), `slack check --agent` clears the session status back to `active` in a `finally` block so the DM is never left spinning.
281
- - *(Note: `features.assistant_view` and `suggested_prompts` control the top-bar split-view UI in Slack and cannot be probed via a Web API read call; installing from `slack-app-manifest-agent.json` configures them declaratively.)*
282
-
283
- **Starting the Agent Surface (`slack agent`):**
284
-
285
- macOS / Linux (bash, zsh):
286
- ```bash
287
- SLACK_BOT_TOKEN=xoxb-… SLACK_APP_TOKEN=xapp-… npx -y hivecode-mcp@latest slack agent "wss://livecode-xoss.onrender.com|room-abc123" --channel D0123456789 --allow U012ABCDEF
288
- ```
289
-
290
- Windows (PowerShell):
291
- ```powershell
292
- $env:SLACK_BOT_TOKEN="xoxb-…"
293
- $env:SLACK_APP_TOKEN="xapp-…"
294
- npx -y hivecode-mcp@latest slack agent "wss://livecode-xoss.onrender.com|room-abc123" --channel D0123456789 --allow U012ABCDEF
295
- ```
296
-
297
- <details>
298
- <summary><strong>Optional Paid-Plan AI Agent Surface Manifest (<code>slack-app-manifest-agent.json</code>)</strong></summary>
299
-
300
- Paste the contents of [`slack-app-manifest-agent.json`](../slack-app-manifest-agent.json):
301
186
 
187
+ #### 3. Agent Surface Manifest (`slack-app-manifest-agent.json`)
302
188
  ```json
303
189
  {
304
190
  "display_information": {
@@ -362,113 +248,26 @@ Paste the contents of [`slack-app-manifest-agent.json`](../slack-app-manifest-ag
362
248
  }
363
249
  }
364
250
  ```
365
- </details>
366
-
367
- **macOS / Linux (bash, zsh):**
368
- ```bash
369
- SLACK_BOT_TOKEN=xoxb-… SLACK_APP_TOKEN=xapp-… npx -y hivecode-mcp@latest slack start "wss://livecode-xoss.onrender.com|room-abc123" --channel C0123456789 --allow U012ABCDEF --dry-run
370
- SLACK_BOT_TOKEN=xoxb-… SLACK_APP_TOKEN=xapp-… npx -y hivecode-mcp@latest slack start "wss://livecode-xoss.onrender.com|room-abc123" --channel C0123456789 --allow U012ABCDEF
371
- ```
372
-
373
- **Windows (PowerShell):**
374
- ```powershell
375
- $env:SLACK_BOT_TOKEN="xoxb-…"
376
- $env:SLACK_APP_TOKEN="xapp-…"
377
- npx -y hivecode-mcp@latest slack start "wss://livecode-xoss.onrender.com|room-abc123" --channel C0123456789 --allow U012ABCDEF --dry-run
378
- npx -y hivecode-mcp@latest slack start "wss://livecode-xoss.onrender.com|room-abc123" --channel C0123456789 --allow U012ABCDEF
379
- ```
380
-
381
- **Windows (Command Prompt / `cmd.exe`):**
382
- ```cmd
383
- set SLACK_BOT_TOKEN=xoxb-…
384
- set SLACK_APP_TOKEN=xapp-…
385
- npx -y hivecode-mcp@latest slack start "wss://livecode-xoss.onrender.com|room-abc123" --channel C0123456789 --allow U012ABCDEF --dry-run
386
- npx -y hivecode-mcp@latest slack start "wss://livecode-xoss.onrender.com|room-abc123" --channel C0123456789 --allow U012ABCDEF
387
- ```
388
-
389
- ### Slackbot Remote MCP Server (`hive-mcp-http.js`)
390
-
391
- If you connect Slackbot's remote MCP client over Streamable HTTP (`hive-mcp-http.js`), it exposes fast room inspection and control tools (`hive_status`, `hive_members`, `hive_tasks`, `hive_claims`, `hive_recent`, `hive_pause`, `hive_resume`, `hive_say`).
392
-
393
- - **Requires both `SLACK_SIGNING_SECRET` and `SLACK_BOT_TOKEN`**: In `--auth slack` mode (the default), `hive-mcp-http.js` refuses to start without `SLACK_SIGNING_SECRET` (`Q510`, copied from **Basic Information → App Credentials → Signing Secret**) **and** `SLACK_BOT_TOKEN` (`Q630`, `xoxb-…` with `users:read` scope so `users.info` can verify callers).
394
- - **Discovers your workspace (`team_id`) from Slack at startup**: When you start `hive-mcp-http.js` without `--team` (`SLACK_TEAM_ID`), it calls Slack's `auth.test` using `SLACK_BOT_TOKEN` before serving requests. If `auth.test` rejects the token (`Q630`) or does not return a `team_id` (`Q634`), startup is refused immediately so external Slack Connect workspace users can never slip through by default.
395
- - **Same access rules (`makeSlackActorCheck`)**:
396
- - **Every request is HMAC-verified first** (`X-Slack-Signature` + 5-minute replay window, `Q511`–`Q513`) before `_meta.slack.user_id` is read.
397
- - **Guests and other workspaces are refused** for all tools (`hive_pause`, `hive_resume`, `hive_say`).
398
- - **`hive_pause` (`stop`) is fail-safe** for any full member of your workspace (and fails open with `(stopped; I could not confirm who you are)` if `users.info` is unreachable during a Slack outage).
399
- - **`hive_resume` and `hive_say` require `--allow <U123,U456>`** (or `SLACK_ALLOW`). Messages sent via `hive_say` are recorded in room chat as `slack:<handle>`.
400
-
401
- **macOS / Linux (bash, zsh):**
402
- ```bash
403
- SLACK_SIGNING_SECRET=… SLACK_BOT_TOKEN=xoxb-… npx -y hivecode-mcp@latest slack mcp-server "wss://livecode-xoss.onrender.com|room-abc123" --port 8790 --allow U012ABCDEF
404
- ```
405
-
406
- **Windows (PowerShell):**
407
- ```powershell
408
- $env:SLACK_SIGNING_SECRET="…"
409
- $env:SLACK_BOT_TOKEN="xoxb-…"
410
- npx -y hivecode-mcp@latest slack mcp-server "wss://livecode-xoss.onrender.com|room-abc123" --port 8790 --allow U012ABCDEF
411
- ```
412
-
413
- **What a First-Time User Sees Step by Step (`hive-mcp-http.js`):**
414
- 1. **If `SLACK_SIGNING_SECRET` is missing**: Process exits immediately (`code 2`) with:
415
- `Q510 --auth slack needs SLACK_SIGNING_SECRET. Use --auth none only on localhost.`
416
- 2. **If `SLACK_BOT_TOKEN` is missing**: Process exits immediately (`code 2`) with:
417
- `Q630 --auth slack needs SLACK_BOT_TOKEN. Set SLACK_BOT_TOKEN=xoxb-… in your environment (docs/SLACK_SETUP.md).`
418
- 3. **When both are set**: `hive-mcp-http.js` calls Slack `auth.test`, locks in your workspace `team_id`, joins the room, and prints:
419
- `[hive-mcp-http] room room-abc123 on :8790/ auth=slack`
420
- 4. **When someone asks Slackbot what is happening (`hive_status`)**: Returns the live room name, agent count, paused list, and task counts.
421
- 5. **When a coworker triggers `hive_pause`**: Pauses the named agent (or `all`) on its own client (`paused AgentA — it will finish its current step and then stop picking up work`), while a guest or Slack Connect user from another workspace gets `:no_entry: Refused: guests and external Slack Connect users cannot control or message this room.`
422
- 6. **When someone not in `--allow` triggers `hive_resume` or `hive_say`**: Refused with `:no_entry: Refused: only Slack user IDs listed in --allow may resume/retry agents or send replies into the room (your ID: U…). Any full workspace member may still type stop.` When a user listed in `--allow` calls `hive_say`, it lands in the room attributed as `slack:<handle>`.
423
251
 
424
252
  ---
425
253
 
426
- ## 6. How to Show It (Live Demo Walkthrough)
427
-
428
- > **Running from a local checkout before npm publish:** Until the latest release is published to npm (`npx -y hivecode-mcp@latest` on npm is `0.12.4` prior to release), replace `npx -y hivecode-mcp@latest` in the commands below with `node C:/Users/G1/Desktop/N-team/hive-mcp.js` (or `node ./hive-mcp.js` from your repo root).
429
-
430
- Use these 5 steps to demonstrate either **GitHub-to-Slack issue threads** (`github start --slack-channel --slack-allow`) or a **live room bridge** (`slack start --channel --allow`) end to end:
431
-
432
- 1. **Install the App & Copy Tokens**:
433
- - Create the Slack app from the **Optional Inbound Manifest** in Section 5 above and install it to your workspace.
434
- - Copy `SLACK_BOT_TOKEN` (`xoxb-…`) from **OAuth & Permissions**, and generate `SLACK_APP_TOKEN` (`xapp-…`, named `socket-mode` with `connections:write`) under **Basic Information → App-Level Tokens**.
435
- 2. **Invite `@Hivecode` & Copy IDs**:
436
- - In your Slack channel, run `/invite @Hivecode` and copy the **Channel ID** (`C…`) from channel details.
437
- - Copy your own **Slack Member ID** (`U…`) from your Slack profile (**Profile → `⋯` → Copy member ID**).
438
- 3. **Start the Watcher or Room Bridge**:
439
- - **Flow A — GitHub Issue Threads + Slack `stop` / `retry` (PowerShell):**
440
- ```powershell
441
- $env:SLACK_BOT_TOKEN="xoxb-…"
442
- $env:SLACK_APP_TOKEN="xapp-…"
443
- npx -y hivecode-mcp@latest github start --agent 'claude -p --output-format json --allowedTools Bash Edit Write Read Glob Grep mcp__hivecode' --slack-channel C0123456789 --slack-allow U012ABCDEF --dry-run
444
- npx -y hivecode-mcp@latest github start --agent 'claude -p --output-format json --allowedTools Bash Edit Write Read Glob Grep mcp__hivecode' --slack-channel C0123456789 --slack-allow U012ABCDEF
445
- ```
446
- - **Flow B — Live Room Bridge + Slack `stop` / `resume` / Thread Replies (PowerShell):**
447
- ```powershell
448
- $env:SLACK_BOT_TOKEN="xoxb-…"
449
- $env:SLACK_APP_TOKEN="xapp-…"
450
- npx -y hivecode-mcp@latest slack start "wss://livecode-xoss.onrender.com|room-abc123" --channel C0123456789 --allow U012ABCDEF --dry-run
451
- npx -y hivecode-mcp@latest slack start "wss://livecode-xoss.onrender.com|room-abc123" --channel C0123456789 --allow U012ABCDEF
452
- ```
453
- 4. **Trigger a Thread**:
454
- - **In Flow A**: Add the `hivecode` label to a GitHub issue. Watch Slack open a single thread (`#12 — Title`) for that issue and post run progress inside it.
455
- - **In Flow B**: Assign a task in the room. Watch Slack open a thread for that task and post gate checks and task chat inside it.
456
- 5. **Control from the Slack Thread**:
457
- - **In Flow A (`github start`)**: Reply `stop` inside the issue's Slack thread to stop the active run (posts `/hivecode stop` on GitHub and ends the room). Reply `retry` (permitted because your ID is in `--slack-allow`) to post `/hivecode retry` and start a fresh run.
458
- - **In Flow B (`slack start`)**: Reply `stop all` (or `stop <AgentName>`) inside the thread to pause agents on their own clients (`controls` map -> `paused`). Reply `resume all` or reply with instructions (`@AgentName …`, delivered into the room as `slack:<your-handle>`).
459
-
460
- ---
254
+ ## 5. Troubleshooting Reference
461
255
 
462
- ## 7. Troubleshooting (`Q510` – `Q521`, `Q630` – `Q635`)
256
+ For complete error explanations and remedies, refer to [`docs/TROUBLESHOOTING.md`](TROUBLESHOOTING.md).
463
257
 
464
- | Code | Cause | Fix |
258
+ | Code | Component | Cause & Remedy |
465
259
  | :--- | :--- | :--- |
466
- | **`Q510`** | `hive-mcp-http.js` started in `--auth slack` mode without `SLACK_SIGNING_SECRET`. | Copy your app's **Signing Secret** from **Basic Information → App Credentials** in Slack app settings and set `SLACK_SIGNING_SECRET` (or use `--auth none` on `localhost` only). |
467
- | **`Q511` – `Q513`** | HTTP request to `hive-mcp-http.js` is unsigned (`Q511`), outside the 5-minute replay window (`Q512`), or its HMAC signature does not match (`Q513`). | Ensure `SLACK_SIGNING_SECRET` matches the Slack app calling the endpoint and that the host clock is synced via NTP. |
468
- | **`Q514` - `Q521`** | `hive-mcp-http.js` answered a request it could not act on: `Q514` no participant of that name is in the room; `Q515` unknown tool; `Q516` a tool failed; `Q517` JSON-RPC method not supported; `Q518` the endpoint takes `POST` only; `Q519` request body too large; `Q520` body is not JSON; `Q521` refused by the Slack actor check (guest, other workspace, or not in `--allow`). | `Q514`: use a name from the "Here now" list in the same message. `Q515`/`Q517`: call a tool listed above (`hive_status`, `hive_members`, `hive_tasks`, `hive_claims`, `hive_recent`, `hive_pause`, `hive_resume`, `hive_say`). `Q518`/`Q520`: send a JSON-RPC `POST`. `Q519`: send a smaller body. `Q521`: add the Slack member ID to `--allow` (resume / say), or ask a full workspace member. `Q516`: read the message after it; it names the failure. |
469
- | **`Q630`** | `SLACK_BOT_TOKEN` is missing (required by `slack start` and by `hive-mcp-http.js` in `--auth slack` mode), invalid (`invalid_auth`), or revoked (`token_revoked`) when checked against Slack `auth.test`. | Copy the **Bot User OAuth Token** (`xoxb-…`) from **OAuth & Permissions** in your Slack app settings and set `SLACK_BOT_TOKEN` in the same terminal session. |
470
- | **`Q631`** | `--channel` is missing, `channel_not_found`, or `@Hivecode` has not been invited to the channel (`is_member: false` / `not_in_channel`). | Open the target channel in Slack and run `/invite @Hivecode`. If it is a private channel, ensure the app has `groups:read` scope and reinstall to workspace. |
471
- | **`Q632`** | Another `SlackBridge` with the same `--name` is already connected to this room. | Stop the existing `slack start` process before starting a new one so events are not posted twice. |
472
- | **`Q633`** | `--slack-channel` was passed to `github start`, but `SLACK_BOT_TOKEN` is not set in the environment. | Set `SLACK_BOT_TOKEN=xoxb-…` (or `$env:SLACK_BOT_TOKEN="xoxb-…"` in PowerShell) before running `github start --slack-channel`. |
473
- | **`Q634`** | Inbound Slack configuration cannot work safely: (1) `SLACK_APP_TOKEN` (`xapp-…`) is missing or rejected by `apps.connections.open`; (2) `--slack-allow` was passed to `github start` without `--slack-channel`, without `SLACK_APP_TOKEN`, or with usernames instead of member IDs (`U…`); or (3) Slack `auth.test` did not return a workspace `team_id` (in `github start` or `hive-mcp-http.js`), so external workspaces could not be refused. | Enable **Socket Mode** and set `SLACK_APP_TOKEN=xapp-…` (`connections:write` scope); pass Slack member IDs (`U…` from **Profile → `⋯` → Copy member ID**) to `--slack-allow` / `--allow`; or pass `--team <T…>` (`SLACK_TEAM_ID`) / reinstall the Slack app if `auth.test` omitted `team_id`. |
474
- | **`Q635`** | Another process is already connected to Slack Socket Mode with this `SLACK_APP_TOKEN` (Slack distributes incoming events across active connections, so messages or "stop" commands may go to the other process). | Ensure each process reading Slack (`slack start`, `github start --slack-allow`, `hive-slack-agent`) uses its own dedicated Slack app and App-Level Token. Check for another running terminal or service and stop it. If you just stopped a bridge or watcher, wait a minute for Slack to release the connection and start again. |
260
+ | **`Q660`** | Relay | `HIVE_SLACK_KEY` encryption key is not configured on the relay (HTTP 503). Set a 32-byte hex secret on the relay server. |
261
+ | **`Q661`** | Relay / CLI | Connection ID is unknown or expired. Re-run `slack connect`. |
262
+ | **`Q662`** | CLI / Relay | Slack rejected the bot token (`auth.test` failed). Ensure token begins with `xoxb-` and the app is installed. |
263
+ | **`Q663`** | Relay | Bad Slack signature or timestamp outside 5-minute replay window (HTTP 401). Verify Signing Secret. |
264
+ | **`Q664`** | Relay | Bot is not in the channel. In Slack, run `/invite @Hivecode` in the target channel. |
265
+ | **`Q665`** | CLI | Invalid room join link syntax provided to `slack connect` or `slack disconnect`. |
266
+ | **`Q666`** | CLI | Network failure reaching relay HTTP endpoint. |
267
+ | **`Q667`** | CLI | Required `SLACK_BOT_TOKEN` or `SLACK_SIGNING_SECRET` was not provided. |
268
+ | **`Q668`** | CLI | Bot is in no channel (run `/invite @Hivecode`), or multiple channels without `--channel` in non-interactive mode. |
269
+ | **`Q630`** | `slack start` | `SLACK_BOT_TOKEN` missing or revoked. |
270
+ | **`Q631`** | `slack start` | Bot not invited to `--channel`. Run `/invite @Hivecode`. |
271
+ | **`Q632`** | `slack start` | Another bridge with the same name is already running in this room. |
272
+ | **`Q634`** | `slack start` | Missing `SLACK_APP_TOKEN` (`xapp-…`) or required scopes for Socket Mode. |
273
+ | **`Q635`** | `slack start` | Multiple active processes sharing the same `SLACK_APP_TOKEN`. |
@@ -56,7 +56,7 @@ This guide explains every error code (`Qxxx`) emitted by Hivecode CLI commands,
56
56
  | `Q571` | `hive-branch.js` | Landing branch PR | Working tree has uncommitted changes on the checkout branch. | Commit or stash changes (`git stash`) before landing PR. |
57
57
  | `Q572` | `hive-branch.js` | Landing branch PR | The room deleted files that were changed on the branch upstream since seeded. | Restore deleted files in the room or delete them upstream on the branch manually. |
58
58
  | `Q573` | `hive-branch.js` | Landing branch PR | Local commits landed on branch successfully, but `git push` failed. | Check remote permissions and push manually (`git push origin <branch>`). |
59
- | `Q574` | `hive-branch.js` | Opening PR from branch | No base branch specified to open PR into, or `gh pr create` failed. | Pass `--base <branch>` or ensure `gh` is authenticated. |
59
+ | `Q574` | `hive-branch.js`, `hive-land.js`, `hive-gh-action.js` | Opening PR from branch or Action landing | No base branch specified (nothing changed), `gh pr create` failed, or GitHub Actions lacks PR creation permissions (HTTP 403; work is already pushed to branch). | Enable Settings → Actions → General → Workflow permissions → "Allow GitHub Actions to create and approve pull requests", or open the PR manually from the branch. Once enabled, the next run opens the PR without changes in the room. For local runs, pass `--base <branch>` or check `gh auth`. |
60
60
  | `Q580` | `sync.js` | Syncing a room folder | Room directory was deleted or moved while sync was active. | Recreate the folder or re-join the room; files on teammates' clients were protected. |
61
61
  | `Q581` | `hive-mcp.js` | Joining a room (`hive_join`) | Participant name is already in use by another connected participant. | Choose a distinct display name via `--name` or in `hive_join`. |
62
62
  | `Q600` | `hive-github.js` | Running `github start` or watcher background polling | GitHub CLI (`gh`) not installed, `gh auth login` required, or token expired/HTTP 401. | Install GitHub CLI (`gh`), run `gh auth login`, or set valid `GH_TOKEN`. |
@@ -115,3 +115,12 @@ This guide explains every error code (`Qxxx`) emitted by Hivecode CLI commands,
115
115
  | `Q655` | `hive-room-mode.js` | Starting room-mode watcher | Another room-mode watcher is already active for this room (watcher lock held). | Stop the other running watcher process, or remove the stale lock file in `.git/hive-room-watcher.lock` if crashed. |
116
116
  | `Q656` | `hive-room-mode.js` | Room-mode watcher connecting to relay | Could not connect to relay at specified URL, or connection timed out. | Verify internet connectivity, check relay status, and ensure the relay URL is correct. |
117
117
  | `Q657` | `hive-room-mode.js` | Dispatching an issue task or chat into room | Delivery verification failed: task assignment or chat message was not confirmed in room within timeout. | Check relay stability, ensure room has reachable participants, and retry with `/hivecode retry`. |
118
+ | `Q660` | `server.js`, `hive-slack-relay.js` | Calling relay Slack endpoints (`/slack/connect`, `/slack/prepare`) | `HIVE_SLACK_KEY` encryption key is not set in the relay environment (HTTP 503). | When self-hosting a relay, set `HIVE_SLACK_KEY` to a 64-hex-character (32-byte) secret to enable encrypted credential storage. |
119
+ | `Q661` | `server.js`, `hive-slack-relay.js` | Slack events webhook or `slack connect` / `disconnect` | Connection ID is unknown, expired, or invalid. | Run `slack connect "<room link>"` to establish a new Slack connection. Prepared IDs expire after 24 hours if unconnected. |
120
+ | `Q662` | `server.js`, `hive-slack-connect.js` | Verifying bot token during `slack connect` | Slack rejected the bot token (`auth.test` failed or invalid bot token). | Check that `SLACK_BOT_TOKEN` is the Bot User OAuth Token (`xoxb-…`) and the Slack app is installed to the workspace. |
121
+ | `Q663` | `server.js`, `hive-slack-relay.js` | Relay receiving Slack event notification | Slack request verification failed: invalid v0 HMAC-SHA256 signature or timestamp outside 5-minute replay window (HTTP 401). | Verify that the signing secret registered during `slack connect` matches the Slack app's Signing Secret. Check server clock NTP sync. |
122
+ | `Q664` | `server.js`, `hive-slack-relay.js` | Relay connecting Slack to room channel | Bot user is not a member of the designated Slack channel. | In Slack, open the target channel and run `/invite @Hivecode`, then run `slack connect` again. |
123
+ | `Q665` | `hive-slack-connect.js` | Running `slack connect` or `slack disconnect` | Provided target is not a valid room link. | Pass a valid room join link (format: `wss://<relay>|room-<id>` or `https://<relay>/#room-<id>`). |
124
+ | `Q666` | `hive-slack-connect.js` | Running `slack connect` or `slack disconnect` | Could not reach the relay server over HTTP/HTTPS. | Verify network connectivity to the relay host and check that the relay server is up and responsive. |
125
+ | `Q667` | `hive-slack-connect.js` | Running `slack connect` or `slack disconnect` | Missing required secret: `SLACK_BOT_TOKEN` or `SLACK_SIGNING_SECRET`. | Provide the missing secret interactively when prompted (hidden input) or set the environment variables `SLACK_BOT_TOKEN` and `SLACK_SIGNING_SECRET`. |
126
+ | `Q668` | `hive-slack-connect.js` | Selecting channel during `slack connect` | The bot is in no channels (or multiple channels without `--channel` in non-interactive mode). | In Slack, invite the bot with `/invite @Hivecode`. In non-interactive mode with multiple channels, specify `--channel <id>`. |
package/hive-gh-action.js CHANGED
@@ -40,6 +40,11 @@ const markOf = (body, kind) => {
40
40
  const oneLine = (s, n = 300) => String(s == null ? '' : s).replace(/\s+/g, ' ').trim().slice(0, n)
41
41
  const PUSHERS = new Set(['admin', 'maintain', 'write'])
42
42
  const ISSUE_TASK = (n) => new RegExp(`^Issue #${n}(?: \\(attempt \\d+\\))?:`)
43
+ // Test output that means "the runner has no installed dependencies", in Node and in Python. The fix
44
+ // is a step in the workflow file, which nobody in the room can add, so the issue comment names it.
45
+ const MISSING_DEPS = /Cannot find (?:module|package)|ERR_MODULE_NOT_FOUND|MODULE_NOT_FOUND|No module named/
46
+ const PR_NOT_ALLOWED = /not permitted to create or approve pull requests/i
47
+ const PR_SETTING = 'Settings → Actions → General → Workflow permissions → "Allow GitHub Actions to create and approve pull requests"'
43
48
 
44
49
  /** The issue a pull request closes, from the "Closes #N" Hivecode writes first in its body. */
45
50
  export function closesOf(body) {
@@ -124,7 +129,9 @@ export async function roomDeps({ link, root, branch, github }) {
124
129
  * Returns { did: [what it did, for the log] }. A refusal is a result; a throw is a failed job, which
125
130
  * GitHub shows in red and the next run retries.
126
131
  */
127
- export async function runAction({ gh, deps, eventName = '', event = {}, branch, label = LABEL, base = '', log = () => { }, notify = async () => false }) {
132
+ export async function runAction({ gh, deps, eventName = '', event = {}, branch, label = LABEL, base = '', runAt = '', log = () => { }, notify = async () => false }) {
133
+ // runAt: the commit this run's checkout is at (the default branch, where the workflow file lives).
134
+ // A landing refused for something the workflow has to fix is tried again when it changes.
128
135
  const did = []
129
136
  const note = (s) => { did.push(s); log(s) }
130
137
  // What a person follows in Slack or Discord (HIVE_NOTIFY_WEBHOOK): the issue's life, never the room link.
@@ -228,32 +235,45 @@ export async function runAction({ gh, deps, eventName = '', event = {}, branch,
228
235
  const task = issueTasks(tasks, n)[0]
229
236
  if (!task) { note(`#${n}: its task is not in the room (the relay lost the room?); nothing to land`); return { did } }
230
237
  let openPr = pulls.find((p) => p.state === 'open' && closesOf(p.body) === n) || null
231
- // A PULL REQUEST ON THE BRANCH THAT SOMEONE ELSE OPENED (an agent's hive_pr, a person) is this
232
- // issue's: the room lands on one branch, one issue at a time. It is told so ("Closes #N" goes in
233
- // first), so merging it closes the issue and the next run sees it as the issue's.
238
+ const lands = commentsOf(n).map((c) => markOf(c.body, 'land')).filter(Boolean)
239
+ const lastLand = lands[lands.length - 1] || null
240
+ // The issue's work is on the branch on origin once a landing pushed it: one that opened or updated
241
+ // a pull request (ok=1), or one that pushed and could not open the pull request (pushed=1).
242
+ const workOnBranch = lands.some((l) => l.ok === '1' || l.pushed === '1')
243
+ // "Closes #N" goes in first, so merging the pull request closes the issue and the next run sees
244
+ // it as the issue's.
245
+ const link = async (pr) => {
246
+ try {
247
+ gh.updatePull(pr.number, { body: `Closes #${n}\n\n${String(pr.body || '')}` })
248
+ pr.body = `Closes #${n}\n\n${String(pr.body || '')}`
249
+ comment(n, `#${pr.number} is this issue's pull request: it is on the room's branch and holds this issue's work, so Hivecode linked it.\n\n${MARK('adopted', { pr: pr.number })}`)
250
+ await deps.say(`@all PR #${pr.number} on the room's branch is now issue #${n}'s pull request (it closes #${n} when merged).`)
251
+ await tell(`PR #${pr.number} is now the pull request for issue #${n}.`)
252
+ note(`#${n}: linked PR #${pr.number}`)
253
+ return true
254
+ } catch (e) { note(`#${n}: could not link PR #${pr.number}: ${oneLine(e.message, 160)}`); return false }
255
+ }
256
+ // A PULL REQUEST ON THE BRANCH THAT SOMEONE ELSE OPENED (an agent, a person) is this issue's ONLY
257
+ // ONCE IT HOLDS THIS ISSUE'S WORK (11 Nov 2026). A pull request head is the branch, so it holds the
258
+ // work from the landing's push on. Before that it is somebody's own change that happens to sit on
259
+ // the room's branch: linking it made merging a setup fix close issue #22 with nothing landed.
234
260
  if (!openPr) {
235
261
  const other = pulls.find((p) => p.state === 'open' && !closesOf(p.body))
236
- if (other) {
237
- try {
238
- gh.updatePull(other.number, { body: `Closes #${n}\n\n${String(other.body || '')}` })
239
- other.body = `Closes #${n}\n\n${String(other.body || '')}`
240
- openPr = other
241
- comment(n, `#${other.number} is this issue's pull request: it was opened on the room's branch, so Hivecode linked it.\n\n${MARK('adopted', { pr: other.number })}`)
242
- await deps.say(`@all PR #${other.number} on the room's branch is now issue #${n}'s pull request (it closes #${n} when merged).`)
243
- await tell(`PR #${other.number} is now the pull request for issue #${n}.`)
244
- note(`#${n}: linked PR #${other.number}`)
245
- } catch (e) { note(`#${n}: could not link PR #${other.number}: ${oneLine(e.message, 160)}`) }
246
- }
262
+ if (other && workOnBranch) { if (await link(other)) openPr = other }
263
+ else if (other) note(`#${n}: PR #${other.number} is open on ${branch} but holds none of this issue's work yet; left alone`)
247
264
  }
248
- const cs = commentsOf(n)
249
- const lastLand = cs.map((c) => markOf(c.body, 'land')).filter(Boolean).pop() || null
265
+ const prOnlyBefore = !!lastLand && lastLand.code === 'Q574' && lastLand.pushed === '1'
266
+ const tryAgain = prOnlyBefore || (!!lastLand && lastLand.deps === '1' && !!runAt && lastLand.at !== runAt)
250
267
  if (task.status === 'done') {
251
268
  await deps.bind()
252
269
  const r = await deps.landIssue({
253
270
  taskId: task.id,
254
271
  issue: { number: n, title: active.title },
255
- // The same plan, branch and room as the last landing, ok or not, is not run again.
256
- state: { lastFailedFingerprint: lastLand ? lastLand.fp : '' },
272
+ // The same plan, branch and room as the last landing, ok or not, is not run again. Two refusals
273
+ // are, because what fixes them changes none of the three: the pull request GitHub would not
274
+ // let the job open (a repository setting; nothing new lands, so no tests run again), and tests
275
+ // that found no installed dependencies, once the workflow file has changed.
276
+ state: { lastFailedFingerprint: lastLand && !tryAgain ? lastLand.fp : '' },
257
277
  by: 'GitHub Actions',
258
278
  closes: true,
259
279
  skipEmpty: !!openPr,
@@ -267,12 +287,36 @@ export async function runAction({ gh, deps, eventName = '', event = {}, branch,
267
287
  : `@all PR #${r.pr.number} for issue #${n} was updated${r.pr.newCommits ? ` with ${r.pr.newCommits} new commit(s)` : ''}.`)
268
288
  note(`#${n}: ${made ? 'opened' : 'updated'} PR #${r.pr.number}`)
269
289
  await tell(made ? `issue #${n} is up for review: ${r.pr.url || `PR #${r.pr.number}`}` : `PR #${r.pr.number} for issue #${n} was updated with the room's fixes.`)
290
+ // The landing went onto a pull request that was already open on the branch and is not yet the
291
+ // issue's: it holds the issue's work now, so it is linked in this run.
292
+ if (!openPr) {
293
+ const landedOn = pulls.find((p) => p.state === 'open' && p.number === r.pr.number && !closesOf(p.body))
294
+ if (landedOn && await link(landedOn)) openPr = landedOn
295
+ }
270
296
  } else if (r && r.code) {
271
297
  // A refusal with a code is said once per landing (its fingerprint), on the issue and in the room.
272
- if (!lastLand || lastLand.fp !== (r.fingerprint || '') || lastLand.code !== r.code) {
273
- comment(n, `Hivecode could not land this (${r.code}): ${oneLine(r.why, 600)}\nNothing was pushed. It is tried again when the room's files or the branch change.\n\n${MARK('land', { fp: r.fingerprint || '', code: r.code })}`)
274
- await tell(`issue #${n}: the landing was refused (${r.code}). Nothing was pushed; the room was told.`)
275
- if (r.code !== 'Q576') await deps.say(`@all issue #${n} is done, but its landing was refused (${r.code}): ${oneLine(r.why, 400)} Nothing was pushed.`)
298
+ const fp = r.fingerprint || ''
299
+ const why = String(r.why || '')
300
+ // PUSHED, BUT NO PULL REQUEST (11 Nov 2026): the commits were on the branch and the comment
301
+ // said "Nothing was pushed". Said once however often it is tried again.
302
+ const prOnly = r.code === 'Q574' && !!r.onBranch
303
+ const noDeps = r.code === 'Q576' && MISSING_DEPS.test(why)
304
+ const repeat = prOnly ? prOnlyBefore
305
+ : !!lastLand && lastLand.fp === fp && lastLand.code === r.code && (!noDeps || !runAt || lastLand.at === runAt)
306
+ if (prOnly && !repeat) {
307
+ const reason = oneLine(why.replace(/^[\s\S]*?opening the PR failed:\s*/, ''), 400)
308
+ const setting = PR_NOT_ALLOWED.test(why)
309
+ comment(n, `The work for this issue is pushed to \`${branch}\`, but Hivecode could not open the pull request (Q574): ${reason}\n` +
310
+ (setting ? `GitHub does not let Actions open pull requests in this repository. Turn it on: ${PR_SETTING} (in an organization, an owner may have to allow it first). The next run then opens the pull request: Actions → Hivecode → Run workflow, or the 10-minute timer.\n`
311
+ : 'The next run tries to open it again.\n') +
312
+ `Or open a pull request from \`${branch}\` by hand: Hivecode links it to this issue.\n\n${MARK('land', { fp, code: r.code, pushed: 1 })}`)
313
+ await tell(`issue #${n}: the work is pushed to ${branch}, but the pull request could not be opened (Q574). ${setting ? 'Turn on "Allow GitHub Actions to create and approve pull requests" in the repository\'s Actions settings, or open it by hand.' : 'Open it by hand, or wait for the next run.'}`)
314
+ await deps.say(`@all issue #${n} is done and its work is pushed to ${branch}, but the pull request could not be opened (Q574): ${reason}. A person has to ${setting ? 'change a repository setting' : 'look at it'} or open the pull request by hand; there is nothing to fix in the room.`)
315
+ } else if (!repeat) {
316
+ const hint = noDeps ? `The tests did not find the project's dependencies on the runner. Add an install step before the Hivecode step in \`.github/workflows/hivecode.yml\` (for npm: \`- run: npm ci\`); the landing is tried again once that change is on the default branch.\n` : ''
317
+ comment(n, `Hivecode could not land this (${r.code}): ${oneLine(why, 600)}\nNothing ${workOnBranch ? 'new ' : ''}was pushed. ${hint}It is tried again when the room's files or the branch change.\n\n${MARK('land', { fp, code: r.code, ...(noDeps ? { deps: 1, ...(runAt ? { at: runAt } : {}) } : {}) })}`)
318
+ await tell(`issue #${n}: the landing was refused (${r.code}). Nothing was pushed; the room was told.${noDeps ? ' The workflow file needs an install step.' : ''}`)
319
+ if (r.code !== 'Q576') await deps.say(`@all issue #${n} is done, but its landing was refused (${r.code}): ${oneLine(why, 400)} Nothing was pushed.`)
276
320
  }
277
321
  note(`#${n}: landing refused (${r.code})`)
278
322
  } else note(`#${n}: not ready to land: ${oneLine(r && r.why, 200)}`)
@@ -308,9 +352,35 @@ export async function runAction({ gh, deps, eventName = '', event = {}, branch,
308
352
  }
309
353
 
310
354
  // ── the workflow file a repository adds ──────────────────────────────────────────────────────────
311
- export function workflowFile({ branch = '<branch>', version = 'latest' } = {}) {
355
+ /** How this repository installs what its tests need, from the lockfile it keeps: { found, commands }.
356
+ * Nothing recognised is found '' and no commands; the workflow then says where the step goes. */
357
+ export function installStepsFor(dir) {
358
+ const has = (f) => { try { return fs.statSync(path.join(dir, f)).isFile() } catch { return false } }
359
+ for (const [file, command] of [
360
+ ['package-lock.json', 'npm ci'],
361
+ ['npm-shrinkwrap.json', 'npm ci'],
362
+ ['pnpm-lock.yaml', 'corepack enable && pnpm install --frozen-lockfile'],
363
+ ['yarn.lock', 'yarn install --frozen-lockfile'],
364
+ ['package.json', 'npm install'],
365
+ ]) if (has(file)) return { found: file, commands: [command] }
366
+ return { found: '', commands: [] }
367
+ }
368
+
369
+ export function workflowFile({ branch = '<branch>', version = 'latest', install = [] } = {}) {
370
+ // THE LANDING RUNS THE REPOSITORY'S TESTS ON THE RUNNER, which starts with no dependencies: without
371
+ // this step every first landing on a project with dependencies failed (11 Nov 2026).
372
+ // `install` is what installStepsFor answers, or just its list of commands.
373
+ const steps = (Array.isArray(install) ? install : (install && Array.isArray(install.commands) ? install.commands : [])).map((c) => String(c || '').replace(/[\r\n]+/g, ' ').trim()).filter(Boolean)
374
+ const installBlock = steps.length
375
+ ? ` # The landing runs your tests on this runner, so what they need is installed first.\n${steps.map((c) => ` - run: ${c}`).join('\n')}\n`
376
+ : ` # The landing runs your tests on this runner. No package.json or lockfile was found, so nothing
377
+ # is installed. If your tests need dependencies, add the install step here, for example:
378
+ # - run: pip install -r requirements.txt
379
+ `
312
380
  return `# Hivecode: GitHub issues go to the agents in a room, and their work comes back as a pull request.
313
381
  # Needs one repository secret, HIVE_ROOM: the room's link (Settings -> Secrets and variables -> Actions).
382
+ # And one setting, so the job may open the pull request: Settings -> Actions -> General ->
383
+ # Workflow permissions -> "Allow GitHub Actions to create and approve pull requests".
314
384
  name: Hivecode
315
385
 
316
386
  on:
@@ -351,7 +421,7 @@ jobs:
351
421
  - uses: actions/setup-node@v4
352
422
  with:
353
423
  node-version: 22
354
- - run: npx -y hivecode-mcp@${version} github action
424
+ ${installBlock} - run: npx -y hivecode-mcp@${version} github action
355
425
  env:
356
426
  HIVE_ROOM: \${{ secrets.HIVE_ROOM }}
357
427
  HIVE_BRANCH: ${branch}
@@ -365,7 +435,10 @@ jobs:
365
435
  export async function main(argv = [], { cwd = process.cwd(), env = process.env, log = console.log, logErr = console.error, call } = {}) {
366
436
  if (argv.includes('--workflow')) {
367
437
  const bi = argv.indexOf('--branch')
368
- log(workflowFile({ branch: bi !== -1 && argv[bi + 1] ? argv[bi + 1] : '<branch>' }))
438
+ // What to install is read from the repository this is run in (its top level, when it is one).
439
+ const here = git(cwd, ['rev-parse', '--show-toplevel'])
440
+ const found = installStepsFor(here.code === 0 && here.out ? path.resolve(here.out) : cwd)
441
+ log(workflowFile({ branch: bi !== -1 && argv[bi + 1] ? argv[bi + 1] : '<branch>', install: found.commands }))
369
442
  return 0
370
443
  }
371
444
  const link = String(env.HIVE_ROOM || '').trim()
@@ -399,7 +472,8 @@ export async function main(argv = [], { cwd = process.cwd(), env = process.env,
399
472
  try {
400
473
  const W = await import('./hive-webhook.js')
401
474
  const notify = W.makeNotifier(String(env.HIVE_NOTIFY_WEBHOOK || '').trim(), { log })
402
- const r = await runAction({ gh, deps, eventName: env.GITHUB_EVENT_NAME || '', event, branch, label, base, log, notify })
475
+ const runAt = git(root, ['rev-parse', 'HEAD']).out.slice(0, 12)
476
+ const r = await runAction({ gh, deps, eventName: env.GITHUB_EVENT_NAME || '', event, branch, label, base, runAt, log, notify })
403
477
  if (!r.did.length) log('nothing to do')
404
478
  return 0
405
479
  } catch (e) {
package/hive-land.js CHANGED
@@ -382,7 +382,9 @@ export async function landAndOpen(opts = {}) {
382
382
  let pr = null
383
383
  try { pr = github.createPull({ title: o.title || `Hivecode: ${name}`, head: name, base: into, body }) } catch (e) {
384
384
  const again = e && e.status === 422 ? (github.pullsFor(name) || []).find((p) => p.state === 'open') : null
385
- if (!again) return refuse('Q574', `the work is pushed to ${name}, but opening the PR failed: ${e.message}`)
385
+ // onBranch: origin has the work and only the pull request is missing. A caller that speaks for
386
+ // the landing (the GitHub Action) must not say "nothing was pushed", and tries the PR again.
387
+ if (!again) return refuse('Q574', `the work is pushed to ${name}, but opening the PR failed: ${e.message}`, { onBranch: true })
386
388
  pr = again
387
389
  }
388
390
  const closed = prs.filter((p) => p.state !== 'open').map((p) => `#${p.number}`)
package/hive-room-mode.js CHANGED
@@ -1314,6 +1314,8 @@ async function landIssueOn(opts = {}) {
1314
1314
  why: landRes.why,
1315
1315
  fingerprint,
1316
1316
  state: nextState,
1317
+ // Q574 after the push: the branch on origin holds the work, only the pull request is missing.
1318
+ ...(landRes.onBranch ? { onBranch: true } : {}),
1317
1319
  }
1318
1320
  }
1319
1321
 
@@ -0,0 +1,150 @@
1
+ // hive-slack-connect.js -- `slack connect` / `slack disconnect`: Slack <-> a room through the relay, with
2
+ // nothing running on anyone's machine (issue #22, part B). The relay does the work (parts A and C); this
3
+ // is the setup: ask the relay for a request URL, print the app manifest with it, take the bot token and
4
+ // signing secret once, let the person pick the channel, hand all of it to the relay.
5
+ //
6
+ // Relay contract (v2): POST /slack/prepare {room} -> {ok,id,eventsUrl}; POST /slack/connect {id,botToken,
7
+ // signingSecret,channel} -> {ok,team,channel,eventsUrl}; POST /slack/disconnect {id|room,signingSecret} -> {ok}.
8
+ // Errors come back as {ok:false,code,why}. Codes here: Q665 bad room link, Q666 relay not reached,
9
+ // Q667 token or signing secret missing, Q668 channel to choose.
10
+ // The secrets go to the relay and nowhere else: not to the log, not to a file, not to the room.
11
+ import fs from 'fs'
12
+ import os from 'os'
13
+ import path from 'path'
14
+ import readline from 'readline'
15
+ import { parseLink } from './sync.js'
16
+ import { slackPost } from './hive-slack.js'
17
+
18
+ export const CONNECT_USAGE = `usage:
19
+ npx -y hivecode-mcp@latest slack connect "<relay|room>" [--channel <id>] (SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET, or it asks)
20
+ npx -y hivecode-mcp@latest slack disconnect "<relay|room>" [--id <connection id>] (SLACK_SIGNING_SECRET, or it asks)
21
+ `
22
+
23
+ const BOT_SCOPES = ['chat:write', 'channels:history', 'groups:history', 'users:read', 'channels:read', 'groups:read']
24
+
25
+ /** The Slack app to create from a manifest: events to the relay, no Socket Mode, so no xapp- token. */
26
+ export function slackManifest(eventsUrl) {
27
+ return {
28
+ display_information: { name: 'Hivecode', description: 'Messages between a Slack channel and a Hivecode room.', background_color: '#111318' },
29
+ features: { bot_user: { display_name: 'Hivecode', always_online: true } },
30
+ oauth_config: { scopes: { bot: BOT_SCOPES } },
31
+ settings: {
32
+ event_subscriptions: { request_url: eventsUrl, bot_events: ['message.channels', 'message.groups'] },
33
+ org_deploy_enabled: false, socket_mode_enabled: false, token_rotation_enabled: false,
34
+ },
35
+ }
36
+ }
37
+
38
+ /** One line from the person. Hidden (no echo) for secrets. Only used on a terminal. */
39
+ export function askLine(prompt, { hidden = false, input = process.stdin, output = process.stderr } = {}) {
40
+ return new Promise((resolve) => {
41
+ const rl = readline.createInterface({ input, output, terminal: true })
42
+ if (hidden) rl._writeToOutput = (s) => { if (s === prompt || /[\r\n]/.test(s)) output.write(s) }
43
+ rl.question(prompt, (a) => { rl.close(); resolve(String(a).trim()) })
44
+ })
45
+ }
46
+
47
+ const stateFile = (home) => path.join(home, '.hivecode', 'slack-connect.json')
48
+ function readState(home) { try { return JSON.parse(fs.readFileSync(stateFile(home), 'utf8')) || {} } catch { return {} } }
49
+ function writeState(home, s) {
50
+ fs.mkdirSync(path.dirname(stateFile(home)), { recursive: true })
51
+ fs.writeFileSync(stateFile(home), JSON.stringify(s, null, 2), { mode: 0o600 })
52
+ }
53
+
54
+ async function callRelay(base, route, body, fetchImpl) {
55
+ try {
56
+ const r = await fetchImpl(`${base}${route}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(45000) })
57
+ const j = await r.json().catch(() => null)
58
+ if (j && typeof j === 'object') return j.ok ? j : { ok: false, code: j.code || `http_${r.status}`, why: j.why || `the relay answered ${r.status}` }
59
+ return { ok: false, code: `http_${r.status}`, why: `the relay answered ${r.status} without a reason` }
60
+ } catch (e) {
61
+ return { ok: false, code: 'Q666', why: `could not reach ${base} (${e && e.message ? e.message : e})` }
62
+ }
63
+ }
64
+
65
+ function target(link, logErr) {
66
+ const { relay, room } = parseLink(link || '')
67
+ if (!relay || !room) { logErr(`Q665 "${link || ''}" is not a room link (expected wss://relay|room-…).`); return null }
68
+ return { base: String(relay).replace(/^ws/i, 'http').replace(/\/+$/, ''), room }
69
+ }
70
+
71
+ /** Channels the bot is in: id, name, privacy. Follows Slack's cursor, at most 5 pages. */
72
+ async function botChannels(token, fetchImpl, api) {
73
+ const out = []
74
+ let cursor = ''
75
+ for (let i = 0; i < 5; i++) {
76
+ const r = await slackPost('users.conversations', { types: 'public_channel,private_channel', exclude_archived: true, limit: 200, cursor: cursor || undefined }, token, fetchImpl, api)
77
+ for (const c of r.channels || []) out.push({ id: c.id, name: c.name || c.id, isPrivate: !!(c.is_private || c.is_group) })
78
+ cursor = (r.response_metadata && r.response_metadata.next_cursor) || ''
79
+ if (!cursor) break
80
+ }
81
+ return out
82
+ }
83
+
84
+ export async function runSlackConnect({ link, channel = '', env = process.env, log = console.log, logErr = console.error, fetchImpl = fetch, ask = askLine, interactive = Boolean(process.stdin.isTTY), home = os.homedir(), api = env.HIVE_SLACK_API || 'https://slack.com/api' } = {}) {
85
+ const t = target(link, logErr)
86
+ if (!t) return 2
87
+
88
+ const prep = await callRelay(t.base, '/slack/prepare', { room: t.room }, fetchImpl)
89
+ if (!prep.ok || !prep.id || !prep.eventsUrl) { logErr(`${prep.code || 'Q666'} the relay did not prepare a Slack connection: ${prep.why || 'no id came back'}`); return 1 }
90
+
91
+ log('1. In Slack, open https://api.slack.com/apps -> Create New App -> From a manifest, and paste this:\n')
92
+ log(JSON.stringify(slackManifest(prep.eventsUrl), null, 2))
93
+ log('\n2. Install the app to your workspace. Copy the Bot User OAuth Token (xoxb-…) from "OAuth & Permissions"\n and the Signing Secret from "Basic Information". Both stay between you and the relay.\n')
94
+
95
+ const need = async (name, prompt) => {
96
+ const v = String(env[name] || '').trim()
97
+ if (v) return v
98
+ return interactive ? ask(prompt, { hidden: true }) : ''
99
+ }
100
+ const botToken = await need('SLACK_BOT_TOKEN', 'Bot token (xoxb-…): ')
101
+ const signingSecret = botToken ? await need('SLACK_SIGNING_SECRET', 'Signing secret: ') : ''
102
+ if (!botToken || !signingSecret) { logErr(`Q667 ${!botToken ? 'SLACK_BOT_TOKEN' : 'SLACK_SIGNING_SECRET'} is missing. Set it in the environment (it is never taken as a flag) and run this again.`); return 1 }
103
+
104
+ let chosen = String(channel || '').trim()
105
+ if (!chosen) {
106
+ let list
107
+ try { list = await botChannels(botToken, fetchImpl, api) } catch (e) {
108
+ const why = (e && (e.slackError || e.message)) || 'unknown'
109
+ logErr(`Q662 Slack refused the bot token (${why}). Check it is the Bot User OAuth Token and the app is installed.`)
110
+ return 1
111
+ }
112
+ if (!list.length) { logErr('Q668 the bot is in no channel. In Slack, open the channel you want and run: /invite @Hivecode'); return 1 }
113
+ if (list.length === 1 && !interactive) chosen = list[0].id
114
+ else if (!interactive) {
115
+ logErr(`Q668 the bot is in ${list.length} channels; say which with --channel <id>:\n${list.map((c) => ` ${c.id} ${c.isPrivate ? '🔒' : '#'}${c.name}`).join('\n')}`)
116
+ return 1
117
+ } else {
118
+ log('Channels the bot is in:')
119
+ list.forEach((c, i) => log(` ${i + 1}. ${c.isPrivate ? '🔒' : '#'}${c.name} (${c.id})`))
120
+ const n = Number(await ask(`Pick one [1-${list.length}]: `)) || 1
121
+ if (n < 1 || n > list.length) { logErr(`Q668 ${n} is not one of the ${list.length} channels.`); return 1 }
122
+ chosen = list[n - 1].id
123
+ }
124
+ }
125
+
126
+ const done = await callRelay(t.base, '/slack/connect', { id: prep.id, botToken, signingSecret, channel: chosen }, fetchImpl)
127
+ if (!done.ok) { logErr(`${done.code} ${done.why}`); return 1 }
128
+ const s = readState(home)
129
+ s[t.room] = { id: prep.id, relay: t.base }
130
+ try { writeState(home, s) } catch { /* disconnecting then needs --id; connecting worked */ }
131
+ log(`✓ Slack is connected to this room${done.channel ? ` (#${done.channel})` : ''}. Post a line in the channel: it shows up in the room.\n To disconnect: npx -y hivecode-mcp@latest slack disconnect "<room link>"`)
132
+ return 0
133
+ }
134
+
135
+ export async function runSlackDisconnect({ link, id = '', env = process.env, log = console.log, logErr = console.error, fetchImpl = fetch, ask = askLine, interactive = Boolean(process.stdin.isTTY), home = os.homedir() } = {}) {
136
+ const t = target(link, logErr)
137
+ if (!t) return 2
138
+ const state = readState(home)
139
+ // The id this computer remembers, or --id; failing both, the relay finds the connection by room (the
140
+ // signing secret is still what proves it is the person who connected).
141
+ const connection = String(id || (state[t.room] && state[t.room].id) || '')
142
+ let signingSecret = String(env.SLACK_SIGNING_SECRET || '').trim()
143
+ if (!signingSecret && interactive) signingSecret = await ask('Signing secret: ', { hidden: true })
144
+ if (!signingSecret) { logErr('Q667 SLACK_SIGNING_SECRET is missing. Only the person who connected can disconnect: it is the same secret.'); return 1 }
145
+ const r = await callRelay(t.base, '/slack/disconnect', connection ? { id: connection, signingSecret } : { room: t.room, signingSecret }, fetchImpl)
146
+ if (!r.ok) { logErr(`${r.code} ${r.why}`); return 1 }
147
+ if (state[t.room]) { delete state[t.room]; try { writeState(home, state) } catch { } }
148
+ log('✓ Slack is disconnected from this room. The relay no longer holds the token.')
149
+ return 0
150
+ }
package/hive-slack.js CHANGED
@@ -61,6 +61,8 @@ export function makeChatForwarder({ chat, forwarded, key, at, threads, onChat })
61
61
  const MAX_SEEN_EVENTS = 500
62
62
 
63
63
  export const USAGE = `usage:
64
+ npx -y hivecode-mcp@latest slack connect "<relay|room>" [--channel <id>] Slack <-> room through the relay, nothing to keep running
65
+ npx -y hivecode-mcp@latest slack disconnect "<relay|room>" [--id <connection id>]
64
66
  SLACK_BOT_TOKEN=xoxb-… npx -y hivecode-mcp@latest slack start "<relay|room>" --channel <id> [--name SlackBridge] [--allow <U123,U456>] [--inbound auto|poll|socket] [--dry-run] [--start-retry <ms>]
65
67
  SLACK_BOT_TOKEN=xoxb-… [SLACK_APP_TOKEN=xapp-…] npx -y hivecode-mcp@latest slack check --channel <id> [--agent] [--inbound auto|poll|socket] [--timeout <ms>] [--start-retry <ms>]
66
68
  SLACK_BOT_TOKEN=xoxb-… SLACK_APP_TOKEN=xapp-… npx -y hivecode-mcp@latest slack agent "<relay|room>" --channel <id> [--name <name>] [--allow <U123,U456>]
@@ -2205,13 +2207,14 @@ export async function runSlackCheck({
2205
2207
  }
2206
2208
  }
2207
2209
 
2208
- export async function main(argv = process.argv.slice(2), { log = console.log, logErr = console.error, fetchImpl = fetch, env = process.env, WebSocketImpl = WebSocket } = {}) {
2210
+ export async function main(argv = process.argv.slice(2), { log = console.log, logErr = console.error, fetchImpl = fetch, env = process.env, WebSocketImpl = WebSocket , connect = {}} = {}) {
2209
2211
  let mode = 'start'
2210
2212
  let CHANNEL = env.SLACK_CHANNEL || ''
2211
2213
  let NAME = 'SlackBridge'
2212
2214
  let ALLOW = env.SLACK_ALLOW || ''
2213
2215
  let allowSource = env.SLACK_ALLOW ? 'SLACK_ALLOW' : '--allow'
2214
2216
  let LINK = ''
2217
+ let CONN_ID = ''
2215
2218
  let dryRun = false
2216
2219
  let agent = false
2217
2220
  let timeoutMs = Number(env.HIVE_SLACK_CHECK_TIMEOUT_MS) || 120000
@@ -2222,7 +2225,8 @@ export async function main(argv = process.argv.slice(2), { log = console.log, lo
2222
2225
  let inbound = String(env.HIVE_SLACK_INBOUND || 'auto').toLowerCase()
2223
2226
  for (let i = 0; i < argv.length; i++) {
2224
2227
  const a = argv[i]
2225
- if ((a === 'start' || a === 'check') && !LINK && i === 0) { mode = a; continue }
2228
+ if ((a === 'start' || a === 'check' || a === 'connect' || a === 'disconnect') && !LINK && i === 0) { mode = a; continue }
2229
+ if (a === '--id') { CONN_ID = argv[++i] || ''; continue }
2226
2230
  if (a === '--inbound') { inbound = String(argv[++i] || '').toLowerCase(); continue }
2227
2231
  if (a === '--help' || a === '-h') { log(USAGE.trim()); return 0 }
2228
2232
  if (a === '--dry-run') { dryRun = true; continue }
@@ -2243,6 +2247,13 @@ export async function main(argv = process.argv.slice(2), { log = console.log, lo
2243
2247
  const TOKEN = env.SLACK_BOT_TOKEN || ''
2244
2248
  const APP_TOKEN = env.SLACK_APP_TOKEN || ''
2245
2249
  const pollMs = Math.max(50, Number(env.HIVE_SLACK_POLL_MS) || INBOUND_DEFAULT_POLL_MS)
2250
+ if (mode === 'connect' || mode === 'disconnect') {
2251
+ const C = await import('./hive-slack-connect.js') // lazy: it imports slackPost from here
2252
+ if (!LINK) { logErr(C.CONNECT_USAGE.trim()); return 2 }
2253
+ return mode === 'connect'
2254
+ ? C.runSlackConnect({ link: LINK, channel: CHANNEL, env, log, logErr, fetchImpl, ...connect })
2255
+ : C.runSlackDisconnect({ link: LINK, id: CONN_ID, env, log, logErr, fetchImpl, ...connect })
2256
+ }
2246
2257
  if (mode === 'check') {
2247
2258
  return await runSlackCheck({
2248
2259
  token: TOKEN,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hivecode-mcp",
3
- "version": "0.14.12",
3
+ "version": "0.14.13",
4
4
  "description": "MCP server that lets any AI coding agent join a Hivecode room — humans and multiple AI agents edit one live codebase with folder-scoped permissions and approval gates.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -87,6 +87,7 @@
87
87
  "hive-notify.js",
88
88
  "hive-webhook.js",
89
89
  "hive-slack.js",
90
+ "hive-slack-connect.js",
90
91
  "hive-slack-agent.js",
91
92
  "hive-mcp-http.js",
92
93
  "slack-app-manifest.json",