claude-slack-channel-bots 0.17.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -7
- package/docs/bots.md +26 -33
- package/docs/configuration.md +9 -7
- package/docs/migration.md +3 -2
- package/docs/reload.md +8 -8
- package/docs/scheduled-prompts.md +1 -1
- package/docs/server-settings.md +21 -8
- package/docs/troubleshooting.md +7 -1
- package/package.json +2 -3
- package/skills/debug-slack-channel-bots/SKILL.md +136 -242
- package/skills/setup-slack-channel-bots/SKILL.md +12 -3
- package/src/ack-tracker.ts +209 -129
- package/src/agent-director-template.ts +10 -6
- package/src/atomic-write.ts +2 -2
- package/src/config.ts +21 -13
- package/src/delivery-constants.ts +0 -1
- package/src/delivery-decision.ts +2 -16
- package/src/delivery-engine.ts +92 -65
- package/src/delivery-lifecycle.ts +33 -16
- package/src/delivery-notices.ts +23 -4
- package/src/delivery-policy.ts +8 -8
- package/src/delivery-queue.ts +210 -16
- package/src/delivery-receipt.ts +6 -6
- package/src/health-check.ts +1 -1
- package/src/jsonl-persistence-check.ts +61 -336
- package/src/legacy-reply-hook-cleanup.ts +60 -0
- package/src/lib.ts +21 -13
- package/src/liveness-reading.ts +29 -35
- package/src/outage-state.ts +16 -0
- package/src/persona-lifecycle.ts +9 -31
- package/src/persona-routing.ts +19 -7
- package/src/registry.ts +20 -19
- package/src/reload-apply.ts +1 -2
- package/src/reload-plan.ts +21 -4
- package/src/reload.ts +3 -3
- package/src/reply-reminder.ts +129 -0
- package/src/restart.ts +122 -335
- package/src/server.ts +98 -169
- package/src/session-manager.ts +92 -444
- package/src/session-transcript.ts +14 -17
- package/src/cozempic.ts +0 -163
- package/src/reply-guard-record.ts +0 -105
- package/src/stop-hook-bootstrap.ts +0 -808
- package/src/trust-bootstrap.ts +0 -172
- package/stop-hooks/slack-reply-guard.sh +0 -357
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ A single HTTP MCP server that runs several independent Claude Code bots, called
|
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Install [Bun](https://bun.sh) before installing CSCB. For installation problems, see [Troubleshooting](docs/troubleshooting.md#troubleshooting).
|
|
10
10
|
|
|
11
11
|
1. **Install globally via bun:**
|
|
12
12
|
|
|
@@ -20,7 +20,7 @@ A single HTTP MCP server that runs several independent Claude Code bots, called
|
|
|
20
20
|
bun pm -g trust claude-slack-channel-bots
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Postinstall creates skeleton config files in `~/.config/claude-slack-channel-bots/` (or `SLACK_STATE_DIR` when set and non-empty) and `~/.claude/slack-mcp.json`, and links the `debug-slack-channel-bots` skill into `~/.claude/skills/`.
|
|
24
24
|
|
|
25
25
|
3. **Run the setup skill:**
|
|
26
26
|
|
|
@@ -47,11 +47,10 @@ A single HTTP MCP server that runs several independent Claude Code bots, called
|
|
|
47
47
|
## Prerequisites
|
|
48
48
|
|
|
49
49
|
- [Bun](https://bun.sh) `>= 1.0.21` (agent-director minimum)
|
|
50
|
-
- [Claude Code](https://claude.ai/code) 2.1.280 or later for the workers, installed and authenticated: CSCB's supported minimum and the version the fleet runs.
|
|
51
|
-
- [`agent-director`](https://github.com/gabemahoney/agent-director) 0.12.0 or later, **installed system-wide
|
|
50
|
+
- [Claude Code](https://claude.ai/code) 2.1.280 or later for the workers, installed and authenticated: CSCB's supported minimum and the version the fleet runs. The `claude` on the workers' `PATH` must be Claude Code's native binary or a wrapper that `exec`s it. See [Troubleshooting](docs/troubleshooting.md#troubleshooting) for launches that stay `pending`.
|
|
51
|
+
- [`agent-director`](https://github.com/gabemahoney/agent-director) 0.12.0 or later, **installed system-wide**. Check it with `bun run install-check`; see [Checking your agent-director install](docs/agent-director.md#checking-your-agent-director-install). For an existing installation, follow [Migration](docs/migration.md#migration), including its switch-over and rollback runbooks.
|
|
52
52
|
- [tmux](https://github.com/tmux/tmux) 3.2 or later, with `remain-on-exit` off. agent-director runs it; CSCB starts no tmux process. agent-director does not depend on tmux's `base-index` or `pane-base-index`, so their values don't matter.
|
|
53
53
|
- Slack workspace admin access (to create and configure one Slack app per persona)
|
|
54
|
-
- **cozempic** (optional) — Python 3.10+ and `pip install cozempic` — used by JSONL path resolution helpers retained for downstream callers.
|
|
55
54
|
|
|
56
55
|
### Supported platforms (inherited from agent-director)
|
|
57
56
|
|
|
@@ -63,7 +62,7 @@ A single HTTP MCP server that runs several independent Claude Code bots, called
|
|
|
63
62
|
| `darwin-x64` (Intel Mac) | **Not supported** by agent-director |
|
|
64
63
|
| Windows | **Not supported** by agent-director |
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
Consult [agent-director's documentation](https://github.com/gabemahoney/agent-director) for the canonical platform support list. For installation or startup failures, see [Startup errors](docs/troubleshooting.md#startup-errors).
|
|
67
66
|
|
|
68
67
|
---
|
|
69
68
|
|
|
@@ -134,7 +133,7 @@ The server applies it without a restart. Lines that start `DESTRUCTIVE:` name pe
|
|
|
134
133
|
| [Channel modes](docs/channel-modes.md) | Choose declarative or fungible mode, and see who can reach a persona in fungible mode |
|
|
135
134
|
| [Reload](docs/reload.md) | Change the configuration of a running server and read its preview |
|
|
136
135
|
| [CLI Reference](docs/cli.md) | Look up a command's behaviour, output and exit codes |
|
|
137
|
-
| [What a bot can do](docs/bots.md) | Learn the bots' tools, how messages reach them, `/interject`, the permission relay and
|
|
136
|
+
| [What a bot can do](docs/bots.md) | Learn the bots' tools, how messages reach them, `/interject`, the permission relay and Slack reply reminders |
|
|
138
137
|
| [Scheduled Prompts](docs/scheduled-prompts.md) | Send prompts to bots on a schedule |
|
|
139
138
|
| [agent-director](docs/agent-director.md) | Check the agent-director install and size CSCB's agent-director timeout |
|
|
140
139
|
| [Troubleshooting](docs/troubleshooting.md) | Fix a persona that is silent or down, a notice, or a startup error |
|
package/docs/bots.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# What a bot can do
|
|
2
2
|
|
|
3
|
-
Tools, receiving messages, injecting a message, permission prompts and
|
|
3
|
+
Tools, receiving messages, injecting a message, permission prompts and Slack reply reminders. See also [Scheduled Prompts](scheduled-prompts.md).
|
|
4
4
|
|
|
5
5
|
## Tools
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ Each MCP endpoint exposes the following tools to the connected Claude Code sessi
|
|
|
8
8
|
|
|
9
9
|
| Tool | Description |
|
|
10
10
|
|---|---|
|
|
11
|
-
| `reply` | Send a message to a channel (in declarative mode one of the persona's configured channels; in fungible mode any channel ID) or, when the persona's `dm.enabled` is `true`, to a DM conversation ID (`D…`) or a Slack user ID (`U…`/`W…`). A user ID opens a DM with that user and posts there as the persona; the result names the DM conversation ID to use for later calls. Splits long text
|
|
11
|
+
| `reply` | Send a message to a channel (in declarative mode one of the persona's configured channels; in fungible mode any channel ID) or, when the persona's `dm.enabled` is `true`, to a DM conversation ID (`D…`) or a Slack user ID (`U…`/`W…`). A user ID opens a DM with that user and posts there as the persona; the result names the DM conversation ID to use for later calls. Splits long text by `reply_chunk_limit` and `reply_chunk_mode`; supports file attachments. With `message_id` (the message being answered), its first successful text post or file upload clears this persona's actual queued or delivered [acknowledgement reaction](server-settings.md#acknowledgement-reactions), without waiting for cleanup before continuing the reply. |
|
|
12
12
|
| `react` | Add an emoji reaction to a Slack message in a channel (a configured channel in declarative mode; any channel ID in fungible mode) or, with `dm.enabled` `true`, a DM conversation (`D…`). |
|
|
13
13
|
| `edit_message` | Edit a previously sent message (bot's own messages only) in a channel (a configured channel in declarative mode; any channel ID in fungible mode) or, with `dm.enabled` `true`, a DM conversation (`D…`). |
|
|
14
14
|
| `fetch_messages` | Fetch message history from a channel (a configured channel in declarative mode; any channel ID in fungible mode), a DM conversation (`D…`, with `dm.enabled` `true`) or a thread in either. Returns oldest-first. |
|
|
@@ -32,7 +32,7 @@ On a host whose Claude Code permission settings don't allow the server's tools a
|
|
|
32
32
|
|
|
33
33
|
## Interject
|
|
34
34
|
|
|
35
|
-
POST to `/interject` to store a message for a configured persona from localhost. The message is queued only for the named persona, including while it is down. Each request is an independent queued item, even with identical sender and text; requests are never merged or deduplicated. Only requests from `127.0.0.1` or `::1` are accepted — external callers are rejected with 403. The bot sees it as described in [How a persona receives messages](#how-a-persona-receives-messages);
|
|
35
|
+
POST to `/interject` to store a message for a configured persona from localhost. The message is queued only for the named persona, including while it is down. Each request is an independent queued item, even with identical sender and text; requests are never merged or deduplicated. Only requests from `127.0.0.1` or `::1` are accepted — external callers are rejected with 403. The bot sees it as described in [How a persona receives messages](#how-a-persona-receives-messages); this input never arms a Slack reply reminder.
|
|
36
36
|
|
|
37
37
|
An injected message carries no Slack conversation; say in its text where to post any Slack reply.
|
|
38
38
|
|
|
@@ -108,6 +108,8 @@ Each persona's Slack app must have **interactivity enabled** with **Socket Mode*
|
|
|
108
108
|
|
|
109
109
|
The `AskUserQuestion` tool is denied for every CSCB-spawned bot via the agent-director template (`deny: ['AskUserQuestion']`). Bots respond to operator questions via the Slack `reply` MCP tool instead. There is no `ask-relay.sh` hook and no `/ask` HTTP endpoint.
|
|
110
110
|
|
|
111
|
+
The template pre-allows `mcp__slack-channel-router__reply`, so a newly spawned Claude persona can answer a reply reminder without human approval. This grants only the reply tool; other tool approvals still follow the permission relay. Template refreshes preserve this rule.
|
|
112
|
+
|
|
111
113
|
### Memory-directory reads
|
|
112
114
|
|
|
113
115
|
The template also pre-allows each bot to read its own persistent-memory directory, so those reads don't surface a permission prompt to a human. One `Read(//<config-dir>/projects/*/memory/**)` rule is derived per distinct Claude config directory across your personas (a persona's own `claude_config_dir`, the top-level `claude_config_dir`, or the `~/.claude` default). The rules follow the applied personas: when a confirmed change alters the set of config directories, the server rewrites them. If that rewrite fails, `server.log` shows a `template refresh: … failed` line. Until the directories change again or the server restarts, a persona using a new directory then asks for permission each time it reads its memory notes. The rule is scoped to `projects/*/memory/**` only — never the config-dir root, which holds live credentials — so it never pre-authorizes credential reads.
|
|
@@ -118,6 +120,8 @@ The template also pre-allows each bot to read its own persistent-memory director
|
|
|
118
120
|
|
|
119
121
|
CSCB stores accepted messages, then types their text into the bot’s terminal in a `<channel source="slack-channel-router" …>` tag. Every Slack message carries its sender’s ID; use the shared prompt file to say whose instructions to follow. The bot answers through `reply`. A message from Slack carries a `via` attribute that says how it reached the persona. An injected message (a scheduled prompt or an `/interject` message) carries no `via`. A received item is durably stored; delivered means its complete text is confirmed in the bot’s transcript. Delivery is at-least-once: an unconfirmed item is offered again, so the bot may see it more than once. Each persona uses strict FIFO across Slack, `/interject` and scheduled prompts, with one item in flight; the next waits until the previous item is confirmed delivered. There is no age cutoff. Delivery and reconnects wait until agent-director reports the bot is `waiting`. If agent-director refuses a key operation, the message stays queued and retries later. Transcript records confirm only the exact message received; they do not decide whether the bot is idle. Messages wait while MCP is disconnected, trigger the configured reconnect, and are delivered once connected again. Which channels a persona serves, and at which delivery, depends on the channel mode (see [Channel modes](channel-modes.md#channel-modes)).
|
|
120
122
|
|
|
123
|
+
Slack feedback has two stages: queued after durable storage (`beehive` by default), then delivered after the whole message is confirmed (`spinthinking` by default). CSCB removes queued before adding delivered; the first successful correlated reply clears the actual stage. Both names are configurable; see [Acknowledgement reactions](server-settings.md#acknowledgement-reactions) for the custom-emoji prerequisite and tracking limits.
|
|
124
|
+
|
|
121
125
|
A message from Slack carries these tag attributes:
|
|
122
126
|
|
|
123
127
|
| Attribute | Value |
|
|
@@ -137,61 +141,50 @@ Each way a message reaches a persona:
|
|
|
137
141
|
|
|
138
142
|
| Kind | How it reaches the persona | Tag attributes | `via` | Reminder |
|
|
139
143
|
|---|---|---|---|---|
|
|
140
|
-
| Direct message | A DM to the persona's own Slack app, when its `dm.enabled` is `true`, whatever the text mentions. With `dm.enabled` `false` the server drops it and logs one `persona-dm-dropped` line. A group DM is never delivered. | Slack attributes; `chat_id` is the DM conversation ID | `dm` | Yes
|
|
141
|
-
| Direct @mention | A message that @mentions the persona. In declarative mode: in a channel listed in its `channels`, with `delivery: mentions` or `delivery: all`. In fungible mode: in a public or private channel its app is in that is not externally shared, at either channel delivery. | Slack attributes | `mention` | Yes
|
|
142
|
-
| `@here` / `@channel` broadcast | A message that uses `@here` or `@channel`. In declarative mode: in a channel listed in the persona's `channels`, with either `delivery`. In fungible mode: in a public or private channel its app is in that is not externally shared, at either channel delivery. `@everyone` and user-group mentions are not broadcasts. | Slack attributes | `broadcast` | Yes
|
|
143
|
-
| Every message, shared channel | A message that neither @mentions the persona nor broadcasts. In declarative mode: in a channel listed in its `channels` with `delivery: all`, when at least one other persona in the applied configuration also has `delivery: all` for that channel, whether or not that persona is up. In fungible mode: in a public or private channel its app is in that is not externally shared, where its channel delivery is `all` (a stored choice, after the loop guard; see [Channel delivery in fungible mode](channel-modes.md#channel-delivery-in-fungible-mode)), when at least one other persona in the applied configuration has a channel delivery of `all` for that channel by the same rule, whether or not that persona's app remains in the channel. | Slack attributes | `receive_all_shared` | Yes
|
|
144
|
-
| Every message, this persona alone | A message that neither @mentions the persona nor broadcasts. In declarative mode: in a channel listed in its `channels` with `delivery: all`, when no other persona in the applied configuration has `delivery: all` for that channel. In fungible mode: in a public or private channel its app is in that is not externally shared, where its channel delivery is `all`, when no other persona in the applied configuration has a channel delivery of `all` for that channel. | Slack attributes | `receive_all` | Yes
|
|
144
|
+
| Direct message | A DM to the persona's own Slack app, when its `dm.enabled` is `true`, whatever the text mentions. With `dm.enabled` `false` the server drops it and logs one `persona-dm-dropped` line. A group DM is never delivered. | Slack attributes; `chat_id` is the DM conversation ID | `dm` | Yes |
|
|
145
|
+
| Direct @mention | A message that @mentions the persona. In declarative mode: in a channel listed in its `channels`, with `delivery: mentions` or `delivery: all`. In fungible mode: in a public or private channel its app is in that is not externally shared, at either channel delivery. | Slack attributes | `mention` | Yes |
|
|
146
|
+
| `@here` / `@channel` broadcast | A message that uses `@here` or `@channel`. In declarative mode: in a channel listed in the persona's `channels`, with either `delivery`. In fungible mode: in a public or private channel its app is in that is not externally shared, at either channel delivery. `@everyone` and user-group mentions are not broadcasts. | Slack attributes | `broadcast` | Yes |
|
|
147
|
+
| Every message, shared channel | A message that neither @mentions the persona nor broadcasts. In declarative mode: in a channel listed in its `channels` with `delivery: all`, when at least one other persona in the applied configuration also has `delivery: all` for that channel, whether or not that persona is up. In fungible mode: in a public or private channel its app is in that is not externally shared, where its channel delivery is `all` (a stored choice, after the loop guard; see [Channel delivery in fungible mode](channel-modes.md#channel-delivery-in-fungible-mode)), when at least one other persona in the applied configuration has a channel delivery of `all` for that channel by the same rule, whether or not that persona's app remains in the channel. | Slack attributes | `receive_all_shared` | Yes |
|
|
148
|
+
| Every message, this persona alone | A message that neither @mentions the persona nor broadcasts. In declarative mode: in a channel listed in its `channels` with `delivery: all`, when no other persona in the applied configuration has `delivery: all` for that channel. In fungible mode: in a public or private channel its app is in that is not externally shared, where its channel delivery is `all`, when no other persona in the applied configuration has a channel delivery of `all` for that channel. | Slack attributes | `receive_all` | Yes |
|
|
145
149
|
| Scheduled prompt | A crontable line naming the persona fires, and the scheduler queues it for that persona only (see [Scheduled Prompts](scheduled-prompts.md#scheduled-prompts-cscb_cron)). | Only `user` = `cscb-cron:<prompt file name without its extension>` (`cscb-cron:standup` for `standup.md`) and `ts` = the fire time in seconds, with six decimals. No `chat_id`, `message_id` or `via`. | none | Never |
|
|
146
150
|
| `/interject` message | A localhost POST to `/interject` addressed by `persona` (see [Interject](#interject)); it reaches only that persona. | Only `user` = the request's `sender` (default `interject`) and `ts`, as for a scheduled prompt. No `chat_id`, `message_id` or `via`. | none | Never |
|
|
147
151
|
|
|
148
|
-
A "Yes" in the Reminder column means
|
|
152
|
+
A "Yes" in the Reminder column means [Slack reply reminders](#slack-reply-reminders) are eligible for that delivery when enabled for the persona.
|
|
149
153
|
|
|
150
154
|
- **The first applicable `via` wins,** in the order `dm`, `mention`, `broadcast`, `receive_all_shared`, `receive_all`. A message that @mentions the persona in a channel it receives in full arrives as `mention`.
|
|
151
|
-
- **
|
|
155
|
+
- **Every @mention stays in the text,** including quoted, repeated and mention-only messages. Original whitespace is kept when the message is stored. Terminal tag formatting converts CRLF/CR line endings to LF and tabs to four spaces, removes other control characters, replaces invalid Unicode surrogates and escapes tag look-alikes and characters the terminal would rewrite.
|
|
152
156
|
- **Every author counts.** Posts by people, bots, integrations and other personas all arrive by these rules. Each persona posts as its own Slack app, so another persona's post reaches it like anyone else's.
|
|
153
157
|
- **Anyone in the workspace can reach a persona,** in both modes: in declarative mode in its listed channels, by each channel's `delivery`; in fungible mode in every channel its app is invited to that it serves, by its channel delivery there; and in both modes by DM when its `dm.enabled` is `true`. There is no approval step. In fungible mode an invite changes who that is (see [Who can reach a persona in fungible mode](channel-modes.md#who-can-reach-a-persona-in-fungible-mode)).
|
|
154
158
|
- **A persona never receives its own posts.**
|
|
155
159
|
- **Slack redeliveries add no second queue item, including across server restarts.** Each accepted Slack message keeps its key for at least 24 hours after intake; the paired channel @mention adds no second item.
|
|
156
160
|
- **In fungible mode, an @mention is delivered from its `message` event.** Of the two events Slack sends for a channel @mention, the `app_mention` event decides nothing and the `message` event carrying the same mention decides, so each @mention is delivered once, whichever event arrives first. An edit that adds a mention to an earlier message sends no new `message` event, so in fungible mode it is not delivered.
|
|
157
161
|
- **A message without `via` is an injected prompt.** Its `user` label is free-form, so an `/interject` sender label, even one starting with `cscb-cron:`, cannot make it look like a Slack message.
|
|
158
|
-
- **When the reminder is off.**
|
|
162
|
+
- **When the reminder is off.** No new reminder is queued when the persona's effective `stop_hook_bootstrap` is `false`. See [Slack reply reminders](#slack-reply-reminders).
|
|
159
163
|
|
|
160
|
-
## Slack
|
|
164
|
+
## Slack reply reminders
|
|
161
165
|
|
|
162
|
-
CSCB
|
|
163
|
-
|
|
164
|
-
| `via` | Reminder |
|
|
165
|
-
|---|---|
|
|
166
|
-
| `dm` | `This turn started from a Slack direct message (conversation <chat_id>) and you haven't replied. If you meant to answer in Slack, do it now with the mcp__slack-channel-router__reply tool. If no reply is needed, just end your turn.` |
|
|
167
|
-
| `mention` | `This turn started from a Slack message that @mentioned you directly (channel <chat_id>) and you haven't replied. If you meant to answer in Slack, do it now with the mcp__slack-channel-router__reply tool. If no reply is needed, just end your turn.` |
|
|
168
|
-
| `broadcast` | `This turn started from an @here or @channel broadcast in a Slack channel (channel <chat_id>) and you haven't replied. If you meant to answer in Slack, do it now with the mcp__slack-channel-router__reply tool. If no reply is needed, just end your turn.` |
|
|
169
|
-
| `receive_all_shared` | `This turn started from a message in a Slack channel where you and other personas receive every message (channel <chat_id>) and you haven't replied. If you meant to answer in Slack, do it now with the mcp__slack-channel-router__reply tool. If no reply is needed, just end your turn.` |
|
|
170
|
-
| `receive_all` | `This turn started from a message in a Slack channel where only you receive every message (channel <chat_id>) and you haven't replied. If you meant to answer in Slack, do it now with the mcp__slack-channel-router__reply tool. If no reply is needed, just end your turn.` |
|
|
171
|
-
| any other value, empty included | `This turn started from a Slack channel message (channel <chat_id>) and you haven't replied. If you meant to answer in Slack, do it now with the mcp__slack-channel-router__reply tool. If no reply is needed, just end your turn.` |
|
|
172
|
-
|
|
173
|
-
`<chat_id>` is the message's `chat_id`. When `chat_id` is absent or is not a Slack ID (capital letters and digits only), the reminder omits the parenthesis, for example:
|
|
166
|
+
After CSCB delivers a Slack message, it watches the persona's agent-director row. When it observes `working` followed by `waiting` in the same running session, and the persona has made no `reply` tool call since that delivery, CSCB queues one reminder through normal message delivery:
|
|
174
167
|
|
|
175
168
|
```text
|
|
176
|
-
|
|
169
|
+
You received a Slack message and you haven't replied. If you meant to answer in Slack, do it now with the reply tool. If no reply is needed, just end your turn.
|
|
177
170
|
```
|
|
178
171
|
|
|
179
|
-
|
|
172
|
+
The persona can reply or end its turn. There is at most one reminder per delivered Slack message, including across server restarts. Both Claude and Codex personas are covered, regardless of `claude_config_dir`; no `jq` installation is needed.
|
|
180
173
|
|
|
181
|
-
|
|
174
|
+
Claude conversations launched before the reply permission was added receive no new reminders. Resuming a conversation preserves its saved permissions, so a server restart or resume alone does not enable reminders for those older conversations. New Claude launches carry the reply permission and a `reply_tool_preallowed=1` agent-director label; reminders require that label. Existing conversations continue receiving ordinary messages.
|
|
182
175
|
|
|
183
|
-
|
|
176
|
+
Only Slack deliveries arm reminders. Scheduled prompts, `/interject` messages and reminders themselves do not. A `reply` call by that persona suppresses the reminder even if posting fails or the call targets another conversation. Replies through another tool or service can therefore be followed by one harmless reminder.
|
|
184
177
|
|
|
185
178
|
### Turning it on or off
|
|
186
179
|
|
|
187
|
-
Set `stop_hook_bootstrap` in `config.json`: at the top level for every persona, or on a persona to override
|
|
188
|
-
|
|
189
|
-
A change reaches a persona at its first launch after the change is applied (see [Reload](reload.md#reload)): a running instance keeps the value it launched with, and a plain server restart reconnects to it without a launch.
|
|
180
|
+
Set `stop_hook_bootstrap` in `config.json`: at the top level for every persona, or on a persona to override that default. This compatibility setting defaults to `true`; `false` disables reminders for that persona. A confirmed [reload](reload.md#reload) applies the change immediately and keeps the running session. The setting applies independently to personas sharing a config directory. A reminder already queued before confirmation still follows normal delivery.
|
|
190
181
|
|
|
191
182
|
### Personas that answer without `reply` — opt them out
|
|
192
183
|
|
|
193
|
-
|
|
184
|
+
A persona that answers only with `edit_message`, `react` or another service can receive a reminder after each Slack delivery. Set `stop_hook_bootstrap: false` on that persona and confirm the change.
|
|
194
185
|
|
|
195
186
|
### When it stays silent
|
|
196
187
|
|
|
197
|
-
|
|
188
|
+
A reminder requires an observed `working` → `waiting` transition after delivery in the same session. A first observation of `waiting` alone is insufficient; CSCB does not infer a missed transition. A changed session, a finished or missing row, or persona teardown cancels pending reminder checks. For diagnosis, see [A persona isn't reminded to reply, or is reminded after opting out](../skills/debug-slack-channel-bots/SKILL.md#a-persona-isnt-reminded-to-reply-or-is-reminded-after-opting-out).
|
|
189
|
+
|
|
190
|
+
A pending reminder check expires 24 hours after delivery. A reminder already queued follows normal delivery.
|
package/docs/configuration.md
CHANGED
|
@@ -19,10 +19,10 @@ Postinstall creates a skeleton with an empty persona list, `{"personas": []}`. A
|
|
|
19
19
|
A complete `config.json` in declarative mode (`allow_invited_channels` absent), with three personas:
|
|
20
20
|
|
|
21
21
|
- `planner` receives every message in its home channel `C0123456789` and in the shared channel `C0555555555`. Its permission prompts and notices go to its home channel. It takes no DMs.
|
|
22
|
-
- `reviewer` receives only its @mentions and `@here` / `@channel` broadcasts in the shared channel. It takes DMs, and its prompts and notices go by DM to its contact `U0123456789`. It has its own Claude config directory and turns
|
|
22
|
+
- `reviewer` receives only its @mentions and `@here` / `@channel` broadcasts in the shared channel. It takes DMs, and its prompts and notices go by DM to its contact `U0123456789`. It has its own Claude config directory and turns [Slack reply reminders](bots.md#slack-reply-reminders) off for itself.
|
|
23
23
|
- `helpdesk` is DM-only: it is in no channel and is reached only by DM. Its prompts and notices go by DM to its contact `U0987654321`.
|
|
24
24
|
|
|
25
|
-
`planner` and `helpdesk` inherit the top-level `claude_config_dir`. The server-wide settings
|
|
25
|
+
`planner` and `helpdesk` inherit the top-level `claude_config_dir`. The server-wide settings select queued/delivered reactions and reply splitting. **Custom emoji must exist in the Slack workspace before configuration**; `spinthinking` is custom. Ensure both names below are available, or choose available names; see [Acknowledgement reactions](server-settings.md#acknowledgement-reactions).
|
|
26
26
|
|
|
27
27
|
```json
|
|
28
28
|
{
|
|
@@ -55,7 +55,8 @@ A complete `config.json` in declarative mode (`allow_invited_channels` absent),
|
|
|
55
55
|
"port": 3100,
|
|
56
56
|
"session_restart_delay": 60,
|
|
57
57
|
"claude_config_dir": "~/.claude-bots",
|
|
58
|
-
"
|
|
58
|
+
"queued_ack_reaction": "beehive",
|
|
59
|
+
"ack_reaction": "spinthinking",
|
|
59
60
|
"reply_chunk_limit": 3000, "reply_chunk_mode": "newline"
|
|
60
61
|
}
|
|
61
62
|
```
|
|
@@ -75,7 +76,7 @@ Replace the channel IDs, user IDs and paths with your own. Each persona's creden
|
|
|
75
76
|
| `permission_prompts` | string | in declarative mode, yes | — | Declarative mode only: the persona's **destination**, where its permission prompts and server notices are posted. One of the persona's own channel IDs, or `"dm"`. See [Permission prompts](#permission-prompts). Not read in fungible mode. |
|
|
76
77
|
| `invited.permission_prompts` | string | no | `"dm"` | Fungible mode only: the persona's **destination**, where its permission prompts and server notices are posted. `"dm"` or a Slack channel ID such as `C0123456789`; the channel needs no listing. Not read in declarative mode. See [Permission prompts in fungible mode](channel-modes.md#permission-prompts-in-fungible-mode). |
|
|
77
78
|
| `claude_config_dir` | path | no | top-level `claude_config_dir` | Claude config directory for this persona. See [Next-launch settings](#next-launch-settings). |
|
|
78
|
-
| `stop_hook_bootstrap` | boolean | no | top-level `stop_hook_bootstrap` | Slack
|
|
79
|
+
| `stop_hook_bootstrap` | boolean | no | top-level `stop_hook_bootstrap` | Slack reply reminder switch for this persona. Confirmed changes apply immediately; see [Slack reply reminders](bots.md#slack-reply-reminders). |
|
|
79
80
|
|
|
80
81
|
`dm` is an object holding `enabled` and `contact`: `"dm": { "enabled": true, "contact": "U0123456789" }`. It applies in both modes. `invited` is an object holding `permission_prompts`: `"invited": { "permission_prompts": "C0123456789" }`. A path is absolute, `~` or starts with `~/`. Paths are never redacted: a log line, preview or error that names a path shows it in full, with `~` expanded.
|
|
81
82
|
|
|
@@ -178,7 +179,7 @@ Choose a destination with [channel delivery](#channel-delivery) in mind. If two
|
|
|
178
179
|
|
|
179
180
|
### Next-launch settings
|
|
180
181
|
|
|
181
|
-
`claude_config_dir`
|
|
182
|
+
`claude_config_dir` can be set on a persona or at the top level. A persona's own value wins; the top-level value is the default for every persona without one. Once a change is confirmed, it takes effect at the persona's next launch (see [When next-launch and server-wide changes take effect](reload.md#when-next-launch-and-server-wide-changes-take-effect)).
|
|
182
183
|
|
|
183
184
|
**`claude_config_dir`** is the Claude config directory the persona's instance launches with (`CLAUDE_CONFIG_DIR`), so each persona can authenticate as a different Claude account. Populate each directory before the persona first launches:
|
|
184
185
|
|
|
@@ -190,9 +191,10 @@ Use `--console` instead of `--claudeai` for a Console account. When neither the
|
|
|
190
191
|
- A directory that doesn't exist yet is fine when its parent exists.
|
|
191
192
|
- A persona whose `claude_config_dir` can't be resolved to a real path (for example, a symlink on its path that points to nothing) is held: it closes or never opens its Slack connection and receives nothing, and `server.log` shows `persona-config-dir-unresolvable`. It reconnects once the directory resolves, with no restart.
|
|
192
193
|
- A confirmed change to a persona's effective `claude_config_dir` makes it start a new conversation on the same instance at its next launch. Nothing is deleted: the old conversation stays in the old directory, with prior sessions retained in the same AD life.
|
|
193
|
-
- The Slack Reply Guard never installs its hook in `~/.claude`, so a persona whose directory resolves there gets no reminder (see the debugging skill's [How the guard is installed and what it reads](../skills/debug-slack-channel-bots/SKILL.md#how-the-guard-is-installed-and-what-it-reads)).
|
|
194
194
|
|
|
195
|
-
|
|
195
|
+
### Slack reply reminders
|
|
196
|
+
|
|
197
|
+
**`stop_hook_bootstrap`** turns [Slack reply reminders](bots.md#slack-reply-reminders) on (`true`, the default) or off. This compatibility setting can be set on a persona or at the top level; a persona's own boolean overrides the default. A confirmed change applies immediately to that persona, including when it shares its `claude_config_dir` with others. See [Personas that answer without `reply`](bots.md#personas-that-answer-without-reply--opt-them-out) for when to turn it off.
|
|
196
198
|
|
|
197
199
|
### Load-time rules
|
|
198
200
|
|
package/docs/migration.md
CHANGED
|
@@ -281,7 +281,7 @@ This major version accepts only the persona configuration format. These are the
|
|
|
281
281
|
|
|
282
282
|
1. **Rewrite `config.json` by hand.** A configuration from an earlier major version stops the server at start, with an error that names the offending setting and says the configuration must be converted to personas. Nothing is converted automatically and the file is not changed. Write a `personas` list as described in [Personas (config.json)](configuration.md#personas-configjson); the server-wide settings keep their names. The `debug-slack-channel-bots` skill covers this error under "Pre-persona configuration".
|
|
283
283
|
2. **Pick persona names whose keys don't start with one another.** The configuration check rejects such a pair. If your bots were named `horde`, `horde_admin`, …, don't name a persona `horde` beside `horde_admin`: give the shorter name a suffix, such as `horde_main` (see [Persona name and key](configuration.md#persona-name-and-key)).
|
|
284
|
-
3. **Set the reply settings in `config.json`.**
|
|
284
|
+
3. **Set the reply settings in `config.json`.** Choose top-level `queued_ack_reaction` and `ack_reaction`, or use their defaults after checking both emoji exist in the Slack workspace; custom emoji must exist before configuration. See [Acknowledgement reactions](server-settings.md#acknowledgement-reactions). If you had changed how replies are split, set `reply_chunk_limit` and `reply_chunk_mode` there too.
|
|
285
285
|
4. **Move the tokens into credentials files.** Tokens come only from each persona's credentials file. Create one [credentials file](configuration.md#credentials-files) per persona, then remove any token environment variables you exported for the previous version.
|
|
286
286
|
5. **Give each persona its own Slack app.** Your existing app can serve one persona; create another app for each additional persona. Add the `im:write`, `channels:read` and `groups:read` bot scopes to the existing app and re-install it; the `debug-slack-channel-bots` skill has the steps under "A persona can't open a DM" and "The message archive shows channel IDs".
|
|
287
287
|
6. **Decide who can reach each persona.** In declarative mode, the mode an upgrade starts in, who can reach a persona is decided only by its `channels`, each channel's `delivery` and its `dm.enabled` switch (see [Channel delivery](configuration.md#channel-delivery) and [Direct messages](configuration.md#direct-messages-dmenabled)). Fungible mode changes that (see [Who can reach a persona in fungible mode](channel-modes.md#who-can-reach-a-persona-in-fungible-mode)).
|
|
@@ -294,7 +294,8 @@ Earlier releases ignore this release’s queue and can resume a bot without thei
|
|
|
294
294
|
|
|
295
295
|
1. **Stop sending and empty the queues.** Wait until `server.log` shows every received item delivered. Otherwise explicitly accept that undelivered messages are lost when using the earlier release.
|
|
296
296
|
2. **Disable resume on the current release.** Set `resume_enabled` to `false` in `config.json`, [confirm the pending change](reload.md#confirming-a-change), and verify `config.json.last-applied` holds `resume_enabled: false`. Keep the current server running until this is applied.
|
|
297
|
-
If the target release does not support these settings, first give every persona a valid declarative section (`permission_prompts`, and `channels` unless `dm.enabled` is `true`; see [Load-time rules](configuration.md#load-time-rules)), remove `allow_invited_channels` and every persona’s `invited`, confirm, and ensure the last-applied record holds neither.
|
|
297
|
+
If the target release does not support these settings, first give every persona a valid declarative section (`permission_prompts`, and `channels` unless `dm.enabled` is `true`; see [Load-time rules](configuration.md#load-time-rules)), remove `allow_invited_channels` and every persona’s `invited`, confirm, and ensure the last-applied record holds neither.
|
|
298
|
+
If the target release does not support `queued_ack_reaction`, remove it from `config.json`, confirm while the current server is running, and verify it is absent from `config.json.last-applied`. These precautions do not replace any phase.
|
|
298
299
|
3. **Stop with the bots, then install and pin the earlier build.** Run `claude-slack-channel-bots stop --stop-bots` and verify it succeeds, install the earlier build (for example `bun install -g claude-slack-channel-bots@<version>`), then restore its required Claude Code version pin before starting it.
|
|
299
300
|
4. **Start the earlier release fresh with channels, then restore resume.** Run `claude-slack-channel-bots start`; verify every bot starts a new conversation with that release’s channels launch and no configuration load error. Only then set `resume_enabled` back to its previous value and confirm the change. The earlier build ignores `channel-delivery.json`; leave it in place.
|
|
300
301
|
|
package/docs/reload.md
CHANGED
|
@@ -93,9 +93,9 @@ Paths in the preview are absolute: a `~` in `config.json` is shown expanded. In
|
|
|
93
93
|
| `…; but at that launch it cannot come up: claude_config_dir cannot be resolved to a real path (<errno>)` | A warning after a changed `claude_config_dir`: the new directory can't be resolved, for example a symlink on its path points to nothing. On a changed top-level `claude_config_dir`, the warning names the inheriting personas it stops; on a `DESTRUCTIVE:` line that also changes the directory, it reads `but it cannot come up`. Confirming still applies the change: at the persona's next launch (at once, for a destructive change) it is held, its Slack connection closed, and it comes up once the directory resolves (`persona-config-dir-unresolvable` in `server.log`). Fix the directory, or the setting, before confirming. A directory that doesn't exist yet but whose parent does is fine. |
|
|
94
94
|
| `… could not be checked.` | The server couldn't check whether an added persona can come up, whether a changed `claude_config_dir` can be resolved, or whether a persona is down because of its credentials. `server.log` has a `reload: cannot check …` line; report it as a bug. |
|
|
95
95
|
| `… changed in the declarative section: recorded, with no effect until allow_invited_channels selects declarative mode.` or `… changed in the fungible section: recorded, with no effect until allow_invited_channels selects fungible mode.` | A recorded change: the persona's section not in force changed (`channels` or `permission_prompts` in fungible mode, `invited` in declarative mode). The line names the persona and the changed fields. The change is recorded and has no effect until the switch selects that section. These lines come after every persona line and before the server-wide setting lines. A change made only of recorded changes previews as `no effective change`, followed by its recorded lines. |
|
|
96
|
-
| `server-wide setting allow_invited_channels changed: turns <mode> mode on, applied in place at once, from the next event, tool call, prompt and notice, for …` | The switch. The line names the mode it turns on (`fungible` or `declarative`) and every persona present in both configurations, or ends `; no persona is affected.` when there is none.
|
|
97
|
-
| `server-wide setting … inherited by …` | A changed top-level default. The line lists the personas that inherit it. If none does,
|
|
98
|
-
| `server-wide setting … changed: once applied, it is recorded and takes effect at the next server start after that.` | A setting such as `port` or `
|
|
96
|
+
| `server-wide setting allow_invited_channels changed: turns <mode> mode on, applied in place at once, from the next event, tool call, prompt and notice, for …` | The switch. The line names the mode it turns on (`fungible` or `declarative`) and every persona present in both configurations, or ends `; no persona is affected.` when there is none. It applies at once, with no restart, and every session is kept (see [Channel modes](channel-modes.md#channel-modes)). The inherited `stop_hook_bootstrap` default also applies immediately to reminder checks. |
|
|
97
|
+
| `server-wide setting … inherited by …` | A changed top-level default. The line lists the personas that inherit it. If none does, a `claude_config_dir` change says `no persona inherits it, so no instance is affected`; a `stop_hook_bootstrap` change says `no persona inherits the change, so no reply reminder is affected`. |
|
|
98
|
+
| `server-wide setting … changed: once applied, it is recorded and takes effect at the next server start after that.` | A setting such as `port`, `bind`, `queued_ack_reaction` or `ack_reaction`. It doesn't take effect until the server starts after the change is applied. Outstanding work keeps its original queued emoji; see [Acknowledgement reactions](server-settings.md#acknowledgement-reactions). |
|
|
99
99
|
| `server-wide setting … changed: once applied, it is recorded, and the CLI takes it from the record from then on (the running server does not use it).` | `stop_timeout` or `exit_timeout`. Only the CLI uses them: once the change is applied, the next `stop` or `clean_restart` uses the new value, with no restart needed. |
|
|
100
100
|
| `server-wide setting … changed: once applied, it is recorded, the CLI takes it from the record from then on, and the running server uses it from its next start.` | `agent_director_call_timeout_ms`. Once the change is applied, the next `stop --stop-bots` or `clean_restart` uses the new value; the running server keeps its current value until it next starts. |
|
|
101
101
|
| `INVALID: <error> Nothing will be applied.` | The edited `config.json` is invalid or missing. Fix the file; nothing is applied until it is valid. `server.log` shows a `reload-invalid` line. |
|
|
@@ -109,7 +109,7 @@ To apply a pending change, rename `config.json.pending` to `config.json.apply` i
|
|
|
109
109
|
cd "${SLACK_STATE_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/claude-slack-channel-bots}" && mv config.json.pending config.json.apply
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
Read the preview first. The server picks the confirmation up at its next check, within about 5 seconds; a check still running an earlier apply delays it, so `reload-applied` can come minutes later. It deletes the confirmation and applies it without a restart
|
|
112
|
+
Read the preview first. The server picks the confirmation up at its next check, within about 5 seconds; a check still running an earlier apply delays it, so `reload-applied` can come minutes later. It deletes the confirmation and applies it without a restart. Next-launch and most server-wide settings are recorded and take effect later; the switch `allow_invited_channels` and reminder setting `stop_hook_bootstrap` (own or inherited) apply in place at once (see [What a confirmation applies](#what-a-confirmation-applies)).
|
|
113
113
|
|
|
114
114
|
You can direct an agent to do the rename for you. Nothing has to be computed, copied or typed. A confirmation made while the server is stopped is processed at the first check after the next start.
|
|
115
115
|
|
|
@@ -136,23 +136,23 @@ Each row is one kind of change: what happens once you confirm it, and what happe
|
|
|
136
136
|
| `channels`, `delivery` or `permission_prompts` changes (in declarative mode), `invited.permission_prompts` changes (in fungible mode), or `dm.enabled` or `dm.contact` changes | Applied in place, immediately: from the next event or post. A changed DM contact is used for the next prompt. | Kept |
|
|
137
137
|
| A credentials file's content changes (same path), such as a rotated token | Only that persona reconnects: the new connection opens, then the old one closes. If the new file can't be used or Slack refuses it, the old connection keeps running, `server.log` shows `persona-credentials-change-failed`, and the change stays pending. If Slack can't be reached, the old connection stays in use while the new one retries. A persona that is retrying (Slack unreachable, its working directory unusable, or held for its `claude_config_dir` by `persona-config-dir-unresolvable`) has no connection and retries with the new content. A persona down because of its credentials comes up on the confirmed change, with no restart; if its new file can't be used, it stays down with its usual line, such as `persona-credentials-invalid`, and nothing stays pending. | Kept |
|
|
138
138
|
| `claude_config_dir` changes (the persona's own or inherited) | Recorded. The persona's next launch uses it and starts a new conversation on the same instance when the directory changed: the conversation is not resumed, nothing is deleted, and the old transcript stays in the old directory. | Kept until the next launch |
|
|
139
|
-
| `stop_hook_bootstrap` changes (the persona's own or inherited) |
|
|
139
|
+
| `stop_hook_bootstrap` changes (the persona's own or inherited) | Applied in place, immediately: subsequent reminder checks use the confirmed value. | Kept |
|
|
140
140
|
| A persona is removed | Torn down: its session is ended, with the result checked and retried, and its agent-director row is kept as the old life until agent-director's `expire` removes it, never resumed. Its posted permission prompts stay in Slack, and clicking one has no effect. Until its old instance has ended, no persona is brought up in its working directory (see "A persona waits on an old instance in its working directory" in [Troubleshooting](troubleshooting.md#troubleshooting)). Previewed `DESTRUCTIVE:`. | Retired: never resumed |
|
|
141
141
|
| A persona's `credentials_file` path or `working_directory` changes | Torn down as a removal is (its row kept, never resumed), then brought up fresh from its new entry on the same instance, with no restart: a new conversation, and the new credentials file is read. The new conversation starts only once the old instance has ended, and any other persona in the old working directory waits until then too (see "A persona waits on an old instance in its working directory" in [Troubleshooting](troubleshooting.md#troubleshooting)). Previewed `DESTRUCTIVE:`. See [Destructive changes](#destructive-changes). | Retired: never resumed |
|
|
142
142
|
| A persona's `name` changes | A removal plus an addition: the name sets the key. The old persona is torn down as a removal is (its row kept until agent-director's `expire` removes it, never resumed), and the new one is brought up fresh once the old one's instance has ended (see "A persona waits on an old instance in its working directory" in [Troubleshooting](troubleshooting.md#troubleshooting)). The removal half is previewed `DESTRUCTIVE:` (`DESTRUCTIVE: persona "<old name>" … is removed`), followed by an `… is added` line for the new name. | Retired: never resumed |
|
|
143
143
|
| The switch `allow_invited_channels` changes, turning fungible mode on or off | Applied in place, at once: from the next event, tool call, prompt and notice, with no restart. Each persona serves the channels of the mode turned on (see [Channel modes](channel-modes.md#channel-modes)). While fungible mode is on, stored channel-delivery choices apply; listed `delivery` values are not carried over, so each channel is at `mentions` unless a stored choice applies. | Kept |
|
|
144
144
|
| `channels` or `permission_prompts` changes in fungible mode, or `invited` changes in declarative mode: the section not in force | Recorded, with no effect until the switch selects that section. A confirmation whose only changes are recorded ones logs `reload-noop`. | Kept |
|
|
145
|
-
| A server-wide setting changes, such as `port` | Recorded. The running server keeps its current value, and the next server start uses the recorded one. `allow_invited_channels`
|
|
145
|
+
| A server-wide setting changes, such as `port` | Recorded. The running server keeps its current value, and the next server start uses the recorded one. `allow_invited_channels` and inherited `stop_hook_bootstrap` apply in place at once (see their rows above). `stop_timeout` and `exit_timeout` differ too: only the CLI uses them, and it takes them from the record at once, so the next `stop` or `clean_restart` uses them. `agent_director_call_timeout_ms` differs as well: the CLI takes it from the record at once, and the running server uses it from its next start. | Not affected |
|
|
146
146
|
| A persona is added | Brought up exactly as at start, including the storage check (`jsonl-non-persistent`, see [Startup errors](troubleshooting.md#startup-errors)). If its credentials file or working directory is bad, it logs the same lines as at start and never affects running personas. See "A persona doesn't come up or doesn't answer" in [Troubleshooting](troubleshooting.md#troubleshooting). | New session |
|
|
147
147
|
|
|
148
|
-
- **Inherited defaults.** A changed top-level `claude_config_dir`
|
|
148
|
+
- **Inherited defaults.** A changed top-level `claude_config_dir` takes effect at each inheriting persona's next launch. A changed top-level `stop_hook_bootstrap` applies immediately to each inheriting persona. The preview lists those personas; a persona that sets its own value isn't affected.
|
|
149
149
|
- **Unchanged personas.** A persona whose declaration and credentials are unchanged is not touched.
|
|
150
150
|
- **Several changes to one persona.** A `credentials_file` path or `working_directory` change wins: the persona is brought up fresh from its new entry, which carries its other changes with it. Otherwise each change applies as its row says. The order is teardowns, in-place updates, credentials reconnects, then bring-ups.
|
|
151
151
|
- **Fixed on Slack's side.** A persona down because Slack refused its tokens, fixed in Slack (for example, its app re-installed) with the same tokens, has nothing pending. Re-save its credentials file with any byte change (a trailing newline is enough), wait for the pending change, then confirm it. The `debug-slack-channel-bots` skill (see [Troubleshooting](troubleshooting.md#troubleshooting)) has the details.
|
|
152
152
|
|
|
153
153
|
## When next-launch and server-wide changes take effect
|
|
154
154
|
|
|
155
|
-
A persona's next launch happens when the bot dies (a crash or a failed health check), at a `clean_restart`, at `stop --stop-bots` then `start`, or after a host reboot. A plain `stop` and `start` reconnects to the running instance, which is not a launch. Server-wide settings take effect at the next server start, except the
|
|
155
|
+
A persona's next launch happens when the bot dies (a crash or a failed health check), at a `clean_restart`, at `stop --stop-bots` then `start`, or after a host reboot. A plain `stop` and `start` reconnects to the running instance, which is not a launch. Server-wide settings take effect at the next server start, except `allow_invited_channels` and the inherited `stop_hook_bootstrap` reminder setting: both take effect when the change is confirmed, and need no `clean_restart`. The CLI takes `stop_timeout`, `exit_timeout` and `agent_director_call_timeout_ms` from the record at once; see the table.
|
|
156
156
|
|
|
157
157
|
To make them take effect sooner, wait for the `reload-applied` line, then run `claude-slack-channel-bots clean_restart`. It reads the bot list from the record before it stops the server, so if you run it earlier it can work from the old record. The server comes back on the record, and every bot is relaunched. Conversations resume, except that a changed `claude_config_dir` starts that persona fresh.
|
|
158
158
|
|
|
@@ -91,7 +91,7 @@ The prompt-file path resolves as follows:
|
|
|
91
91
|
|
|
92
92
|
## How fires appear
|
|
93
93
|
|
|
94
|
-
The server directly queues a scheduled fire only for the target persona, as an injected message with no Slack conversation attached. Its `sender` label is `cscb-cron:<prompt-file-basename>` — for a prompt file `standup.md` the sender is `cscb-cron:standup`. This distinguishes a cron tick from a human and from peer-bot traffic.
|
|
94
|
+
The server directly queues a scheduled fire only for the target persona, as an injected message with no Slack conversation attached. Its `sender` label is `cscb-cron:<prompt-file-basename>` — for a prompt file `standup.md` the sender is `cscb-cron:standup`. This distinguishes a cron tick from a human and from peer-bot traffic. Scheduled fires never arm [Slack reply reminders](bots.md#slack-reply-reminders); like every injected message, a fire carries no `via`. Because a fire carries no channel, a prompt file that wants a Slack reply must say where to post it. Each schedule queues its own message independently, so when several schedules match the same minute for the same persona each one is its own queued item.
|
|
95
95
|
|
|
96
96
|
## The cron log
|
|
97
97
|
|
package/docs/server-settings.md
CHANGED
|
@@ -4,13 +4,13 @@ The top-level `config.json` settings that apply to the whole server, the environ
|
|
|
4
4
|
|
|
5
5
|
## Server-wide settings
|
|
6
6
|
|
|
7
|
-
These top-level fields apply to the whole server.
|
|
7
|
+
These top-level fields apply to the whole server. Confirmed changes take effect at the next server start, with exceptions: `allow_invited_channels` applies in place at once; `claude_config_dir` applies at each inheriting persona's next launch; `stop_hook_bootstrap` applies immediately to each inheriting persona; and the CLI reads `stop_timeout`, `exit_timeout` and `agent_director_call_timeout_ms` from the confirmed record at once. See [Reload](reload.md#reload).
|
|
8
8
|
|
|
9
9
|
| Field | Type | Default | Description |
|
|
10
10
|
|---|---|---|---|
|
|
11
11
|
| `bind` | string | `"127.0.0.1"` | Interface the HTTP server binds to. Use `"0.0.0.0"` to expose on all interfaces. Scheduled prompts directly enter the queue and need no HTTP or loopback. [`clear-latch`](cli.md#claude-slack-channel-bots-clear-latch) still needs loopback (the default, or `0.0.0.0`). |
|
|
12
12
|
| `port` | number | `3100` | Port the HTTP server listens on. |
|
|
13
|
-
| `session_restart_delay` | number | `60` | Seconds to wait before auto-restarting a session that crashed or disappeared.
|
|
13
|
+
| `session_restart_delay` | number | `60` | Seconds to wait before auto-restarting a session that crashed or disappeared. Clean exits normally relaunch about 3 s after disconnect, at most once per configured delay. A Slack message triggers reconnect after an MCP disconnect. Set to `0` to disable automatic restarts and the health check's and message-triggered reconnects; messages then wait until reconnect or restart. Agent-director refusal retries still run. Must be non-negative. For recovery notices and timing, see [Troubleshooting](troubleshooting.md#troubleshooting). |
|
|
14
14
|
| `health_check_interval` | number | `120` | Seconds between periodic liveness polls. Set to `0` to disable; the retries of a persona agent-director refused still run. Must be non-negative. |
|
|
15
15
|
| `exit_timeout` | number | `120` | Seconds to wait for a managed Claude Code session to exit gracefully during `clean_restart` or `stop --stop-bots` before force-killing it through agent-director. |
|
|
16
16
|
| `stop_timeout` | number | `30` | Seconds to wait for the server process to exit after `SIGTERM` before escalating to `SIGKILL`. |
|
|
@@ -18,23 +18,36 @@ These top-level fields apply to the whole server. A confirmed change to one take
|
|
|
18
18
|
| `append_system_prompt_file` | string | — | One shared file supplies obedience instructions for every persona: say whose instructions each bot follows here. Its text is composed with CSCB’s tag description for `--append-system-prompt-file`. Missing file silently skipped. See `skills/EXAMPLE_CLAUDE.md` for a template. |
|
|
19
19
|
| `system_prompt_mode` | string | `"append"` | Controls how `append_system_prompt_file` is applied. `"append"`: the custom prompt file is appended on top of `CLAUDE.md` (default, current behavior). `"none"`: the operator file is ignored even if set. CSCB’s tag description is always carried, including in `none` mode. Use `"none"` when the project's `CLAUDE.md` already contains everything the bot needs. |
|
|
20
20
|
| `fresh_system_prompt` | boolean | `true` | When `true` (default), every bot launches with `--system-prompt-snapshot off`, so a restarted or resumed bot loads the current system prompt file and `CLAUDE.md` and keeps its conversation, whatever `system_prompt_mode` is. When `false`, the flag is omitted. A change takes effect at the next server start. Needs a Claude Code that has `--system-prompt-snapshot` (confirmed on 2.1.280); on an older one set it to `false`. A bot keeps the launch arguments it was started with on every resume, so a change reaches a bot only when it next starts a new conversation; a `clean_restart` that resumes conversations does not apply it to those bots. |
|
|
21
|
-
| `cozempic_prescription` | string | `"standard"` |
|
|
21
|
+
| `cozempic_prescription` | string | `"standard"` | Accepted compatibility setting; has no runtime effect. Valid values: `gentle`, `standard`, `aggressive`. |
|
|
22
22
|
| `message_archive_db` | string | — | Path to a SQLite DB where every inbound Slack message is archived in real time. Parent directories are created if missing; schema is initialized on first open. Compatible with the `archive-messages.py` backfill script — both can write concurrently. Each channel's name is looked up through the receiving persona's Slack app, which needs the `channels:read` and `groups:read` bot scopes that the shipped `slack-app-manifest.yml` grants. For channel IDs in place of names, see [The message archive shows channel IDs](../skills/debug-slack-channel-bots/SKILL.md#the-message-archive-shows-channel-ids-re-install-each-app-to-gain-channelsread-and-groupsread). A DM's rows keep the DM's `D…` ID whatever the scopes, as a DM has no name; a group DM's rows keep its ID too. Feature is disabled when absent. |
|
|
23
23
|
| `claude_config_dir` | string | — | Default Claude on-disk config directory for every persona. When a persona has one (its own or this default), its session launches with `CLAUDE_CONFIG_DIR='<resolved-path>'` so the bot authenticates against a specific account. `~` is expanded and the path is resolved to absolute. A persona's own `claude_config_dir` overrides this value. When neither is set, Claude's own default applies. Must be non-empty when set. |
|
|
24
|
-
| `resume_enabled` | boolean | `true` | When `true` (default),
|
|
24
|
+
| `resume_enabled` | boolean | `true` | When `true` (default), startup and runtime recovery attempt to resume the bot's stored conversation, including after a host reboot or pod resume. When `false`, they start a new conversation on the same instance instead; its agent-director life and prior sessions are retained. For failed-resume recovery, see [Troubleshooting](troubleshooting.md#troubleshooting). |
|
|
25
25
|
| `agent_director_poll_interval_ms` | number | `1000` | Poll interval (ms) for the agent-director permission relay tick. Must be a positive integer in `[200, 3_600_000]`. For configuration errors, see [Troubleshooting](troubleshooting.md#troubleshooting). |
|
|
26
26
|
| `agent_director_call_timeout_ms` | number | `60000` | How long (ms) CSCB waits on each agent-director call it makes for its personas, in the server and in `stop --stop-bots` and `clean_restart`. Must be an integer in `[1000, 3_600_000]`; out-of-range values are rejected at load. It should be greater than the need that agent-director's timing settings give; see [Sizing the agent-director call timeout](agent-director.md#sizing-the-agent-director-call-timeout). |
|
|
27
|
-
| `stop_hook_bootstrap` | boolean | `true` | Default for every persona: whether
|
|
27
|
+
| `stop_hook_bootstrap` | boolean | `true` | Default for every persona: whether it gets [Slack reply reminders](bots.md#slack-reply-reminders). A persona's own boolean overrides this value. Confirmed changes apply immediately to inheriting personas and keep their running sessions. Non-boolean values are rejected at load. |
|
|
28
28
|
| `cron_table_path` | string | `~/.config/claude-slack-channel-bots/crontab` | Path to the crontable for the built-in cron scheduler (`cscb_cron`). When absent, it is `crontab` in the state directory, beside `config.json`: `~/.config/claude-slack-channel-bots/crontab` by default, outside `~/.claude`, so a bot's edit is not a write under `~/.claude`, which Claude Code asks the user to approve. A path you set is used as written, with `~` expanded like other path keys: the server never creates its directory or moves a crontable to it. The resolved path is exported into every managed session as `CSCB_CRONTABLE_PATH` so bots can find the crontable and self-schedule (see [Scheduled Prompts](scheduled-prompts.md#scheduled-prompts-cscb_cron)). Must be a non-empty string when set. |
|
|
29
29
|
| `cron_log_path` | string | `<config dir>/cron.log` | Path to the `cscb_cron` log file. Defaults to `cron.log` in the directory of the loaded `config.json`, wherever the crontable is. `~` is expanded like other path keys. Must be a non-empty string when set. |
|
|
30
30
|
| `cron_log_max_bytes` | number | — | Size cap in bytes for the cron log. Once a line takes the log past the cap, the server drops its oldest whole lines until it fits. The newest line is always kept, even when it alone is larger than the cap. Nothing is rotated or archived, so dropped lines are gone. Absent means no cap: the log only grows. Must be a positive integer when set. |
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
31
|
+
| `queued_ack_reaction` | string | `"beehive"` | Emoji name, without colons, added under each receiving persona's own Slack identity after durable acceptance, including while its bot is busy or down. Must be non-empty when set. See [Acknowledgement reactions](#acknowledgement-reactions). |
|
|
32
|
+
| `ack_reaction` | string | `"spinthinking"` | Emoji name, without colons, replacing the queued reaction after transcript receipts confirm the whole message and delivery is durably recorded. Must be non-empty when set. `spinthinking` is a custom emoji; see the prerequisite below. |
|
|
33
|
+
| `reply_chunk_limit` | number | `4000` | Maximum Unicode code points per posted message: the `reply` tool splits longer text into several messages without splitting a surrogate pair. Must be a positive integer. |
|
|
34
|
+
| `reply_chunk_mode` | `"length"` \| `"newline"` | `"newline"` | How the `reply` tool splits text longer than `reply_chunk_limit`. `length`: hard split at the limit. `newline`: prefer the last newline within the limit, keeping that newline in the preceding message; hard split a line that exceeds the limit. Both modes preserve all whitespace: concatenating the posted texts in order reproduces the supplied text exactly. |
|
|
34
35
|
| `allow_invited_channels` | boolean | `false` | Picks the channel mode for every persona. Absent or `false` is **declarative mode**: each persona's `channels` and top-level `permission_prompts` are in force, and its `invited` section is not read. `true` is **fungible mode**: each persona's `channels` and top-level `permission_prompts` are not read, and its `invited` section is in force. A confirmed change applies in place at once and keeps every session. Non-boolean values are rejected at load. See [Channel modes](channel-modes.md#channel-modes), and read [Who can reach a persona in fungible mode](channel-modes.md#who-can-reach-a-persona-in-fungible-mode) before turning it on. |
|
|
35
36
|
|
|
36
37
|
---
|
|
37
38
|
|
|
39
|
+
### Acknowledgement reactions
|
|
40
|
+
|
|
41
|
+
**Custom emoji must exist in the Slack workspace before you configure them.** `spinthinking` is custom; before starting, make sure both selected names are available in each persona's workspace. Omitting either setting selects its default; empty strings and `null` are invalid.
|
|
42
|
+
|
|
43
|
+
CSCB removes the queued reaction before adding the delivered reaction. Setting both names alike keeps one reaction through both stages; dropped messages and injected messages get neither. The first successful `reply` text post or file upload carrying the original `message_id` clears that persona's actual stage without delaying the rest of the reply.
|
|
44
|
+
|
|
45
|
+
Both settings take effect only at server startup after a [confirmed change](reload.md#what-a-confirmation-applies). Outstanding work retains its original queued name across restarts and setting changes, including when queued feedback is restored after reconnect; its delivered stage uses the setting in effect at delivery.
|
|
46
|
+
|
|
47
|
+
A reply before delivery suppresses later stages: early replies are remembered for 24 hours and pending/completed reaction tracking lasts 30 days in memory. Restart loses reply suppression, so already answered retained work can acquire a reaction again. For missing or lingering reactions, see [Troubleshooting](troubleshooting.md#troubleshooting).
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
38
51
|
## Environment Variables
|
|
39
52
|
|
|
40
53
|
Runtime options are read from environment variables. None of them is required. Slack tokens come only from each persona's credentials file (see [Credentials files](configuration.md#credentials-files)). There is no `.env` file — export any of these in your shell profile.
|
package/docs/troubleshooting.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
The `debug-slack-channel-bots` skill has an entry for every persona log class, every `config.json` rejection and each recovery step; run `/debug-slack-channel-bots` from Claude Code for a guided diagnosis. This page lists the common symptoms, then the server log and the startup-error log.
|
|
4
4
|
|
|
5
|
+
**Installation fails or launches stay pending**
|
|
6
|
+
Check Bun, the global package trust, the supported host, and the worker's Claude Code version and launcher. [Full entry](../skills/debug-slack-channel-bots/SKILL.md#symptom-installation-fails-or-launches-stay-pending).
|
|
7
|
+
|
|
5
8
|
**config.json not found**
|
|
6
9
|
With no last-applied record, `start` exits with `missing prerequisite: config.json not found at <path>, and no config.json.last-applied at <path>`. Run `bun src/postinstall.ts` from the installed package directory to create the skeleton, or create the file, and check that `SLACK_STATE_DIR` names the directory you populated. [Full entry](../skills/debug-slack-channel-bots/SKILL.md#symptom-configjson-not-found).
|
|
7
10
|
|
|
@@ -20,6 +23,9 @@ The first start recorded the bad setting, such as `port` or `bind`, before it fa
|
|
|
20
23
|
**A persona doesn't come up or doesn't answer**
|
|
21
24
|
The persona is silent. Accepted messages remain queued while its instance is unavailable. Find its key on its `persona-start` line in `server.log` and read its lines: each names a class (such as `persona-credentials-invalid` or `persona-directory-missing`) and the file or directory to fix. [Full entry](../skills/debug-slack-channel-bots/SKILL.md#symptom-a-persona-doesnt-come-up-or-doesnt-answer).
|
|
22
25
|
|
|
26
|
+
**Acknowledgement reaction missing or stuck**
|
|
27
|
+
Check that both configured emoji exist in the workspace and the confirmed settings took effect at server start; use `message-delivered` to confirm delivery when Slack reaction calls fail. [Full entry](../skills/debug-slack-channel-bots/SKILL.md#symptom-acknowledgement-reaction-missing-or-stuck).
|
|
28
|
+
|
|
23
29
|
**A persona's instance waits at a startup prompt**
|
|
24
30
|
agent-director pre-trusts the working directory during launch. If any startup prompt still needs an answer, attach with `tmux attach -t =slack_bot_<key>` and answer it yourself; CSCB sends no automatic startup answers. [Full entry](../skills/debug-slack-channel-bots/SKILL.md#symptom-a-personas-instance-waits-at-a-startup-prompt).
|
|
25
31
|
|
|
@@ -54,7 +60,7 @@ The bot is stuck on a Claude Code prompt that never reached Slack. A human reads
|
|
|
54
60
|
Restarts back off from `session_restart_delay`, and after 5 failed launches in a row the persona posts `SpawnCapReached` and automatic restarts stop. Fix the cause, then restart the server (`claude-slack-channel-bots stop && claude-slack-channel-bots start`) to clear the limit. Accepted messages remain queued while the persona cannot receive them. [Full entry](../skills/debug-slack-channel-bots/SKILL.md#symptom-session-not-restarting-after-crash).
|
|
55
61
|
|
|
56
62
|
**A persona is retried after agent-director refuses it**
|
|
57
|
-
The server retries the persona on its own (after 30, 60, 120 and 240 s, then every 300 s) and never counts it toward the restart limit. If it never clears, check that `agent-director version` answers and fix agent-director; no server restart is needed. [Full entry](../skills/debug-slack-channel-bots/SKILL.md#symptom-a-persona-is-retried-after-agent-director-refuses-it).
|
|
63
|
+
The server retries the persona on its own (after 30, 60, 120 and 240 s, then every 300 s) and never counts it toward the restart limit. If agent-director reports its installation disappeared, the persona stays in an unknown state until agent-director reports its state again; no kill or relaunch follows from the outage alone. If it never clears, check that `agent-director version` answers and fix agent-director; no server restart is needed. [Full entry](../skills/debug-slack-channel-bots/SKILL.md#symptom-a-persona-is-retried-after-agent-director-refuses-it).
|
|
58
64
|
|
|
59
65
|
**A persona waits on an old instance in its working directory**
|
|
60
66
|
After a removal, rename or path change, or a start whose clean-up could not end an old instance, no persona comes up in that directory until the old instance has ended; the server keeps ending it. If its kill keeps failing, a human follows the "Operator actions" section of agent-director's README. [Full entry](../skills/debug-slack-channel-bots/SKILL.md#symptom-a-persona-waits-on-an-old-instance-in-its-working-directory).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-slack-channel-bots",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Run several Claude Code personas, each its own Slack app, from one server via Socket Mode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,8 +24,7 @@
|
|
|
24
24
|
"docs/server-settings.md",
|
|
25
25
|
"docs/troubleshooting.md",
|
|
26
26
|
"docs/migration.md",
|
|
27
|
-
"skills/"
|
|
28
|
-
"stop-hooks/"
|
|
27
|
+
"skills/"
|
|
29
28
|
],
|
|
30
29
|
"engines": {
|
|
31
30
|
"bun": ">=1.0.21"
|