claude-slack-channel-bots 0.17.1 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +6 -7
  2. package/docs/agent-director.md +2 -2
  3. package/docs/bots.md +32 -35
  4. package/docs/configuration.md +10 -8
  5. package/docs/migration.md +5 -4
  6. package/docs/reload.md +8 -8
  7. package/docs/scheduled-prompts.md +5 -5
  8. package/docs/server-settings.md +21 -8
  9. package/docs/troubleshooting.md +7 -1
  10. package/package.json +2 -3
  11. package/scripts/install-check.ts +5 -5
  12. package/scripts/write-credentials.sh +1 -1
  13. package/skills/debug-slack-channel-bots/SKILL.md +452 -558
  14. package/skills/setup-slack-channel-bots/SKILL.md +14 -5
  15. package/src/ack-tracker.ts +209 -129
  16. package/src/ad-config-file.ts +2 -2
  17. package/src/ad-description-phrases.ts +24 -25
  18. package/src/ad-error-class.ts +28 -30
  19. package/src/ad-phase1-types.ts +4 -5
  20. package/src/ad-settings.ts +36 -37
  21. package/src/ad-version-gate.ts +71 -78
  22. package/src/agent-director-client.ts +16 -16
  23. package/src/agent-director-errors.ts +23 -25
  24. package/src/agent-director-logger.ts +3 -3
  25. package/src/agent-director-startup.ts +22 -24
  26. package/src/agent-director-template.ts +34 -28
  27. package/src/atomic-write.ts +7 -7
  28. package/src/backoff.ts +3 -3
  29. package/src/bot-system-prompt.ts +1 -1
  30. package/src/channel-delivery.ts +71 -72
  31. package/src/checked-kill.ts +54 -54
  32. package/src/clear-latch.ts +6 -6
  33. package/src/cli-teardown.ts +64 -75
  34. package/src/cli.ts +138 -149
  35. package/src/codex-config-keys.ts +2 -2
  36. package/src/config.ts +191 -192
  37. package/src/conflict-latch.ts +114 -119
  38. package/src/cron-bootstrap.ts +2 -2
  39. package/src/cron-dispatch.ts +7 -7
  40. package/src/cron-log.ts +15 -16
  41. package/src/cron-scheduler.ts +10 -11
  42. package/src/crontable.ts +4 -4
  43. package/src/delivery-ad-scope.ts +1 -1
  44. package/src/delivery-constants.ts +1 -2
  45. package/src/delivery-decision.ts +38 -55
  46. package/src/delivery-engine.ts +98 -71
  47. package/src/delivery-intake.ts +5 -5
  48. package/src/delivery-lifecycle.ts +34 -17
  49. package/src/delivery-notices.ts +25 -6
  50. package/src/delivery-policy.ts +9 -9
  51. package/src/delivery-queue.ts +215 -21
  52. package/src/delivery-receipt.ts +9 -9
  53. package/src/health-check.ts +68 -69
  54. package/src/install-check-labels.ts +2 -3
  55. package/src/install-check.ts +9 -11
  56. package/src/install-skill-pointer.ts +2 -2
  57. package/src/interject.ts +2 -2
  58. package/src/invalid-flags-hold.ts +27 -28
  59. package/src/json-position.ts +2 -2
  60. package/src/jsonl-persistence-check.ts +70 -345
  61. package/src/kill-failure-alert.ts +38 -38
  62. package/src/kill-retry.ts +30 -31
  63. package/src/legacy-reply-hook-cleanup.ts +60 -0
  64. package/src/lib.ts +22 -14
  65. package/src/live-row-sequence.ts +146 -150
  66. package/src/liveness-reading.ts +40 -46
  67. package/src/logging.ts +1 -1
  68. package/src/lost-message.ts +9 -10
  69. package/src/message-archive.ts +2 -2
  70. package/src/old-life-wait.ts +32 -33
  71. package/src/outage-state.ts +115 -106
  72. package/src/pane-read.ts +14 -15
  73. package/src/pending-row.ts +122 -133
  74. package/src/permission-click-handler.ts +33 -33
  75. package/src/permission-poller.ts +116 -118
  76. package/src/permission-trail.ts +21 -23
  77. package/src/persona-bringup-controller.ts +48 -48
  78. package/src/persona-bringup.ts +9 -10
  79. package/src/persona-connection-errors.ts +2 -2
  80. package/src/persona-connections.ts +31 -31
  81. package/src/persona-credentials.ts +12 -12
  82. package/src/persona-destination-hold.ts +22 -29
  83. package/src/persona-destination.ts +28 -28
  84. package/src/persona-diagnostics.ts +32 -34
  85. package/src/persona-episodes.ts +118 -126
  86. package/src/persona-event-router.ts +7 -8
  87. package/src/persona-identity.ts +20 -22
  88. package/src/persona-lifecycle.ts +115 -141
  89. package/src/persona-notifier.ts +42 -49
  90. package/src/persona-retry-schedule.ts +9 -9
  91. package/src/persona-routing.ts +61 -50
  92. package/src/persona-serializer.ts +4 -4
  93. package/src/persona-slack-clients.ts +4 -4
  94. package/src/persona-slack-episodes.ts +4 -4
  95. package/src/persona-slack-validation.ts +17 -18
  96. package/src/persona-start.ts +17 -18
  97. package/src/persona-web-api-watch.ts +2 -2
  98. package/src/pid.ts +1 -1
  99. package/src/postinstall.ts +9 -9
  100. package/src/registry.ts +95 -104
  101. package/src/reload-apply.ts +16 -17
  102. package/src/reload-fingerprint.ts +6 -6
  103. package/src/reload-plan.ts +87 -74
  104. package/src/reload-preview-clauses.ts +5 -5
  105. package/src/reload-timer.ts +3 -3
  106. package/src/reload.ts +118 -123
  107. package/src/reply-reminder.ts +129 -0
  108. package/src/restart.ts +389 -612
  109. package/src/retired-keys.ts +110 -113
  110. package/src/row-read-rules.ts +24 -25
  111. package/src/server.ts +504 -586
  112. package/src/session-manager.ts +1943 -2400
  113. package/src/session-transcript.ts +14 -17
  114. package/src/shutdown-deadline.ts +1 -1
  115. package/src/slack-log-redaction.ts +1 -1
  116. package/src/slow-recovery.ts +12 -13
  117. package/src/startup-errors.ts +1 -1
  118. package/src/startup-summary-ending.ts +3 -3
  119. package/src/state-dir-migration.ts +16 -16
  120. package/src/tag-description.ts +1 -1
  121. package/src/tag-wrapper.ts +4 -4
  122. package/src/test-mcp-drop.ts +1 -1
  123. package/src/test-timer-overrides.ts +1 -1
  124. package/src/tokens.ts +1 -1
  125. package/src/unavailable-retry.ts +193 -210
  126. package/src/cozempic.ts +0 -163
  127. package/src/reply-guard-record.ts +0 -105
  128. package/src/stop-hook-bootstrap.ts +0 -808
  129. package/src/trust-bootstrap.ts +0 -172
  130. 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
- **[Bun](https://bun.sh) is required** — it is CSCB's runtime and the interpreter its install and start scripts run under. Install bun before you install CSCB. `npm install -g claude-slack-channel-bots` on a box without bun fails during postinstall (the postinstall script is a bun script).
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
- Bun blocks the lifecycle scripts of untrusted packages, so the postinstall does not run on the plain `install` above — you must trust the package for it to fire. The `-g` flag targets the global install; without it `bun pm trust` looks for a `package.json` in the current directory and errors with `No package.json was found`. (Run `bun pm -g untrusted` to confirm it is listed first.) The postinstall then creates skeleton config files in `~/.config/claude-slack-channel-bots/` (or in `SLACK_STATE_DIR` when it is set and non-empty, the same directory the server reads) and `~/.claude/slack-mcp.json`, and links the `debug-slack-channel-bots` skill into `~/.claude/skills/`. Skip this step and a later `start` fails with `missing prerequisite: config.json`.
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. No version pin or channels permission is required. A Claude Code too old for exec-form hooks (older than 2.1.139) has its hooks ignored, so every launch stays `pending`. The `claude` on the workers' `PATH` must be Claude Code's native binary or a wrapper that `exec`s it: a launcher or shim that runs Claude Code as a child process makes agent-director ignore every hook (`ad.hook.ignored` with the reason `pid_mismatch`), so every launch stays `pending`.
51
- - [`agent-director`](https://github.com/gabemahoney/agent-director) 0.12.0 or later, **installed system-wide** as a prerequisite, like `git` or `docker`. The npm `agent-director` package CSCB depends on is the client only: it finds the system-installed binary at startup and refuses to start when the binary is missing, too old, or unreachable. This release pins the client to `agent-director@0.12.0` and requires the CLI at 0.12.0 or later, installed on the host together with it, and the two are rolled back together; CSCB changes no agent-director code (see "Switching over to agent-director Phase 1" in [Migration](docs/migration.md#migration)). Every agent on the host, with every long-running agent-director process (`agent-director serve` included), is stopped before either binary change (the Phase 1 install, and the rollback's restore of the previous binary) and started again after it. The startup gate refuses a binary below the client's own minimum (`min_binary_version` in the client's `dist/version-floor.json`) or below 0.12.0, and names the required version (see [Startup errors](docs/troubleshooting.md#startup-errors)). To check the install before you start, run `bun run install-check` (see [Checking your agent-director install](docs/agent-director.md#checking-your-agent-director-install)).
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
- If the host is unsupported, the system-installed `agent-director` itself will refuse to install or run; CSCB's startup gate then exits non-zero with one of the `ad-system-install-*` class labels (see [Startup errors](docs/troubleshooting.md#startup-errors)) and writes the failure to `~/.config/claude-slack-channel-bots/startup-errors.log` and stderr. Consult [agent-director's documentation](https://github.com/gabemahoney/agent-director) for the canonical platform support list.
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 the Slack Reply Guard |
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 |
@@ -155,13 +155,13 @@ At agent-director's defaults, `pause` sets the need: its 9 s plus 30 s is 39 s,
155
155
  **The startup warning.** Once per start, right after the server reads agent-director's settings and before any persona is brought up, the server writes one line to `server.log` when the setting is at or below the need:
156
156
 
157
157
  ```text
158
- [slack] agent-director settings: agent_director_call_timeout_ms is <value>, at or below its need of <need> ms (the <verb> ceiling plus the 15000 ms margin): a call can time out while its verb still acts; see the switch-over runbook in docs/migration.md, section "Switching over to agent-director Phase 1" (b.jg5 SRJ-213)
158
+ [slack] agent-director settings: agent_director_call_timeout_ms is <value>, at or below its need of <need> ms (the <verb> ceiling plus the 15000 ms margin): a call can time out while its verb still acts; see the switch-over runbook in docs/migration.md, section "Switching over to agent-director Phase 1"
159
159
  ```
160
160
 
161
161
  `<verb>` names the row whose ceiling sets the need: its verb, or `resume/spawn-with-reuse/plain-spawn` for the launch row those three share. When `[pause] timeout_seconds` holds a value that is not used, the line is this one instead, whatever the setting is:
162
162
 
163
163
  ```text
164
- [slack] agent-director settings: agent_director_call_timeout_ms is <value>; its need is unknown: [pause] timeout_seconds holds <found>, a value that is not used, so pause's wait is not counted and the need without it is <need> ms; see the switch-over runbook in docs/migration.md, section "Switching over to agent-director Phase 1" (b.jg5 SRJ-213)
164
+ [slack] agent-director settings: agent_director_call_timeout_ms is <value>; its need is unknown: [pause] timeout_seconds holds <found>, a value that is not used, so pause's wait is not counted and the need without it is <need> ms; see the switch-over runbook in docs/migration.md, section "Switching over to agent-director Phase 1"
165
165
  ```
166
166
 
167
167
  `<found>` is the number (for example `0`) or the kind of value, never the file's text. At agent-director's defaults, the default `60000` writes no line; a setting of `54000` writes the first line, naming `pause`.
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 the Slack Reply Guard. See also [Scheduled Prompts](scheduled-prompts.md).
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 into several messages by the server-wide `reply_chunk_limit` and `reply_chunk_mode` settings. With `message_id` (the message being answered), removes this persona's acknowledgement reaction from that message. Supports file attachments. |
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); the Slack Reply Guard does not remind it to reply.
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
 
@@ -106,11 +106,17 @@ Each persona's Slack app must have **interactivity enabled** with **Socket Mode*
106
106
 
107
107
  ### AskUserQuestion
108
108
 
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.
109
+ For fresh or reuse Claude launches, the agent-director template denies `AskUserQuestion` (`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
+
111
+ Fresh or reuse Claude launches save the exact `mcp__slack-channel-router__reply` allowance in their launch arguments, so it is retained on resume. Deny rules and managed restrictions still take priority; other tool approvals follow the existing permission relay. Template refreshes preserve this reply rule for subsequent launches.
112
+
113
+ Agent-director resume does not restore the template's `AskUserQuestion` denial or memory-read allowances; only the exact reply allowance is also saved in launch arguments. Independently configured permission rules and managed restrictions still apply.
110
114
 
111
115
  ### Memory-directory reads
112
116
 
113
- 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.
117
+ On fresh or reuse Claude launches, the template also pre-allows each persona to read its own persistent-memory directory, subject to deny rules and managed restrictions. 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.
118
+
119
+ 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 may need permission to read its memory notes, depending on independently configured rules. The rule is scoped to `projects/*/memory/**` only — never the config-dir root, which holds live credentials — so it never pre-authorizes credential reads.
114
120
 
115
121
  ---
116
122
 
@@ -118,6 +124,8 @@ The template also pre-allows each bot to read its own persistent-memory director
118
124
 
119
125
  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
126
 
127
+ 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.
128
+
121
129
  A message from Slack carries these tag attributes:
122
130
 
123
131
  | Attribute | Value |
@@ -137,61 +145,50 @@ Each way a message reaches a persona:
137
145
 
138
146
  | Kind | How it reaches the persona | Tag attributes | `via` | Reminder |
139
147
  |---|---|---|---|---|
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, direct-message wording |
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, direct-mention wording |
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, broadcast wording |
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, shared-channel wording |
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, only-you wording |
148
+ | 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 |
149
+ | 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 |
150
+ | `@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 |
151
+ | 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 |
152
+ | 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
153
  | 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
154
  | `/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
155
 
148
- A "Yes" in the Reminder column means the [Slack Reply Guard](#slack-reply-guard-stop-hook) reminds the persona once if it ends its turn without replying, wherever the reminder is on for that persona. Each `via` has its own reminder wording, stating how the message arrived; that section lists them.
156
+ A "Yes" in the Reminder column means [Slack reply reminders](#slack-reply-reminders) are eligible for that delivery when enabled for the persona.
149
157
 
150
158
  - **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
- - **The persona's own @mention is removed from the text.** Mentions of other personas and `@here` / `@channel` stay.
159
+ - **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
160
  - **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
161
  - **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
162
  - **A persona never receives its own posts.**
155
163
  - **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
164
  - **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
165
  - **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.** A persona gets no reminder when its effective `stop_hook_bootstrap` was `false` at its last launch, when it has no `claude_config_dir` (its own or the top-level one), when that directory resolves to `~/.claude`, or when `jq` is not installed. See [Slack Reply Guard](#slack-reply-guard-stop-hook).
166
+ - **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
167
 
160
- ## Slack Reply Guard (Stop hook)
168
+ ## Slack reply reminders
161
169
 
162
- CSCB ships a Claude Code Stop hook that gives a persona a **one-time, declinable reminder** to answer in Slack. It is on for every persona whose effective `stop_hook_bootstrap` is `true` (the default), except a persona with no `claude_config_dir`, one whose directory resolves to `~/.claude`, or any persona when `jq` is missing (see [When it stays silent](#when-it-stays-silent)). When the messages that started the turn include a message from Slack and the persona ends its turn without calling the `mcp__slack-channel-router__reply` tool, the hook exits `2` and Claude Code shows the persona a reminder. The message's `via` picks the wording:
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:
170
+ 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
171
 
175
172
  ```text
176
- This turn started from a Slack direct message 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.
173
+ 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
174
  ```
178
175
 
179
- When several Slack messages started the turn, the last one picks the wording. A `reply` call after the earliest of them counts as a reply. The persona then continues once: it can reply, or end the turn without replying. That continuation carries `stop_hook_active=true`, which the guard always lets through, so the reminder appears at most once per turn and never loops.
176
+ 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
177
 
181
- CSCB types the tag, including when a message arrives mid-turn or is pasted into the composer. The guard recognises these forms in the transcript.
178
+ Claude reminders require the `reply_tool_preallowed=1` agent-director label; conversations without it receive no new reminders. An existing label alone does not prove that the conversation's saved launch arguments include the reply allowance. Updating the template leaves existing conversations and their saved arguments unchanged; ordinary fresh or reuse launches obtain the current rule, and existing conversations continue receiving ordinary messages.
182
179
 
183
- **Only messages from Slack trigger it.** The guard recognises a message from Slack by its `via` attribute; any `via` value counts. Injected prompts (scheduled prompts and `/interject` messages) carry no `via`, so they never trigger the reminder: no Slack conversation is waiting on them. An injected message that arrives right after a Slack message does not cancel the reminder for the Slack message. [How a persona receives messages](#how-a-persona-receives-messages) lists every kind of message and its `via`.
180
+ 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
181
 
185
182
  ### Turning it on or off
186
183
 
187
- Set `stop_hook_bootstrap` in `config.json`: at the top level for every persona, or on a persona to override the top-level value for that persona alone. It defaults to `true`. Setting `stop_hook_bootstrap: false` on the persona, or inheriting `false` from the top level, is all an opt-out needs, even when the persona shares its `claude_config_dir` with other personas. See [Next-launch settings](configuration.md#next-launch-settings) and [Server-wide settings](server-settings.md#server-wide-settings).
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.
184
+ 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
185
 
191
186
  ### Personas that answer without `reply` — opt them out
192
187
 
193
- The guard only recognises a reply made with `mcp__slack-channel-router__reply`. A persona whose only Slack surface is `edit_message` or `react` ends its turn without that call, so the guard reminds it after every Slack message, costing one useless continuation each time. Set `stop_hook_bootstrap: false` on that persona.
188
+ 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
189
 
195
190
  ### When it stays silent
196
191
 
197
- The guard is fail-open: when anything is missing or goes wrong, the turn ends normally with no reminder. Besides the opt-out, it stays silent for a persona with no `claude_config_dir`, one whose directory resolves to your own `~/.claude` (the server never edits `~/.claude/settings.json`), any persona while `jq` is not installed (installing it later is enough), and when the message's `<channel>` tag, which CSCB types, isn't in the shape the guard expects. After every CSCB or Claude Code upgrade, check the guard end to end with the steps in the debugging skill's ["Checking the guard after an upgrade"](../skills/debug-slack-channel-bots/SKILL.md#checking-the-guard-after-an-upgrade). How the server installs the hook and the per-persona record it reads are in ["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).
192
+ 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).
193
+
194
+ A pending reminder check expires 24 hours after delivery. A reminder already queued follows normal delivery.
@@ -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 the [Slack Reply Guard](bots.md#slack-reply-guard-stop-hook) off for itself.
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 turn on an acknowledgement reaction and set how replies are split.
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
- "ack_reaction": "eyes",
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 Reply Guard switch for this persona. See [Next-launch settings](#next-launch-settings). |
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
 
@@ -112,7 +113,7 @@ The key is used for:
112
113
 
113
114
  **No persona's key may start with another persona's key.** The server rejects such a pair when it checks the configuration (see [Load-time rules](#load-time-rules)), with an error that names both personas and suggests a new name. tmux matches a session target by prefix unless it is written with `=`, so a human's tmux command without `=` for one persona (for example `tmux attach -t slack_bot_dev`) could reach another persona's session. With personas `dev` and `dev_2`, that command could reach `slack_bot_dev_2`.
114
115
 
115
- Give the shorter name a suffix, such as `dev_main` beside `dev_2`, or `horde_main` beside `horde_admin`, or rename either so that neither key starts with the other. Keys that only share a start, such as `dev_a` and `dev_b`, are fine. A name that isn't its own key counts by its key: `"Dev Bot"` (key `dev_bot_…`) can't sit beside `dev` either.
116
+ Give the shorter name a suffix, such as `dev_main` beside `dev_2`, or `team_main` beside `team_admin`, or rename either so that neither key starts with the other. Keys that only share a start, such as `dev_a` and `dev_b`, are fine. A name that isn't its own key counts by its key: `"Dev Bot"` (key `dev_bot_…`) can't sit beside `dev` either.
116
117
 
117
118
  Renaming a persona changes its key, so the old persona is removed and a new one added (see [What a confirmation applies](reload.md#what-a-confirmation-applies)).
118
119
 
@@ -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` and `stop_hook_bootstrap` 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
+ `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
- **`stop_hook_bootstrap`** turns the [Slack Reply Guard](bots.md#slack-reply-guard-stop-hook) reminder on (`true`, the default) or off for the persona. It applies to that persona alone, even when it shares its `claude_config_dir` with other personas. A running bot keeps the value it was launched with until it is relaunched. See [Personas that answer without `reply`](bots.md#personas-that-answer-without-reply--opt-them-out) for when to turn it off.
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
@@ -280,8 +280,8 @@ After upgrading, every bot starts one fresh conversation; memory files are kept.
280
280
  This major version accepts only the persona configuration format. These are the configuration steps of an upgrade from an earlier major version. Do them inside the switch-over runbook, not on their own: steps 1, 2, 3, 5 and 6 (the persona file, names, reply settings, Slack apps and who can reach each persona) at [step 1 of the switch-over](#step-1-check-the-host-and-stage-the-release), and steps 4, 7 and 8 (credentials files, crontable and `/interject` callers) at [step 7 of the switch-over](#step-7-install-the-new-cscb-without-starting-it). At switch-over step 1, these steps are written into the separate staged file, not `config.json`; step 7 puts that file in place as `config.json`. Until the new CSCB's first start, the new CLI keeps using the old state directory, `~/.claude/channels/slack`, so the persona `config.json`, the credentials files and the crontable go there; the first start moves that directory to `~/.config/claude-slack-channel-bots`.
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
- 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`.** Nothing else carries an acknowledgement reaction over: to keep one, set `ack_reaction` as a top-level setting. If you had changed how replies are split, set `reply_chunk_limit` and `reply_chunk_mode` there too. See [Server-wide settings](server-settings.md#server-wide-settings).
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 `team`, `team_admin`, …, don't name a persona `team` beside `team_admin`: give the shorter name a suffix, such as `team_main` (see [Persona name and key](configuration.md#persona-name-and-key)).
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,13 +294,14 @@ 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. These precautions do not replace any phase.
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
 
301
302
  ---
302
303
 
303
- ## Upgrading from pre-Epic-2 (v0.5.x → v0.6.x)
304
+ ## Upgrading from pre-v0.6 (v0.5.x → v0.6.x)
304
305
 
305
306
  If you installed CSCB before v0.6.0 you may have legacy artifacts on disk that are no longer needed. Clean them up by hand; no install step removes them.
306
307
 
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. Unlike other server-wide settings, it applies at once, with no restart, and every session is kept (see [Channel modes](channel-modes.md#channel-modes)). |
97
- | `server-wide setting … inherited by …` | A changed top-level default. The line lists the personas that inherit it. If none does, it says `no persona inherits it, so no instance 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` or `bind`. It doesn't take effect until the server starts after the change is applied. |
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; next-launch and server-wide settings are recorded and take effect later, except the switch `allow_invited_channels`, which applies in place at once (see [What a confirmation applies](#what-a-confirmation-applies)).
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) | Recorded. The persona's next launch uses it. | Kept |
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` is the exception: it is applied in place at once (see the switch's row 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 |
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` or `stop_hook_bootstrap` takes effect at each inheriting persona's next launch. The preview lists those personas; a persona that sets its own value isn't affected.
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 switch `allow_invited_channels`: it takes effect when the change is confirmed, and needs no `clean_restart`. The CLI takes `stop_timeout`, `exit_timeout` and `agent_director_call_timeout_ms` from the record at once; see the table.
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
 
@@ -46,16 +46,16 @@ On first boot the server auto-creates the crontable with this self-documenting h
46
46
  # skipped on its own; sibling lines still schedule.
47
47
  #
48
48
  # Example (every day at 09:00, run grooming-tick.md, target two personas):
49
- # 0 9 * * * /home/horde/prompts/grooming-tick.md planner,reviewer
49
+ # 0 9 * * * ~/prompts/grooming-tick.md planner,reviewer
50
50
  #
51
51
  # Example (every hour, run standup.md, no persona list — NOT delivered yet):
52
- # 0 * * * * /home/horde/prompts/standup.md
52
+ # 0 * * * * ~/prompts/standup.md
53
53
  ```
54
54
 
55
55
  Each schedule is one line of **exactly 5 cron fields**, then the prompt-file path, then an optional comma-separated list of personas, each written as the persona's name or its key:
56
56
 
57
57
  ```
58
- 0 9 * * 1 /home/horde/prompts/weekly-report.md planner,reviewer
58
+ 0 9 * * 1 ~/prompts/weekly-report.md planner,reviewer
59
59
  ```
60
60
 
61
61
  Rules:
@@ -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. The [Slack Reply Guard](bots.md#slack-reply-guard-stop-hook) never reminds a bot to reply to a scheduled prompt: 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.
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
 
@@ -117,7 +117,7 @@ Each load of the crontable, at start and after each edit, writes one line with i
117
117
  Each load also writes one `warn` line for each line with no persona list, naming its line number in the file and its prompt path:
118
118
 
119
119
  ```text
120
- 2026-10-09T15:12:00.002Z cscb-cron:grooming - warn line=34 prompt=/home/horde/prompts/grooming.md undeliverable: this crontable line names no personas, so it will never be delivered in this version …
120
+ 2026-10-09T15:12:00.002Z cscb-cron:grooming - warn line=34 prompt=~/prompts/grooming.md undeliverable: this crontable line names no personas, so it will never be delivered in this version …
121
121
  ```
122
122
 
123
123
  Add a persona list to that line. The warning comes back at every start and every edit until you do; an unchanged crontable is not reloaded, so it is not repeated each minute. To list them in the cron log at its default location:
@@ -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. A confirmed change to one takes effect at the next server start, with four exceptions. `allow_invited_channels` is the one setting a confirmation applies in place, at once. A change to `claude_config_dir` or `stop_hook_bootstrap` takes effect at each inheriting persona's next launch. `stop_timeout` and `exit_timeout` are used only by the CLI, which takes them from the record at once. `agent_director_call_timeout_ms` is used by both: the CLI takes it from the record at once, and the running server uses it from its next start. See [What a confirmation applies](reload.md#what-a-confirmation-applies).
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. A session that ended cleanly (an `agent-director pause`, a `/exit`) is relaunched about 3 s after it disconnects instead, unless the persona's last launch failed or it had such a fast relaunch within the last `session_restart_delay` seconds (see "Session not restarting after crash" in [Troubleshooting](troubleshooting.md#troubleshooting)). Set to `0` to disable auto-restart, clean exits included, and the health check's reconnects (not the retries of a persona agent-director refused; see [Troubleshooting](troubleshooting.md#troubleshooting)); a persona whose session runs but can't receive messages then gets a *Not connected* notice at its destination (see [Troubleshooting](troubleshooting.md#troubleshooting)). A Slack message also triggers reconnect after MCP disconnect; at `0` it does not, and messages wait until reconnect or restart. At any value, a persona reported as `working` that the server can't prove idle for 10 minutes gets a *Not connected* notice. Must be non-negative. |
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"` | Cozempic cleaning intensity before resume. Valid values: `gentle`, `standard`, `aggressive`. Has no effect if cozempic is not installed. |
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), a bot whose session died — including after a host reboot or pod resume — comes back with its prior conversation history intact instead of starting fresh; after 2 resumes in a row that end before the session starts (a transcript Claude Code can't load), it starts fresh instead (see "Session not restarting after crash" under [Troubleshooting](troubleshooting.md#troubleshooting)). When `false`, a bot is never resumed: on startup and on runtime auto-restart it starts a new conversation on the same instance, even when a stored session exists, and its Claude row retains its AD life and prior sessions. For resume failures, see [Troubleshooting](troubleshooting.md#troubleshooting). |
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 the persona gets the Slack Reply Guard reminder (see [Slack Reply Guard (Stop hook)](bots.md#slack-reply-guard-stop-hook)). The value applies per persona, from the persona's first launch after the change is applied (see [Reload](reload.md#reload)). A persona's own `stop_hook_bootstrap` overrides this value. Non-boolean values are rejected at startup. |
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
- | `ack_reaction` | string | — | Emoji name, without colons (for example `"eyes"`), added under each receiving persona's own Slack identity after its bot's transcript confirms the whole message. Queued messages get it after delivery, including messages waiting while the bot is down. Dropped messages and injected messages get none. A successful `reply` with the message's `message_id` removes only that persona's reaction; a reply before delivery suppresses the later reaction. That early-reply memory lasts 24 hours and is forgotten on a server restart, so an already answered message can then keep a reaction. Absent means no acknowledgement. Must be non-empty when set. |
32
- | `reply_chunk_limit` | number | `4000` | Maximum characters per posted message: the `reply` tool splits longer text into several messages. Must be a positive integer. |
33
- | `reply_chunk_mode` | `"length"` \| `"newline"` | `"newline"` | How the `reply` tool splits text longer than `reply_chunk_limit`. `length`: hard split at the limit. `newline`: split at newline boundaries within the limit; a single line longer than the limit is posted whole. |
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.