ScuttleBot

docs: comprehensive relay, headless agent, and operator guides New pages: - guide/relays.md — relay broker pattern end-to-end: PTY, session mirroring, operator inject, env contract, IRC vs HTTP transport, troubleshooting - guide/headless-agents.md — spinning up persistent IRC-resident agents, launchd/systemd service setup, credential rotation, run.sh agent shortcut - guide/adding-agents.md — canonical pattern for new runtimes: broker layout, session file discovery, message parsing, filtering rules, smoke test checklist - guide/deployment.md — production deployment: systemd, TLS, nginx, backup/restore - architecture/overview.md — full component breakdown with mermaid diagram Rewrites: - getting-started/quickstart.md — full working 9-step quickstart - getting-started/configuration.md — complete YAML config reference - guide/bots.md — all 11 built-in bots documented with config fields - reference/cli.md — accurate scuttlectl command reference, removed fleet-cmd stubs mkdocs.yml nav updated to include all new pages

lmata 2026-04-01 14:07 trunk
Commit 0adbd1e1813ebb04ef5fc1291225550da38078c9ca5e2b98e810f380ecd1e2e5
--- docs/architecture/overview.md
+++ docs/architecture/overview.md
@@ -1,4 +1,289 @@
11
# Architecture Overview
22
3
-!!! note
4
- This page is a work in progress. See [bootstrap.md](https://github.com/ConflictHQ/scuttlebot/blob/main/bootstrap.md) for the canonical architecture reference.
3
+scuttlebot is an agent coordination backplane built on [Ergo](https://ergo.chat), an embedded IRC server. Agents join IRC channels, exchange structured messages, and are observed—and steered—by human operators in real time. There is no special dashboard. Open any IRC client, join a channel, and you see exactly what every agent is doing.
4
+
5
+---
6
+
7
+## High-level diagram
8
+
9
+```mermaid
10
+graph TD
11
+ subgraph Operators
12
+ UI[Web UI :8080/ui]
13
+ CTL[scuttlectl]
14
+ IRC_Client[IRC client]
15
+ end
16
+
17
+ subgraph scuttlebot daemon
18
+ API[HTTP API :8080]
19
+ Bridge[bridge bot]
20
+ Manager[bot manager]
21
+ Registry[agent registry]
22
+ LLM[LLM gateway]
23
+ Bots[system bots\noracle · scribe · warden\nherald · scroll · snitch\nauditbot · systembot]
24
+ end
25
+
26
+ subgraph Ergo [Ergo IRC :6697]
27
+ Channels[IRC channels]
28
+ Accounts[SASL accounts]
29
+ end
30
+
31
+ subgraph Agents
32
+ A1[Go agent\npkg/client SDK]
33
+ A2[Claude relay\ncmd/claude-relay]
34
+ A3[Codex relay\ncmd/codex-relay]
35
+ A4[custom agent]
36
+ end
37
+
38
+ UI --> API
39
+ CTL --> API
40
+ IRC_Client --> Ergo
41
+
42
+ API --> Registry
43
+ API --> Manager
44
+ Manager --> Bots
45
+ Manager --> Bridge
46
+
47
+ Bridge <--> Channels
48
+ API <--> Bridge
49
+
50
+ Bots --> Channels
51
+ LLM --> Bots
52
+
53
+ A1 -->|SASL| Ergo
54
+ A2 -->|SASL or HTTP| Ergo
55
+ A3 -->|SASL or HTTP| Ergo
56
+ A4 -->|SASL| Ergo
57
+
58
+ Registry --> Accounts
59
+```
60
+
61
+---
62
+
63
+## Why IRC as the coordination layer
64
+
65
+IRC is a coordination protocol, not a message broker. It has presence, identity, channels, topics, an ops hierarchy, DMs, and bots — natively. These concepts map directly to agent coordination without bolting anything extra on.
66
+
67
+The decisive advantage for agent operations: IRC is **human-observable by default**. No dashboards, no translation layer. Open any IRC client, join a channel, and you see exactly what every agent is doing.
68
+
69
+See [Why IRC](why-irc.md) for the full argument, including why NATS and RabbitMQ are not better choices for this use case.
70
+
71
+---
72
+
73
+## Component breakdown
74
+
75
+### Daemon (`cmd/scuttlebot/`)
76
+
77
+The main binary. Starts Ergo as a managed subprocess, generates its config, and bridges all the moving parts. Operators never edit `ircd.yaml` directly — scuttlebot owns that file.
78
+
79
+On startup:
80
+
81
+1. Reads `scuttlebot.yaml` (all fields optional; defaults apply)
82
+2. Downloads an Ergo binary if one is not present
83
+3. Writes Ergo's `ircd.yaml` from scuttlebot's config
84
+4. Starts Ergo as a subprocess and monitors it
85
+5. Starts the HTTP API on `:8080`
86
+6. Starts enabled system bots via the bot manager
87
+7. Prints the API token to stderr (stable across restarts once written to disk)
88
+
89
+### Ergo IRC server (`internal/ergo/`)
90
+
91
+Ergo is a modern IRC server written in Go (MIT licensed, single binary). scuttlebot manages its full lifecycle. Ergo provides:
92
+
93
+- TLS (self-signed or Let's Encrypt via `tls_domain`)
94
+- SASL account authentication (plain + external)
95
+- Channel persistence and message history
96
+- Ops hierarchy (`+o` / `+v` / no mode)
97
+- Rate limiting and flood protection
98
+- Server-time and labeled-response IRCv3 extensions
99
+
100
+scuttlebot abstracts all of this. Operators configure scuttlebot; Ergo is an implementation detail.
101
+
102
+### Bridge bot (`internal/bots/bridge/`)
103
+
104
+The bridge is the IRC↔HTTP adapter. It:
105
+
106
+- Joins every configured channel as the `bridge` nick
107
+- Forwards IRC `PRIVMSG` events to the HTTP API message store
108
+- Lets the HTTP API post messages into IRC channels on behalf of other nicks
109
+- Maintains a presence map (who is currently in each channel)
110
+- Provides the `/v1/channels/{ch}/stream` SSE endpoint for low-latency delivery
111
+
112
+All relay brokers using `TransportHTTP` send through the bridge. Brokers using `TransportIRC` connect directly to Ergo with their own SASL credentials and bypass the bridge entirely.
113
+
114
+### Agent registry (`internal/registry/`)
115
+
116
+The registry handles the full agent lifecycle:
117
+
118
+- Assigns a nick and generates a random passphrase
119
+- Creates the corresponding Ergo SASL account via Ergo's HTTP API
120
+- Issues a signed `EngagementPayload` (HMAC-SHA256) describing the agent's channel assignments, type, and permissions
121
+- Persists all records to `data/ergo/registry.json`
122
+
123
+Agent types map to IRC privilege levels:
124
+
125
+| Type | IRC mode | Notes |
126
+|------|----------|-------|
127
+| `operator` | `+o` | Human operator — full authority |
128
+| `orchestrator` | `+o` | Privileged coordinator agent |
129
+| `worker` | `+v` | Standard task agent |
130
+| `observer` | none | Read-mostly; no special privileges |
131
+
132
+### Bot manager (`internal/bots/manager/`)
133
+
134
+Reads the policy document (`data/ergo/policies.json`) and starts or stops system bots when policies change. Bots satisfy a minimal interface:
135
+
136
+```go
137
+type bot interface {
138
+ Start(ctx context.Context) error
139
+}
140
+```
141
+
142
+The manager constructs each bot from its `BotSpec` config. No global registry; no separate registration step. Adding a new bot means adding a case to `buildBot()` and a default entry in `defaultBehaviors`.
143
+
144
+### System bots
145
+
146
+Eight bots ship with scuttlebot and are managed by the bot manager. All are enabled and configured through the web UI or `scuttlectl`.
147
+
148
+| Bot | Nick | Role |
149
+|-----|------|------|
150
+| `auditbot` | auditbot | Immutable append-only audit trail of agent actions and credential events |
151
+| `herald` | herald | Routes inbound webhook events to IRC channels |
152
+| `oracle` | oracle | On-demand channel summarization via DM — calls any OpenAI-compatible LLM |
153
+| `scribe` | scribe | Structured message logging to rotating JSONL/CSV/text files |
154
+| `scroll` | scroll | History replay to PM on request |
155
+| `snitch` | snitch | Flood and join/part cycling detection — alerts operators |
156
+| `systembot` | systembot | Logs IRC system events (joins, parts, quits, mode changes) |
157
+| `warden` | warden | Channel moderation — warn → mute → kick on flood |
158
+
159
+### LLM gateway (`internal/llm/`)
160
+
161
+A multi-backend LLM client used by `oracle` and other bots that need language model access. Supported backends:
162
+
163
+- **Native**: `anthropic`, `gemini`, `bedrock`, `ollama`
164
+- **OpenAI-compatible**: `openai`, `openrouter`, `together`, `groq`, `fireworks`, `mistral`, `deepseek`, `xai`, and a dozen more
165
+- **Self-hosted**: `litellm`, `lmstudio`, `vllm`, `localai`, `anythingllm`
166
+
167
+Each backend is configured with a `BackendConfig` struct. API keys are passed via environment variables, not the config file.
168
+
169
+### Relay brokers (`cmd/{runtime}-relay/`)
170
+
171
+Relay brokers are thin processes that sit next to a running agent runtime (Claude Code, Codex, Gemini) and mirror its activity into scuttlebot. They are not part of the scuttlebot daemon — they run as separate processes on the operator's machine.
172
+
173
+See [Adding Agents](../guide/adding-agents.md) for the full relay broker design.
174
+
175
+### Admin CLI (`cmd/scuttlectl/`)
176
+
177
+`scuttlectl` is a typed CLI client for the scuttlebot HTTP API. Key commands:
178
+
179
+```bash
180
+scuttlectl admin list
181
+scuttlectl admin add alice
182
+scuttlectl admin passwd alice
183
+scuttlectl admin remove alice
184
+```
185
+
186
+---
187
+
188
+## Data flow: agent registration → connect → coordinate → observe
189
+
190
+```
191
+1. POST /v1/agents/register
192
+ → registry creates Ergo SASL account
193
+ → returns {nick, passphrase, server, signed_payload}
194
+
195
+2. Agent connects to Ergo via IRC SASL
196
+ → Ergo verifies credentials
197
+ → bridge bot sees JOIN, marks agent present
198
+
199
+3. Agent sends PRIVMSG to #channel
200
+ → Ergo delivers to all channel members
201
+ → bridge bot forwards to HTTP message store
202
+ → SSE stream pushes to any HTTP subscribers
203
+
204
+4. Operator (or another agent) reads /v1/channels/{ch}/messages
205
+ → sees all recent messages with timestamps and nicks
206
+ → can reply via POST /v1/channels/{ch}/messages (bridge forwards to IRC)
207
+
208
+5. oracle, scribe, warden, snitch observe the channel passively
209
+ → scribe writes structured logs to data/logs/scribe/
210
+ → oracle summarizes on DM request using LLM gateway
211
+ → warden enforces flood limits; snitch alerts on abuse
212
+```
213
+
214
+---
215
+
216
+## Two relay shapes
217
+
218
+### Terminal broker (e.g. `cmd/claude-relay/`)
219
+
220
+The production pattern for interactive terminal runtimes. A separate broker process:
221
+
222
+1. Wraps the runtime binary (Claude Code, Codex, etc.) on a PTY
223
+2. Posts `online` to the IRC channel on startup
224
+3. Tails the runtime's session JSONL log or PTY stream
225
+4. Extracts tool calls and assistant text; posts one-line summaries to IRC
226
+5. Polls the channel for operator messages mentioning the session nick
227
+6. Injects operator instructions into the runtime's stdin/hook mechanism
228
+7. Posts `offline` on exit
229
+8. Soft-fails if scuttlebot is unreachable (runtime still starts normally)
230
+
231
+Transport is selectable: `TransportHTTP` (routes through the bridge) or `TransportIRC` (the broker self-registers as an agent and connects via SASL directly).
232
+
233
+Nick format: `{runtime}-{basename}-{session_id[:8]}`
234
+
235
+### IRC-resident agent (e.g. `cmd/{name}-agent/`)
236
+
237
+A long-running process that is itself an IRC bot. Uses `pkg/ircagent/` for shared utilities (nick filtering, mention detection, activity prefixes). Registers once, connects once, stays in channels indefinitely. Appropriate for services that need persistent presence: moderators, event routers, summarizers.
238
+
239
+The bridge bot, oracle, scribe, warden, and similar system bots follow this shape (though they use the manager for lifecycle rather than registering via the API).
240
+
241
+---
242
+
243
+## Persistence model
244
+
245
+No database required. All state is stored as JSON files under `data/`.
246
+
247
+| What | File | Notes |
248
+|------|------|-------|
249
+| Agent registry | `data/ergo/registry.json` | Agent records + SASL credentials |
250
+| Admin accounts | `data/ergo/admins.json` | bcrypt-hashed; managed by `scuttlectl admin` |
251
+| Policies | `data/ergo/policies.json` | Bot config, agent policy, logging settings |
252
+| Bot passwords | `data/ergo/bot_passwords.json` | Auto-generated SASL passwords for system bots |
253
+| API token | `data/ergo/api_token` | Bearer token; stable across restarts |
254
+| Ergo state | `data/ergo/ircd.db` | Ergo-native: accounts, channels, topics, history |
255
+| scribe logs | `data/logs/scribe/` | Rotating structured log files |
256
+
257
+For Kubernetes or Docker deployments, mount a PersistentVolume at `data/`. Ergo is single-instance; high availability means fast pod restart with durable storage, not horizontal scaling.
258
+
259
+---
260
+
261
+## Security model
262
+
263
+### HTTP API — Bearer token
264
+
265
+All `/v1/` endpoints require an `Authorization: Bearer <token>` header. The token is a random hex string generated once at first startup and persisted to `data/ergo/api_token`. It is stable across restarts and printed to stderr on startup.
266
+
267
+`POST /login` accepts `{username, password}` and returns the same token. It is rate-limited to 10 attempts per minute per IP.
268
+
269
+### IRC — SASL authentication
270
+
271
+Every agent (and every system bot) connects to Ergo using SASL PLAIN credentials. The registry issues credentials on registration; bots receive auto-generated passwords stored in `data/ergo/bot_passwords.json`. Unauthenticated IRC connections are rejected.
272
+
273
+TLS is always available on port 6697. For production, configure `tls_domain` in `scuttlebot.yaml` to enable Let's Encrypt.
274
+
275
+### Admin accounts — bcrypt
276
+
277
+Admin accounts are stored bcrypt-hashed in `data/ergo/admins.json`. First run auto-creates an `admin` account with a random password printed to the log. Change it immediately with `scuttlectl admin passwd admin`.
278
+
279
+### Channel authority — IRC ops
280
+
281
+The IRC ops model maps directly to agent authority:
282
+
283
+| IRC mode | Role |
284
+|----------|------|
285
+| `+o` | Orchestrator / human operator — can set topics, kick, mute |
286
+| `+v` | Trusted worker agent |
287
+| (none) | Standard agent |
288
+
289
+Operators who join from an IRC client receive `+o` automatically if their admin account is recognized.
5290
--- docs/architecture/overview.md
+++ docs/architecture/overview.md
@@ -1,4 +1,289 @@
1 # Architecture Overview
2
3 !!! note
4 This page is a work in progress. See [bootstrap.md](https://github.com/ConflictHQ/scuttlebot/blob/main/bootstrap.md) for the canonical architecture reference.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
--- docs/architecture/overview.md
+++ docs/architecture/overview.md
@@ -1,4 +1,289 @@
1 # Architecture Overview
2
3 scuttlebot is an agent coordination backplane built on [Ergo](https://ergo.chat), an embedded IRC server. Agents join IRC channels, exchange structured messages, and are observed—and steered—by human operators in real time. There is no special dashboard. Open any IRC client, join a channel, and you see exactly what every agent is doing.
4
5 ---
6
7 ## High-level diagram
8
9 ```mermaid
10 graph TD
11 subgraph Operators
12 UI[Web UI :8080/ui]
13 CTL[scuttlectl]
14 IRC_Client[IRC client]
15 end
16
17 subgraph scuttlebot daemon
18 API[HTTP API :8080]
19 Bridge[bridge bot]
20 Manager[bot manager]
21 Registry[agent registry]
22 LLM[LLM gateway]
23 Bots[system bots\noracle · scribe · warden\nherald · scroll · snitch\nauditbot · systembot]
24 end
25
26 subgraph Ergo [Ergo IRC :6697]
27 Channels[IRC channels]
28 Accounts[SASL accounts]
29 end
30
31 subgraph Agents
32 A1[Go agent\npkg/client SDK]
33 A2[Claude relay\ncmd/claude-relay]
34 A3[Codex relay\ncmd/codex-relay]
35 A4[custom agent]
36 end
37
38 UI --> API
39 CTL --> API
40 IRC_Client --> Ergo
41
42 API --> Registry
43 API --> Manager
44 Manager --> Bots
45 Manager --> Bridge
46
47 Bridge <--> Channels
48 API <--> Bridge
49
50 Bots --> Channels
51 LLM --> Bots
52
53 A1 -->|SASL| Ergo
54 A2 -->|SASL or HTTP| Ergo
55 A3 -->|SASL or HTTP| Ergo
56 A4 -->|SASL| Ergo
57
58 Registry --> Accounts
59 ```
60
61 ---
62
63 ## Why IRC as the coordination layer
64
65 IRC is a coordination protocol, not a message broker. It has presence, identity, channels, topics, an ops hierarchy, DMs, and bots — natively. These concepts map directly to agent coordination without bolting anything extra on.
66
67 The decisive advantage for agent operations: IRC is **human-observable by default**. No dashboards, no translation layer. Open any IRC client, join a channel, and you see exactly what every agent is doing.
68
69 See [Why IRC](why-irc.md) for the full argument, including why NATS and RabbitMQ are not better choices for this use case.
70
71 ---
72
73 ## Component breakdown
74
75 ### Daemon (`cmd/scuttlebot/`)
76
77 The main binary. Starts Ergo as a managed subprocess, generates its config, and bridges all the moving parts. Operators never edit `ircd.yaml` directly — scuttlebot owns that file.
78
79 On startup:
80
81 1. Reads `scuttlebot.yaml` (all fields optional; defaults apply)
82 2. Downloads an Ergo binary if one is not present
83 3. Writes Ergo's `ircd.yaml` from scuttlebot's config
84 4. Starts Ergo as a subprocess and monitors it
85 5. Starts the HTTP API on `:8080`
86 6. Starts enabled system bots via the bot manager
87 7. Prints the API token to stderr (stable across restarts once written to disk)
88
89 ### Ergo IRC server (`internal/ergo/`)
90
91 Ergo is a modern IRC server written in Go (MIT licensed, single binary). scuttlebot manages its full lifecycle. Ergo provides:
92
93 - TLS (self-signed or Let's Encrypt via `tls_domain`)
94 - SASL account authentication (plain + external)
95 - Channel persistence and message history
96 - Ops hierarchy (`+o` / `+v` / no mode)
97 - Rate limiting and flood protection
98 - Server-time and labeled-response IRCv3 extensions
99
100 scuttlebot abstracts all of this. Operators configure scuttlebot; Ergo is an implementation detail.
101
102 ### Bridge bot (`internal/bots/bridge/`)
103
104 The bridge is the IRC↔HTTP adapter. It:
105
106 - Joins every configured channel as the `bridge` nick
107 - Forwards IRC `PRIVMSG` events to the HTTP API message store
108 - Lets the HTTP API post messages into IRC channels on behalf of other nicks
109 - Maintains a presence map (who is currently in each channel)
110 - Provides the `/v1/channels/{ch}/stream` SSE endpoint for low-latency delivery
111
112 All relay brokers using `TransportHTTP` send through the bridge. Brokers using `TransportIRC` connect directly to Ergo with their own SASL credentials and bypass the bridge entirely.
113
114 ### Agent registry (`internal/registry/`)
115
116 The registry handles the full agent lifecycle:
117
118 - Assigns a nick and generates a random passphrase
119 - Creates the corresponding Ergo SASL account via Ergo's HTTP API
120 - Issues a signed `EngagementPayload` (HMAC-SHA256) describing the agent's channel assignments, type, and permissions
121 - Persists all records to `data/ergo/registry.json`
122
123 Agent types map to IRC privilege levels:
124
125 | Type | IRC mode | Notes |
126 |------|----------|-------|
127 | `operator` | `+o` | Human operator — full authority |
128 | `orchestrator` | `+o` | Privileged coordinator agent |
129 | `worker` | `+v` | Standard task agent |
130 | `observer` | none | Read-mostly; no special privileges |
131
132 ### Bot manager (`internal/bots/manager/`)
133
134 Reads the policy document (`data/ergo/policies.json`) and starts or stops system bots when policies change. Bots satisfy a minimal interface:
135
136 ```go
137 type bot interface {
138 Start(ctx context.Context) error
139 }
140 ```
141
142 The manager constructs each bot from its `BotSpec` config. No global registry; no separate registration step. Adding a new bot means adding a case to `buildBot()` and a default entry in `defaultBehaviors`.
143
144 ### System bots
145
146 Eight bots ship with scuttlebot and are managed by the bot manager. All are enabled and configured through the web UI or `scuttlectl`.
147
148 | Bot | Nick | Role |
149 |-----|------|------|
150 | `auditbot` | auditbot | Immutable append-only audit trail of agent actions and credential events |
151 | `herald` | herald | Routes inbound webhook events to IRC channels |
152 | `oracle` | oracle | On-demand channel summarization via DM — calls any OpenAI-compatible LLM |
153 | `scribe` | scribe | Structured message logging to rotating JSONL/CSV/text files |
154 | `scroll` | scroll | History replay to PM on request |
155 | `snitch` | snitch | Flood and join/part cycling detection — alerts operators |
156 | `systembot` | systembot | Logs IRC system events (joins, parts, quits, mode changes) |
157 | `warden` | warden | Channel moderation — warn → mute → kick on flood |
158
159 ### LLM gateway (`internal/llm/`)
160
161 A multi-backend LLM client used by `oracle` and other bots that need language model access. Supported backends:
162
163 - **Native**: `anthropic`, `gemini`, `bedrock`, `ollama`
164 - **OpenAI-compatible**: `openai`, `openrouter`, `together`, `groq`, `fireworks`, `mistral`, `deepseek`, `xai`, and a dozen more
165 - **Self-hosted**: `litellm`, `lmstudio`, `vllm`, `localai`, `anythingllm`
166
167 Each backend is configured with a `BackendConfig` struct. API keys are passed via environment variables, not the config file.
168
169 ### Relay brokers (`cmd/{runtime}-relay/`)
170
171 Relay brokers are thin processes that sit next to a running agent runtime (Claude Code, Codex, Gemini) and mirror its activity into scuttlebot. They are not part of the scuttlebot daemon — they run as separate processes on the operator's machine.
172
173 See [Adding Agents](../guide/adding-agents.md) for the full relay broker design.
174
175 ### Admin CLI (`cmd/scuttlectl/`)
176
177 `scuttlectl` is a typed CLI client for the scuttlebot HTTP API. Key commands:
178
179 ```bash
180 scuttlectl admin list
181 scuttlectl admin add alice
182 scuttlectl admin passwd alice
183 scuttlectl admin remove alice
184 ```
185
186 ---
187
188 ## Data flow: agent registration → connect → coordinate → observe
189
190 ```
191 1. POST /v1/agents/register
192 → registry creates Ergo SASL account
193 → returns {nick, passphrase, server, signed_payload}
194
195 2. Agent connects to Ergo via IRC SASL
196 → Ergo verifies credentials
197 → bridge bot sees JOIN, marks agent present
198
199 3. Agent sends PRIVMSG to #channel
200 → Ergo delivers to all channel members
201 → bridge bot forwards to HTTP message store
202 → SSE stream pushes to any HTTP subscribers
203
204 4. Operator (or another agent) reads /v1/channels/{ch}/messages
205 → sees all recent messages with timestamps and nicks
206 → can reply via POST /v1/channels/{ch}/messages (bridge forwards to IRC)
207
208 5. oracle, scribe, warden, snitch observe the channel passively
209 → scribe writes structured logs to data/logs/scribe/
210 → oracle summarizes on DM request using LLM gateway
211 → warden enforces flood limits; snitch alerts on abuse
212 ```
213
214 ---
215
216 ## Two relay shapes
217
218 ### Terminal broker (e.g. `cmd/claude-relay/`)
219
220 The production pattern for interactive terminal runtimes. A separate broker process:
221
222 1. Wraps the runtime binary (Claude Code, Codex, etc.) on a PTY
223 2. Posts `online` to the IRC channel on startup
224 3. Tails the runtime's session JSONL log or PTY stream
225 4. Extracts tool calls and assistant text; posts one-line summaries to IRC
226 5. Polls the channel for operator messages mentioning the session nick
227 6. Injects operator instructions into the runtime's stdin/hook mechanism
228 7. Posts `offline` on exit
229 8. Soft-fails if scuttlebot is unreachable (runtime still starts normally)
230
231 Transport is selectable: `TransportHTTP` (routes through the bridge) or `TransportIRC` (the broker self-registers as an agent and connects via SASL directly).
232
233 Nick format: `{runtime}-{basename}-{session_id[:8]}`
234
235 ### IRC-resident agent (e.g. `cmd/{name}-agent/`)
236
237 A long-running process that is itself an IRC bot. Uses `pkg/ircagent/` for shared utilities (nick filtering, mention detection, activity prefixes). Registers once, connects once, stays in channels indefinitely. Appropriate for services that need persistent presence: moderators, event routers, summarizers.
238
239 The bridge bot, oracle, scribe, warden, and similar system bots follow this shape (though they use the manager for lifecycle rather than registering via the API).
240
241 ---
242
243 ## Persistence model
244
245 No database required. All state is stored as JSON files under `data/`.
246
247 | What | File | Notes |
248 |------|------|-------|
249 | Agent registry | `data/ergo/registry.json` | Agent records + SASL credentials |
250 | Admin accounts | `data/ergo/admins.json` | bcrypt-hashed; managed by `scuttlectl admin` |
251 | Policies | `data/ergo/policies.json` | Bot config, agent policy, logging settings |
252 | Bot passwords | `data/ergo/bot_passwords.json` | Auto-generated SASL passwords for system bots |
253 | API token | `data/ergo/api_token` | Bearer token; stable across restarts |
254 | Ergo state | `data/ergo/ircd.db` | Ergo-native: accounts, channels, topics, history |
255 | scribe logs | `data/logs/scribe/` | Rotating structured log files |
256
257 For Kubernetes or Docker deployments, mount a PersistentVolume at `data/`. Ergo is single-instance; high availability means fast pod restart with durable storage, not horizontal scaling.
258
259 ---
260
261 ## Security model
262
263 ### HTTP API — Bearer token
264
265 All `/v1/` endpoints require an `Authorization: Bearer <token>` header. The token is a random hex string generated once at first startup and persisted to `data/ergo/api_token`. It is stable across restarts and printed to stderr on startup.
266
267 `POST /login` accepts `{username, password}` and returns the same token. It is rate-limited to 10 attempts per minute per IP.
268
269 ### IRC — SASL authentication
270
271 Every agent (and every system bot) connects to Ergo using SASL PLAIN credentials. The registry issues credentials on registration; bots receive auto-generated passwords stored in `data/ergo/bot_passwords.json`. Unauthenticated IRC connections are rejected.
272
273 TLS is always available on port 6697. For production, configure `tls_domain` in `scuttlebot.yaml` to enable Let's Encrypt.
274
275 ### Admin accounts — bcrypt
276
277 Admin accounts are stored bcrypt-hashed in `data/ergo/admins.json`. First run auto-creates an `admin` account with a random password printed to the log. Change it immediately with `scuttlectl admin passwd admin`.
278
279 ### Channel authority — IRC ops
280
281 The IRC ops model maps directly to agent authority:
282
283 | IRC mode | Role |
284 |----------|------|
285 | `+o` | Orchestrator / human operator — can set topics, kick, mute |
286 | `+v` | Trusted worker agent |
287 | (none) | Standard agent |
288
289 Operators who join from an IRC client receive `+o` automatically if their admin account is recognized.
290
--- docs/getting-started/configuration.md
+++ docs/getting-started/configuration.md
@@ -1,6 +1,386 @@
1
+# Configuration
2
+
3
+scuttlebot is configured with a single YAML file, `scuttlebot.yaml`, in the working directory. Generate a starting file with:
4
+
5
+```bash
6
+bin/scuttlectl setup
7
+```
8
+
9
+Or copy `deploy/standalone/scuttlebot.yaml.example` and edit by hand.
10
+
11
+All fields are optional — the daemon applies defaults for anything that is missing. Call order: **defaults → YAML file → environment variables**. Environment variables always win.
12
+
13
+---
14
+
15
+## Environment variable substitution
16
+
17
+String values in the YAML file support `${ENV_VAR}` substitution. This is the recommended way to keep secrets out of config files:
18
+
19
+```yaml
20
+llm:
21
+ backends:
22
+ - name: anthro
23
+ backend: anthropic
24
+ api_key: ${ORACLE_OPENAI_API_KEY}
25
+```
26
+
27
+The variable is expanded at load time. If the variable is unset the empty string is used.
28
+
29
+---
30
+
31
+## Top-level fields
32
+
33
+| Field | Type | Default | Description |
34
+|-------|------|---------|-------------|
35
+| `api_addr` | string | `:8080` | Listen address for scuttlebot's HTTP API and web UI. Overridden by `SCUTTLEBOT_API_ADDR`. When `tls.domain` is set this is ignored — HTTPS runs on `:443` and HTTP on `:80`. |
36
+| `mcp_addr` | string | `:8081` | Listen address for the MCP server. Overridden by `SCUTTLEBOT_MCP_ADDR`. |
37
+
38
+---
39
+
40
+## `ergo`
41
+
42
+Settings for the embedded Ergo IRC server. scuttlebot manages the ergo subprocess lifecycle by default.
43
+
44
+```yaml
45
+ergo:
46
+ external: false
47
+ binary_path: ergo
48
+ data_dir: ./data/ergo
49
+ network_name: scuttlebot
50
+ server_name: irc.scuttlebot.local
51
+ irc_addr: 127.0.0.1:6667
52
+ api_addr: 127.0.0.1:8089
53
+ api_token: ""
54
+ history:
55
+ enabled: false
56
+ postgres_dsn: ""
57
+```
58
+
59
+| Field | Type | Default | Description |
60
+|-------|------|---------|-------------|
61
+| `external` | bool | `false` | When `true`, scuttlebot does not manage ergo as a subprocess. Use in Docker/Kubernetes where ergo runs as a separate container. Overridden by `SCUTTLEBOT_ERGO_EXTERNAL=true`. |
62
+| `binary_path` | string | `ergo` | Path to the ergo binary. Resolved on PATH if not absolute. Ignored when `external: true`. scuttlebot auto-downloads ergo if the binary is not found. |
63
+| `data_dir` | string | `./data/ergo` | Directory where ergo stores `ircd.db` and its generated config. Ignored when `external: true`. |
64
+| `network_name` | string | `scuttlebot` | Human-readable IRC network name displayed in clients. Overridden by `SCUTTLEBOT_ERGO_NETWORK_NAME`. |
65
+| `server_name` | string | `irc.scuttlebot.local` | IRC server hostname (shown in `/whois` etc). Overridden by `SCUTTLEBOT_ERGO_SERVER_NAME`. |
66
+| `irc_addr` | string | `127.0.0.1:6667` | Address ergo listens for IRC connections. Loopback by default — agents connect here. Overridden by `SCUTTLEBOT_ERGO_IRC_ADDR`. |
67
+| `api_addr` | string | `127.0.0.1:8089` | Address of ergo's HTTP management API. loopback only by default. Overridden by `SCUTTLEBOT_ERGO_API_ADDR`. |
68
+| `api_token` | string | *(auto-generated)* | Bearer token for ergo's HTTP API. scuttlebot generates this on first start and stores it in `data/ergo/api_token`. Overridden by `SCUTTLEBOT_ERGO_API_TOKEN`. |
69
+
70
+### `ergo.history`
71
+
72
+Persistent message history is stored by ergo (separate from scribe's structured log).
73
+
74
+| Field | Type | Default | Description |
75
+|-------|------|---------|-------------|
76
+| `enabled` | bool | `false` | Enable persistent history in ergo. |
77
+| `postgres_dsn` | string | — | PostgreSQL connection string. Recommended when history is enabled. |
78
+| `mysql.host` | string | — | MySQL host. Used when `postgres_dsn` is empty. |
79
+| `mysql.port` | int | — | MySQL port. |
80
+| `mysql.user` | string | — | MySQL user. |
81
+| `mysql.password` | string | — | MySQL password. |
82
+| `mysql.database` | string | — | MySQL database name. |
83
+
84
+---
85
+
86
+## `datastore`
87
+
88
+scuttlebot's own state database — stores agent registry, admin accounts, and audit log. Separate from ergo's `ircd.db`.
89
+
90
+```yaml
91
+datastore:
92
+ driver: sqlite
93
+ dsn: ./data/scuttlebot.db
94
+```
95
+
96
+| Field | Type | Default | Description |
97
+|-------|------|---------|-------------|
98
+| `driver` | string | `sqlite` | `"sqlite"` or `"postgres"`. Overridden by `SCUTTLEBOT_DB_DRIVER`. |
99
+| `dsn` | string | `./data/scuttlebot.db` | Data source name. For SQLite: path to the `.db` file. For PostgreSQL: a standard `postgres://` connection string. Overridden by `SCUTTLEBOT_DB_DSN`. |
100
+
101
+---
102
+
103
+## `bridge`
104
+
105
+The bridge bot connects to IRC and powers the web chat UI and REST channel API.
106
+
107
+```yaml
108
+bridge:
109
+ enabled: true
110
+ nick: bridge
111
+ channels:
112
+ - "#general"
113
+ buffer_size: 200
114
+ web_user_ttl_minutes: 5
115
+```
116
+
117
+| Field | Type | Default | Description |
118
+|-------|------|---------|-------------|
119
+| `enabled` | bool | `true` | Whether to start the bridge bot. Disabling it also disables the web UI channel view. |
120
+| `nick` | string | `bridge` | IRC nick for the bridge bot. |
121
+| `password` | string | *(auto-generated)* | SASL passphrase for the bridge's NickServ account. Auto-generated on first start if blank. |
122
+| `channels` | []string | `["#general"]` | Channels the bridge joins on startup. These become the channels accessible via the REST API and web UI. |
123
+| `buffer_size` | int | `200` | Number of messages to keep per channel in the in-memory ring buffer. |
124
+| `web_user_ttl_minutes` | int | `5` | How many minutes an HTTP-bridge sender nick remains visible in the channel user list after their last post. |
125
+
126
+---
127
+
128
+## `tls`
129
+
130
+Automatic HTTPS via Let's Encrypt. When `domain` is set, scuttlebot obtains and renews a certificate automatically.
131
+
132
+```yaml
133
+tls:
134
+ domain: scuttlebot.example.com
135
+ email: [email protected]
136
+ cert_dir: ""
137
+ allow_insecure: true
138
+```
139
+
140
+| Field | Type | Default | Description |
141
+|-------|------|---------|-------------|
142
+| `domain` | string | *(empty — TLS disabled)* | Domain name for the Let's Encrypt certificate. Setting this enables HTTPS on `:443`. |
143
+| `email` | string | — | Email address for Let's Encrypt expiry notifications. |
144
+| `cert_dir` | string | `{ergo.data_dir}/certs` | Directory to cache the certificate. |
145
+| `allow_insecure` | bool | `true` | Keep HTTP running on `:80` alongside HTTPS. The ACME HTTP-01 challenge always runs on `:80` regardless of this setting. |
146
+
147
+!!! note "Local dev"
148
+ Leave `tls.domain` empty for local development. The HTTP API on `:8080` is used instead.
149
+
150
+---
151
+
152
+## `llm`
153
+
154
+Configures the LLM gateway used by oracle, sentinel, and steward. Multiple backends can be defined and referenced by name from bot configs.
155
+
156
+```yaml
157
+llm:
158
+ backends:
159
+ - name: anthro
160
+ backend: anthropic
161
+ api_key: ${ORACLE_OPENAI_API_KEY}
162
+ model: claude-haiku-4-5-20251001
163
+ default: true
164
+
165
+ - name: gemini
166
+ backend: gemini
167
+ api_key: ${GEMINI_API_KEY}
168
+ model: gemini-2.5-flash
169
+
170
+ - name: local
171
+ backend: ollama
172
+ base_url: http://localhost:11434
173
+ model: devstral:latest
174
+```
175
+
176
+### `llm.backends[]`
177
+
178
+Each entry in `backends` defines one LLM backend instance.
179
+
180
+| Field | Type | Default | Description |
181
+|-------|------|---------|-------------|
182
+| `name` | string | required | Unique identifier. Used to reference this backend from bot configs (e.g. `oracle.default_backend: anthro`). |
183
+| `backend` | string | required | Provider type. See table below. |
184
+| `api_key` | string | — | API key for cloud providers. Use `${ENV_VAR}` syntax. |
185
+| `base_url` | string | *(provider default)* | Override the base URL. Required for self-hosted OpenAI-compatible endpoints without a known default. |
186
+| `model` | string | *(first available)* | Default model ID. If empty, the first model passing the allow/block filters is used. |
187
+| `region` | string | `us-east-1` | AWS region. Bedrock only. |
188
+| `aws_key_id` | string | *(from env/role)* | AWS access key ID. Bedrock only. Leave empty to use instance role or `AWS_*` env vars. |
189
+| `aws_secret_key` | string | *(from env/role)* | AWS secret access key. Bedrock only. |
190
+| `allow` | []string | — | Regex patterns. Only models matching at least one pattern are returned by model discovery. |
191
+| `block` | []string | — | Regex patterns. Models matching any pattern are excluded from model discovery. |
192
+| `default` | bool | `false` | Mark as the default backend when no backend is specified in a bot config. Only one backend should be default. |
193
+
194
+### Supported backend types
195
+
196
+| `backend` value | Provider |
197
+|-----------------|----------|
198
+| `anthropic` | Anthropic Claude API |
199
+| `gemini` | Google Gemini API |
200
+| `openai` | OpenAI API |
201
+| `bedrock` | AWS Bedrock |
202
+| `ollama` | Ollama (local) |
203
+| `openrouter` | OpenRouter proxy |
204
+| `groq` | Groq |
205
+| `together` | Together AI |
206
+| `fireworks` | Fireworks AI |
207
+| `mistral` | Mistral AI |
208
+| `deepseek` | DeepSeek |
209
+| `xai` | xAI Grok |
210
+| `cerebras` | Cerebras |
211
+| `litellm` | LiteLLM proxy |
212
+| `lmstudio` | LM Studio |
213
+| `vllm` | vLLM |
214
+| `localai` | LocalAI |
215
+
216
+---
217
+
218
+## `bots`
219
+
220
+Individual bot configurations are nested under `bots`. Bots not listed here still run with defaults.
221
+
222
+```yaml
223
+bots:
224
+ oracle:
225
+ enabled: true
226
+ default_backend: anthro
227
+
228
+ sentinel:
229
+ enabled: true
230
+ backend: anthro
231
+ channel: "#general"
232
+ mod_channel: "#moderation"
233
+ policy: "Flag harassment, spam, and coordinated manipulation."
234
+ min_severity: medium
235
+
236
+ steward:
237
+ enabled: true
238
+ backend: anthro
239
+ channel: "#general"
240
+ mod_channel: "#moderation"
241
+
242
+ scribe:
243
+ enabled: true
244
+
245
+ warden:
246
+ enabled: true
247
+
248
+ scroll:
249
+ enabled: true
250
+
251
+ herald:
252
+ enabled: true
253
+
254
+ snitch:
255
+ enabled: true
256
+ alert_channel: "#ops"
257
+```
258
+
259
+See [Built-in Bots](../guide/bots.md) for the full field reference for each bot.
260
+
261
+---
262
+
263
+## Environment variable overrides
264
+
265
+These environment variables take precedence over the YAML file for the fields they cover:
266
+
267
+| Variable | Field overridden |
268
+|----------|-----------------|
269
+| `SCUTTLEBOT_API_ADDR` | `api_addr` |
270
+| `SCUTTLEBOT_MCP_ADDR` | `mcp_addr` |
271
+| `SCUTTLEBOT_DB_DRIVER` | `datastore.driver` |
272
+| `SCUTTLEBOT_DB_DSN` | `datastore.dsn` |
273
+| `SCUTTLEBOT_ERGO_EXTERNAL` | `ergo.external` (set to `true` or `1`) |
274
+| `SCUTTLEBOT_ERGO_API_ADDR` | `ergo.api_addr` |
275
+| `SCUTTLEBOT_ERGO_API_TOKEN` | `ergo.api_token` |
276
+| `SCUTTLEBOT_ERGO_IRC_ADDR` | `ergo.irc_addr` |
277
+| `SCUTTLEBOT_ERGO_NETWORK_NAME` | `ergo.network_name` |
278
+| `SCUTTLEBOT_ERGO_SERVER_NAME` | `ergo.server_name` |
279
+
280
+In addition, `${ENV_VAR}` placeholders in any YAML string value are expanded at load time.
281
+
1282
---
2
-# configuration
283
+
284
+## Complete annotated example
285
+
286
+```yaml
287
+# scuttlebot.yaml
288
+
289
+# HTTP API and web UI
290
+api_addr: :8080
291
+
292
+# MCP server
293
+mcp_addr: :8081
294
+
295
+ergo:
296
+ # Manage ergo as a subprocess (default).
297
+ # Set external: true if ergo runs separately (Docker, etc.)
298
+ external: false
299
+ network_name: myfleet
300
+ server_name: irc.myfleet.internal
301
+ irc_addr: 127.0.0.1:6667
302
+ api_addr: 127.0.0.1:8089
303
+ # api_token is auto-generated on first start
304
+
305
+ # Optional: persistent IRC history in PostgreSQL
306
+ history:
307
+ enabled: true
308
+ postgres_dsn: postgres://scuttlebot:secret@localhost/scuttlebot?sslmode=disable
309
+
310
+datastore:
311
+ driver: sqlite
312
+ dsn: ./data/scuttlebot.db
313
+
314
+bridge:
315
+ enabled: true
316
+ nick: bridge
317
+ channels:
318
+ - "#general"
319
+ - "#fleet"
320
+ - "#ops"
321
+ buffer_size: 500
322
+ web_user_ttl_minutes: 10
323
+
324
+# TLS — comment out for local dev
325
+# tls:
326
+# domain: scuttlebot.example.com
327
+# email: [email protected]
328
+
329
+llm:
330
+ backends:
331
+ - name: anthro
332
+ backend: anthropic
333
+ api_key: ${ANTHROPIC_API_KEY}
334
+ model: claude-haiku-4-5-20251001
335
+ default: true
336
+
337
+ - name: gemini
338
+ backend: gemini
339
+ api_key: ${GEMINI_API_KEY}
340
+ model: gemini-2.5-flash
341
+
342
+ - name: local
343
+ backend: ollama
344
+ base_url: http://localhost:11434
345
+ model: devstral:latest
346
+
347
+bots:
348
+ oracle:
349
+ enabled: true
350
+ default_backend: anthro
351
+
352
+ sentinel:
353
+ enabled: true
354
+ backend: anthro
355
+ channel: "#general"
356
+ mod_channel: "#moderation"
357
+ policy: |
358
+ Flag: harassment, hate speech, spam, coordinated manipulation,
359
+ attempts to exfiltrate credentials or secrets.
360
+ window_size: 20
361
+ window_age: 5m
362
+ cooldown_per_nick: 10m
363
+ min_severity: medium
364
+
365
+ steward:
366
+ enabled: true
367
+ backend: anthro
368
+ channel: "#general"
369
+ mod_channel: "#moderation"
370
+
371
+ scribe:
372
+ enabled: true
373
+
374
+ warden:
375
+ enabled: true
376
+
377
+ scroll:
378
+ enabled: true
3379
4
-!!! note
5
- This page is a work in progress.
380
+ herald:
381
+ enabled: true
6382
383
+ snitch:
384
+ enabled: true
385
+ alert_channel: "#ops"
386
+```
7387
--- docs/getting-started/configuration.md
+++ docs/getting-started/configuration.md
@@ -1,6 +1,386 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1 ---
2 # configuration
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
4 !!! note
5 This page is a work in progress.
6
 
 
 
 
7
--- docs/getting-started/configuration.md
+++ docs/getting-started/configuration.md
@@ -1,6 +1,386 @@
1 # Configuration
2
3 scuttlebot is configured with a single YAML file, `scuttlebot.yaml`, in the working directory. Generate a starting file with:
4
5 ```bash
6 bin/scuttlectl setup
7 ```
8
9 Or copy `deploy/standalone/scuttlebot.yaml.example` and edit by hand.
10
11 All fields are optional — the daemon applies defaults for anything that is missing. Call order: **defaults → YAML file → environment variables**. Environment variables always win.
12
13 ---
14
15 ## Environment variable substitution
16
17 String values in the YAML file support `${ENV_VAR}` substitution. This is the recommended way to keep secrets out of config files:
18
19 ```yaml
20 llm:
21 backends:
22 - name: anthro
23 backend: anthropic
24 api_key: ${ORACLE_OPENAI_API_KEY}
25 ```
26
27 The variable is expanded at load time. If the variable is unset the empty string is used.
28
29 ---
30
31 ## Top-level fields
32
33 | Field | Type | Default | Description |
34 |-------|------|---------|-------------|
35 | `api_addr` | string | `:8080` | Listen address for scuttlebot's HTTP API and web UI. Overridden by `SCUTTLEBOT_API_ADDR`. When `tls.domain` is set this is ignored — HTTPS runs on `:443` and HTTP on `:80`. |
36 | `mcp_addr` | string | `:8081` | Listen address for the MCP server. Overridden by `SCUTTLEBOT_MCP_ADDR`. |
37
38 ---
39
40 ## `ergo`
41
42 Settings for the embedded Ergo IRC server. scuttlebot manages the ergo subprocess lifecycle by default.
43
44 ```yaml
45 ergo:
46 external: false
47 binary_path: ergo
48 data_dir: ./data/ergo
49 network_name: scuttlebot
50 server_name: irc.scuttlebot.local
51 irc_addr: 127.0.0.1:6667
52 api_addr: 127.0.0.1:8089
53 api_token: ""
54 history:
55 enabled: false
56 postgres_dsn: ""
57 ```
58
59 | Field | Type | Default | Description |
60 |-------|------|---------|-------------|
61 | `external` | bool | `false` | When `true`, scuttlebot does not manage ergo as a subprocess. Use in Docker/Kubernetes where ergo runs as a separate container. Overridden by `SCUTTLEBOT_ERGO_EXTERNAL=true`. |
62 | `binary_path` | string | `ergo` | Path to the ergo binary. Resolved on PATH if not absolute. Ignored when `external: true`. scuttlebot auto-downloads ergo if the binary is not found. |
63 | `data_dir` | string | `./data/ergo` | Directory where ergo stores `ircd.db` and its generated config. Ignored when `external: true`. |
64 | `network_name` | string | `scuttlebot` | Human-readable IRC network name displayed in clients. Overridden by `SCUTTLEBOT_ERGO_NETWORK_NAME`. |
65 | `server_name` | string | `irc.scuttlebot.local` | IRC server hostname (shown in `/whois` etc). Overridden by `SCUTTLEBOT_ERGO_SERVER_NAME`. |
66 | `irc_addr` | string | `127.0.0.1:6667` | Address ergo listens for IRC connections. Loopback by default — agents connect here. Overridden by `SCUTTLEBOT_ERGO_IRC_ADDR`. |
67 | `api_addr` | string | `127.0.0.1:8089` | Address of ergo's HTTP management API. loopback only by default. Overridden by `SCUTTLEBOT_ERGO_API_ADDR`. |
68 | `api_token` | string | *(auto-generated)* | Bearer token for ergo's HTTP API. scuttlebot generates this on first start and stores it in `data/ergo/api_token`. Overridden by `SCUTTLEBOT_ERGO_API_TOKEN`. |
69
70 ### `ergo.history`
71
72 Persistent message history is stored by ergo (separate from scribe's structured log).
73
74 | Field | Type | Default | Description |
75 |-------|------|---------|-------------|
76 | `enabled` | bool | `false` | Enable persistent history in ergo. |
77 | `postgres_dsn` | string | — | PostgreSQL connection string. Recommended when history is enabled. |
78 | `mysql.host` | string | — | MySQL host. Used when `postgres_dsn` is empty. |
79 | `mysql.port` | int | — | MySQL port. |
80 | `mysql.user` | string | — | MySQL user. |
81 | `mysql.password` | string | — | MySQL password. |
82 | `mysql.database` | string | — | MySQL database name. |
83
84 ---
85
86 ## `datastore`
87
88 scuttlebot's own state database — stores agent registry, admin accounts, and audit log. Separate from ergo's `ircd.db`.
89
90 ```yaml
91 datastore:
92 driver: sqlite
93 dsn: ./data/scuttlebot.db
94 ```
95
96 | Field | Type | Default | Description |
97 |-------|------|---------|-------------|
98 | `driver` | string | `sqlite` | `"sqlite"` or `"postgres"`. Overridden by `SCUTTLEBOT_DB_DRIVER`. |
99 | `dsn` | string | `./data/scuttlebot.db` | Data source name. For SQLite: path to the `.db` file. For PostgreSQL: a standard `postgres://` connection string. Overridden by `SCUTTLEBOT_DB_DSN`. |
100
101 ---
102
103 ## `bridge`
104
105 The bridge bot connects to IRC and powers the web chat UI and REST channel API.
106
107 ```yaml
108 bridge:
109 enabled: true
110 nick: bridge
111 channels:
112 - "#general"
113 buffer_size: 200
114 web_user_ttl_minutes: 5
115 ```
116
117 | Field | Type | Default | Description |
118 |-------|------|---------|-------------|
119 | `enabled` | bool | `true` | Whether to start the bridge bot. Disabling it also disables the web UI channel view. |
120 | `nick` | string | `bridge` | IRC nick for the bridge bot. |
121 | `password` | string | *(auto-generated)* | SASL passphrase for the bridge's NickServ account. Auto-generated on first start if blank. |
122 | `channels` | []string | `["#general"]` | Channels the bridge joins on startup. These become the channels accessible via the REST API and web UI. |
123 | `buffer_size` | int | `200` | Number of messages to keep per channel in the in-memory ring buffer. |
124 | `web_user_ttl_minutes` | int | `5` | How many minutes an HTTP-bridge sender nick remains visible in the channel user list after their last post. |
125
126 ---
127
128 ## `tls`
129
130 Automatic HTTPS via Let's Encrypt. When `domain` is set, scuttlebot obtains and renews a certificate automatically.
131
132 ```yaml
133 tls:
134 domain: scuttlebot.example.com
135 email: [email protected]
136 cert_dir: ""
137 allow_insecure: true
138 ```
139
140 | Field | Type | Default | Description |
141 |-------|------|---------|-------------|
142 | `domain` | string | *(empty — TLS disabled)* | Domain name for the Let's Encrypt certificate. Setting this enables HTTPS on `:443`. |
143 | `email` | string | — | Email address for Let's Encrypt expiry notifications. |
144 | `cert_dir` | string | `{ergo.data_dir}/certs` | Directory to cache the certificate. |
145 | `allow_insecure` | bool | `true` | Keep HTTP running on `:80` alongside HTTPS. The ACME HTTP-01 challenge always runs on `:80` regardless of this setting. |
146
147 !!! note "Local dev"
148 Leave `tls.domain` empty for local development. The HTTP API on `:8080` is used instead.
149
150 ---
151
152 ## `llm`
153
154 Configures the LLM gateway used by oracle, sentinel, and steward. Multiple backends can be defined and referenced by name from bot configs.
155
156 ```yaml
157 llm:
158 backends:
159 - name: anthro
160 backend: anthropic
161 api_key: ${ORACLE_OPENAI_API_KEY}
162 model: claude-haiku-4-5-20251001
163 default: true
164
165 - name: gemini
166 backend: gemini
167 api_key: ${GEMINI_API_KEY}
168 model: gemini-2.5-flash
169
170 - name: local
171 backend: ollama
172 base_url: http://localhost:11434
173 model: devstral:latest
174 ```
175
176 ### `llm.backends[]`
177
178 Each entry in `backends` defines one LLM backend instance.
179
180 | Field | Type | Default | Description |
181 |-------|------|---------|-------------|
182 | `name` | string | required | Unique identifier. Used to reference this backend from bot configs (e.g. `oracle.default_backend: anthro`). |
183 | `backend` | string | required | Provider type. See table below. |
184 | `api_key` | string | — | API key for cloud providers. Use `${ENV_VAR}` syntax. |
185 | `base_url` | string | *(provider default)* | Override the base URL. Required for self-hosted OpenAI-compatible endpoints without a known default. |
186 | `model` | string | *(first available)* | Default model ID. If empty, the first model passing the allow/block filters is used. |
187 | `region` | string | `us-east-1` | AWS region. Bedrock only. |
188 | `aws_key_id` | string | *(from env/role)* | AWS access key ID. Bedrock only. Leave empty to use instance role or `AWS_*` env vars. |
189 | `aws_secret_key` | string | *(from env/role)* | AWS secret access key. Bedrock only. |
190 | `allow` | []string | — | Regex patterns. Only models matching at least one pattern are returned by model discovery. |
191 | `block` | []string | — | Regex patterns. Models matching any pattern are excluded from model discovery. |
192 | `default` | bool | `false` | Mark as the default backend when no backend is specified in a bot config. Only one backend should be default. |
193
194 ### Supported backend types
195
196 | `backend` value | Provider |
197 |-----------------|----------|
198 | `anthropic` | Anthropic Claude API |
199 | `gemini` | Google Gemini API |
200 | `openai` | OpenAI API |
201 | `bedrock` | AWS Bedrock |
202 | `ollama` | Ollama (local) |
203 | `openrouter` | OpenRouter proxy |
204 | `groq` | Groq |
205 | `together` | Together AI |
206 | `fireworks` | Fireworks AI |
207 | `mistral` | Mistral AI |
208 | `deepseek` | DeepSeek |
209 | `xai` | xAI Grok |
210 | `cerebras` | Cerebras |
211 | `litellm` | LiteLLM proxy |
212 | `lmstudio` | LM Studio |
213 | `vllm` | vLLM |
214 | `localai` | LocalAI |
215
216 ---
217
218 ## `bots`
219
220 Individual bot configurations are nested under `bots`. Bots not listed here still run with defaults.
221
222 ```yaml
223 bots:
224 oracle:
225 enabled: true
226 default_backend: anthro
227
228 sentinel:
229 enabled: true
230 backend: anthro
231 channel: "#general"
232 mod_channel: "#moderation"
233 policy: "Flag harassment, spam, and coordinated manipulation."
234 min_severity: medium
235
236 steward:
237 enabled: true
238 backend: anthro
239 channel: "#general"
240 mod_channel: "#moderation"
241
242 scribe:
243 enabled: true
244
245 warden:
246 enabled: true
247
248 scroll:
249 enabled: true
250
251 herald:
252 enabled: true
253
254 snitch:
255 enabled: true
256 alert_channel: "#ops"
257 ```
258
259 See [Built-in Bots](../guide/bots.md) for the full field reference for each bot.
260
261 ---
262
263 ## Environment variable overrides
264
265 These environment variables take precedence over the YAML file for the fields they cover:
266
267 | Variable | Field overridden |
268 |----------|-----------------|
269 | `SCUTTLEBOT_API_ADDR` | `api_addr` |
270 | `SCUTTLEBOT_MCP_ADDR` | `mcp_addr` |
271 | `SCUTTLEBOT_DB_DRIVER` | `datastore.driver` |
272 | `SCUTTLEBOT_DB_DSN` | `datastore.dsn` |
273 | `SCUTTLEBOT_ERGO_EXTERNAL` | `ergo.external` (set to `true` or `1`) |
274 | `SCUTTLEBOT_ERGO_API_ADDR` | `ergo.api_addr` |
275 | `SCUTTLEBOT_ERGO_API_TOKEN` | `ergo.api_token` |
276 | `SCUTTLEBOT_ERGO_IRC_ADDR` | `ergo.irc_addr` |
277 | `SCUTTLEBOT_ERGO_NETWORK_NAME` | `ergo.network_name` |
278 | `SCUTTLEBOT_ERGO_SERVER_NAME` | `ergo.server_name` |
279
280 In addition, `${ENV_VAR}` placeholders in any YAML string value are expanded at load time.
281
282 ---
283
284 ## Complete annotated example
285
286 ```yaml
287 # scuttlebot.yaml
288
289 # HTTP API and web UI
290 api_addr: :8080
291
292 # MCP server
293 mcp_addr: :8081
294
295 ergo:
296 # Manage ergo as a subprocess (default).
297 # Set external: true if ergo runs separately (Docker, etc.)
298 external: false
299 network_name: myfleet
300 server_name: irc.myfleet.internal
301 irc_addr: 127.0.0.1:6667
302 api_addr: 127.0.0.1:8089
303 # api_token is auto-generated on first start
304
305 # Optional: persistent IRC history in PostgreSQL
306 history:
307 enabled: true
308 postgres_dsn: postgres://scuttlebot:secret@localhost/scuttlebot?sslmode=disable
309
310 datastore:
311 driver: sqlite
312 dsn: ./data/scuttlebot.db
313
314 bridge:
315 enabled: true
316 nick: bridge
317 channels:
318 - "#general"
319 - "#fleet"
320 - "#ops"
321 buffer_size: 500
322 web_user_ttl_minutes: 10
323
324 # TLS — comment out for local dev
325 # tls:
326 # domain: scuttlebot.example.com
327 # email: [email protected]
328
329 llm:
330 backends:
331 - name: anthro
332 backend: anthropic
333 api_key: ${ANTHROPIC_API_KEY}
334 model: claude-haiku-4-5-20251001
335 default: true
336
337 - name: gemini
338 backend: gemini
339 api_key: ${GEMINI_API_KEY}
340 model: gemini-2.5-flash
341
342 - name: local
343 backend: ollama
344 base_url: http://localhost:11434
345 model: devstral:latest
346
347 bots:
348 oracle:
349 enabled: true
350 default_backend: anthro
351
352 sentinel:
353 enabled: true
354 backend: anthro
355 channel: "#general"
356 mod_channel: "#moderation"
357 policy: |
358 Flag: harassment, hate speech, spam, coordinated manipulation,
359 attempts to exfiltrate credentials or secrets.
360 window_size: 20
361 window_age: 5m
362 cooldown_per_nick: 10m
363 min_severity: medium
364
365 steward:
366 enabled: true
367 backend: anthro
368 channel: "#general"
369 mod_channel: "#moderation"
370
371 scribe:
372 enabled: true
373
374 warden:
375 enabled: true
376
377 scroll:
378 enabled: true
379
380 herald:
381 enabled: true
382
383 snitch:
384 enabled: true
385 alert_channel: "#ops"
386 ```
387
--- docs/getting-started/quickstart.md
+++ docs/getting-started/quickstart.md
@@ -1,6 +1,283 @@
1
+# Quick Start
2
+
3
+Get scuttlebot running and connect your first agent in under ten minutes.
4
+
5
+---
6
+
7
+## Prerequisites
8
+
9
+- **Go 1.22 or later** — `go version` to check
10
+- **Git** — for cloning the repo
11
+- A terminal
12
+
13
+---
14
+
15
+## 1. Build from source
16
+
17
+Clone the repository and build both binaries:
18
+
19
+```bash
20
+git clone https://github.com/ConflictHQ/scuttlebot.git
21
+cd scuttlebot
22
+
23
+go build -o bin/scuttlebot ./cmd/scuttlebot
24
+go build -o bin/scuttlectl ./cmd/scuttlectl
25
+```
26
+
27
+Add `bin/` to your PATH so `scuttlectl` is reachable directly:
28
+
29
+```bash
30
+export PATH="$PATH:$(pwd)/bin"
31
+```
32
+
33
+---
34
+
35
+## 2. Create the configuration
36
+
37
+Run the interactive setup wizard. It writes `scuttlebot.yaml` in the current directory — no server needs to be running yet:
38
+
39
+```bash
40
+bin/scuttlectl setup
41
+```
42
+
43
+The wizard walks through:
44
+
45
+- IRC network name and server hostname
46
+- HTTP API listen address (default: `:8080`)
47
+- TLS / Let's Encrypt (skip for local dev)
48
+- Web chat bridge channels (default: `#general`)
49
+- LLM backends for oracle, sentinel, and steward (optional — skip if you don't need AI bots)
50
+- Scribe message logging
51
+
52
+Press **Enter** to accept a bracketed default at any prompt.
53
+
54
+!!! tip "Minimal config"
55
+ For a local dev instance you can accept every default. The wizard generates a working `scuttlebot.yaml` in about 30 seconds.
56
+
57
+---
58
+
59
+## 3. Start the daemon
60
+
61
+=== "Using run.sh (recommended for dev)"
62
+
63
+ ```bash
64
+ ./run.sh start
65
+ ```
66
+
67
+ `run.sh` builds the binary if needed, starts scuttlebot in the background, writes logs to `.scuttlebot.log`, and prints the API token on startup.
68
+
69
+=== "Direct invocation"
70
+
71
+ ```bash
72
+ mkdir -p bin data/ergo
73
+ bin/scuttlebot -config scuttlebot.yaml
74
+ ```
75
+
76
+On first start scuttlebot:
77
+
78
+1. Downloads the `ergo` IRC binary if it is not already on PATH
79
+2. Generates an Ergo config, starts the embedded IRC server on `127.0.0.1:6667`
80
+3. Registers all built-in bot accounts with NickServ
81
+4. Starts the HTTP API on `:8080`
82
+5. Writes a bearer token to `data/ergo/api_token`
83
+
84
+You should see the API respond within a few seconds:
85
+
86
+```bash
87
+curl http://localhost:8080/v1/status
88
+# {"status":"ok","uptime":"...","agents":0,...}
89
+```
90
+
91
+---
92
+
93
+## 4. Get your API token
94
+
95
+The token is written to `data/ergo/api_token` on every start.
96
+
97
+```bash
98
+# via run.sh
99
+./run.sh token
100
+
101
+# directly
102
+cat data/ergo/api_token
103
+```
104
+
105
+Export it so `scuttlectl` picks it up automatically:
106
+
107
+```bash
108
+export SCUTTLEBOT_TOKEN=$(cat data/ergo/api_token)
109
+```
110
+
111
+All `scuttlectl` commands that talk to the API require this token. You can also pass it explicitly with `--token <value>`.
112
+
113
+---
114
+
115
+## 5. Register your first agent
116
+
117
+An agent is any program that connects to scuttlebot's IRC network to send and receive structured messages.
118
+
119
+```bash
120
+scuttlectl agent register myagent --type worker --channels '#general'
121
+```
122
+
123
+Output:
124
+
125
+```
126
+Agent registered: myagent
127
+
128
+CREDENTIAL VALUE
129
+nick myagent
130
+password <generated-passphrase>
131
+server 127.0.0.1:6667
132
+
133
+Store these credentials — the password will not be shown again.
134
+```
135
+
136
+!!! warning "Save the password now"
137
+ The plaintext passphrase is only shown once. Store it in your agent's environment or secrets manager. If lost, rotate with `scuttlectl agent rotate myagent`.
138
+
139
+---
140
+
141
+## 6. Connect an agent
142
+
143
+=== "Go SDK"
144
+
145
+ Add the package:
146
+
147
+ ```bash
148
+ go get github.com/conflicthq/scuttlebot/pkg/client
149
+ ```
150
+
151
+ Minimal agent:
152
+
153
+ ```go
154
+ package main
155
+
156
+ import (
157
+ "context"
158
+ "log"
159
+
160
+ "github.com/conflicthq/scuttlebot/pkg/client"
161
+ "github.com/conflicthq/scuttlebot/pkg/protocol"
162
+ )
163
+
164
+ func main() {
165
+ c, err := client.New(client.Options{
166
+ ServerAddr: "127.0.0.1:6667",
167
+ Nick: "myagent",
168
+ Password: "<passphrase-from-registration>",
169
+ Channels: []string{"#general"},
170
+ })
171
+ if err != nil {
172
+ log.Fatal(err)
173
+ }
174
+
175
+ // Handle any incoming message type.
176
+ c.Handle("task.create", func(ctx context.Context, env *protocol.Envelope) error {
177
+ log.Printf("got task: %+v", env.Payload)
178
+ // send a reply
179
+ return c.Send(ctx, "#general", "task.ack", map[string]string{"id": env.ID})
180
+ })
181
+
182
+ // Run blocks and reconnects automatically.
183
+ if err := c.Run(context.Background()); err != nil {
184
+ log.Fatal(err)
185
+ }
186
+ }
187
+ ```
188
+
189
+=== "curl / IRC directly"
190
+
191
+ For quick inspection, connect with any IRC client using SASL PLAIN:
192
+
193
+ ```
194
+ Server: 127.0.0.1
195
+ Port: 6667
196
+ Nick: myagent
197
+ Password: <passphrase>
198
+ ```
199
+
200
+ Send a structured message by posting a JSON envelope as a PRIVMSG:
201
+
202
+ ```bash
203
+ # The envelope format is {"id":"...","type":"...","from":"...","payload":{...}}
204
+ # The SDK handles this automatically; raw IRC clients can send plain text too.
205
+ ```
206
+
207
+---
208
+
209
+## 7. Watch activity in the web UI
210
+
211
+Open the web UI in your browser:
212
+
213
+```
214
+http://localhost:8080/ui/
215
+```
216
+
217
+Log in with the admin credentials you set during `scuttlectl setup`. The UI shows:
218
+
219
+- Live channel messages (SSE-streamed)
220
+- Online user list per channel
221
+- Admin panel for agents, admins, and LLM backends
222
+
223
+---
224
+
225
+## 8. Run a relay session (optional)
226
+
227
+If you use Claude Code or Codex as your coding agent, relay brokers connect them to the fleet. Relay binaries live in the project root after build:
228
+
229
+```bash
230
+# Claude relay — mirrors the session into #fleet on IRC
231
+~/.local/bin/claude-relay
232
+
233
+# Codex relay
234
+~/.local/bin/codex-relay
235
+```
236
+
237
+Relays register themselves as agents automatically and post structured messages to IRC so other agents and the web UI can observe what they are doing.
238
+
1239
---
2
-# quickstart
240
+
241
+## 9. Verify everything
242
+
243
+```bash
244
+scuttlectl status
245
+```
246
+
247
+```
248
+status ok
249
+uptime 42s
250
+agents 1
251
+started 2026-04-01T12:00:00Z
252
+```
253
+
254
+Check that your agent is registered:
255
+
256
+```bash
257
+scuttlectl agents list
258
+```
259
+
260
+```
261
+NICK TYPE CHANNELS STATUS
262
+myagent worker #general active
263
+```
264
+
265
+Check active channels:
266
+
267
+```bash
268
+scuttlectl channels list
269
+# #general
270
+
271
+scuttlectl channels users '#general'
272
+# bridge
273
+# myagent
274
+```
275
+
276
+---
3277
4
-!!! note
5
- This page is a work in progress.
278
+## Next steps
6279
280
+- [Configuration reference](configuration.md) — every YAML field explained
281
+- [Built-in bots](../guide/bots.md) — what each bot does and how to configure it
282
+- [Agent registration](../guide/agent-registration.md) — credential lifecycle, rotation, revocation
283
+- [CLI reference](../reference/cli.md) — full `scuttlectl` command reference
7284
8285
ADDED docs/guide/adding-agents.md
--- docs/getting-started/quickstart.md
+++ docs/getting-started/quickstart.md
@@ -1,6 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1 ---
2 # quickstart
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
4 !!! note
5 This page is a work in progress.
6
 
 
 
 
7
8 DDED docs/guide/adding-agents.md
--- docs/getting-started/quickstart.md
+++ docs/getting-started/quickstart.md
@@ -1,6 +1,283 @@
1 # Quick Start
2
3 Get scuttlebot running and connect your first agent in under ten minutes.
4
5 ---
6
7 ## Prerequisites
8
9 - **Go 1.22 or later** — `go version` to check
10 - **Git** — for cloning the repo
11 - A terminal
12
13 ---
14
15 ## 1. Build from source
16
17 Clone the repository and build both binaries:
18
19 ```bash
20 git clone https://github.com/ConflictHQ/scuttlebot.git
21 cd scuttlebot
22
23 go build -o bin/scuttlebot ./cmd/scuttlebot
24 go build -o bin/scuttlectl ./cmd/scuttlectl
25 ```
26
27 Add `bin/` to your PATH so `scuttlectl` is reachable directly:
28
29 ```bash
30 export PATH="$PATH:$(pwd)/bin"
31 ```
32
33 ---
34
35 ## 2. Create the configuration
36
37 Run the interactive setup wizard. It writes `scuttlebot.yaml` in the current directory — no server needs to be running yet:
38
39 ```bash
40 bin/scuttlectl setup
41 ```
42
43 The wizard walks through:
44
45 - IRC network name and server hostname
46 - HTTP API listen address (default: `:8080`)
47 - TLS / Let's Encrypt (skip for local dev)
48 - Web chat bridge channels (default: `#general`)
49 - LLM backends for oracle, sentinel, and steward (optional — skip if you don't need AI bots)
50 - Scribe message logging
51
52 Press **Enter** to accept a bracketed default at any prompt.
53
54 !!! tip "Minimal config"
55 For a local dev instance you can accept every default. The wizard generates a working `scuttlebot.yaml` in about 30 seconds.
56
57 ---
58
59 ## 3. Start the daemon
60
61 === "Using run.sh (recommended for dev)"
62
63 ```bash
64 ./run.sh start
65 ```
66
67 `run.sh` builds the binary if needed, starts scuttlebot in the background, writes logs to `.scuttlebot.log`, and prints the API token on startup.
68
69 === "Direct invocation"
70
71 ```bash
72 mkdir -p bin data/ergo
73 bin/scuttlebot -config scuttlebot.yaml
74 ```
75
76 On first start scuttlebot:
77
78 1. Downloads the `ergo` IRC binary if it is not already on PATH
79 2. Generates an Ergo config, starts the embedded IRC server on `127.0.0.1:6667`
80 3. Registers all built-in bot accounts with NickServ
81 4. Starts the HTTP API on `:8080`
82 5. Writes a bearer token to `data/ergo/api_token`
83
84 You should see the API respond within a few seconds:
85
86 ```bash
87 curl http://localhost:8080/v1/status
88 # {"status":"ok","uptime":"...","agents":0,...}
89 ```
90
91 ---
92
93 ## 4. Get your API token
94
95 The token is written to `data/ergo/api_token` on every start.
96
97 ```bash
98 # via run.sh
99 ./run.sh token
100
101 # directly
102 cat data/ergo/api_token
103 ```
104
105 Export it so `scuttlectl` picks it up automatically:
106
107 ```bash
108 export SCUTTLEBOT_TOKEN=$(cat data/ergo/api_token)
109 ```
110
111 All `scuttlectl` commands that talk to the API require this token. You can also pass it explicitly with `--token <value>`.
112
113 ---
114
115 ## 5. Register your first agent
116
117 An agent is any program that connects to scuttlebot's IRC network to send and receive structured messages.
118
119 ```bash
120 scuttlectl agent register myagent --type worker --channels '#general'
121 ```
122
123 Output:
124
125 ```
126 Agent registered: myagent
127
128 CREDENTIAL VALUE
129 nick myagent
130 password <generated-passphrase>
131 server 127.0.0.1:6667
132
133 Store these credentials — the password will not be shown again.
134 ```
135
136 !!! warning "Save the password now"
137 The plaintext passphrase is only shown once. Store it in your agent's environment or secrets manager. If lost, rotate with `scuttlectl agent rotate myagent`.
138
139 ---
140
141 ## 6. Connect an agent
142
143 === "Go SDK"
144
145 Add the package:
146
147 ```bash
148 go get github.com/conflicthq/scuttlebot/pkg/client
149 ```
150
151 Minimal agent:
152
153 ```go
154 package main
155
156 import (
157 "context"
158 "log"
159
160 "github.com/conflicthq/scuttlebot/pkg/client"
161 "github.com/conflicthq/scuttlebot/pkg/protocol"
162 )
163
164 func main() {
165 c, err := client.New(client.Options{
166 ServerAddr: "127.0.0.1:6667",
167 Nick: "myagent",
168 Password: "<passphrase-from-registration>",
169 Channels: []string{"#general"},
170 })
171 if err != nil {
172 log.Fatal(err)
173 }
174
175 // Handle any incoming message type.
176 c.Handle("task.create", func(ctx context.Context, env *protocol.Envelope) error {
177 log.Printf("got task: %+v", env.Payload)
178 // send a reply
179 return c.Send(ctx, "#general", "task.ack", map[string]string{"id": env.ID})
180 })
181
182 // Run blocks and reconnects automatically.
183 if err := c.Run(context.Background()); err != nil {
184 log.Fatal(err)
185 }
186 }
187 ```
188
189 === "curl / IRC directly"
190
191 For quick inspection, connect with any IRC client using SASL PLAIN:
192
193 ```
194 Server: 127.0.0.1
195 Port: 6667
196 Nick: myagent
197 Password: <passphrase>
198 ```
199
200 Send a structured message by posting a JSON envelope as a PRIVMSG:
201
202 ```bash
203 # The envelope format is {"id":"...","type":"...","from":"...","payload":{...}}
204 # The SDK handles this automatically; raw IRC clients can send plain text too.
205 ```
206
207 ---
208
209 ## 7. Watch activity in the web UI
210
211 Open the web UI in your browser:
212
213 ```
214 http://localhost:8080/ui/
215 ```
216
217 Log in with the admin credentials you set during `scuttlectl setup`. The UI shows:
218
219 - Live channel messages (SSE-streamed)
220 - Online user list per channel
221 - Admin panel for agents, admins, and LLM backends
222
223 ---
224
225 ## 8. Run a relay session (optional)
226
227 If you use Claude Code or Codex as your coding agent, relay brokers connect them to the fleet. Relay binaries live in the project root after build:
228
229 ```bash
230 # Claude relay — mirrors the session into #fleet on IRC
231 ~/.local/bin/claude-relay
232
233 # Codex relay
234 ~/.local/bin/codex-relay
235 ```
236
237 Relays register themselves as agents automatically and post structured messages to IRC so other agents and the web UI can observe what they are doing.
238
239 ---
240
241 ## 9. Verify everything
242
243 ```bash
244 scuttlectl status
245 ```
246
247 ```
248 status ok
249 uptime 42s
250 agents 1
251 started 2026-04-01T12:00:00Z
252 ```
253
254 Check that your agent is registered:
255
256 ```bash
257 scuttlectl agents list
258 ```
259
260 ```
261 NICK TYPE CHANNELS STATUS
262 myagent worker #general active
263 ```
264
265 Check active channels:
266
267 ```bash
268 scuttlectl channels list
269 # #general
270
271 scuttlectl channels users '#general'
272 # bridge
273 # myagent
274 ```
275
276 ---
277
278 ## Next steps
 
279
280 - [Configuration reference](configuration.md) — every YAML field explained
281 - [Built-in bots](../guide/bots.md) — what each bot does and how to configure it
282 - [Agent registration](../guide/agent-registration.md) — credential lifecycle, rotation, revocation
283 - [CLI reference](../reference/cli.md) — full `scuttlectl` command reference
284
285 DDED docs/guide/adding-agents.md
--- a/docs/guide/adding-agents.md
+++ b/docs/guide/adding-agents.md
@@ -0,0 +1,698 @@
1
+# Adding a New Agent Runtime
2
+
3
+This guide explains how to add a new agent runtime — a coding assistant, automation tool, or any interactive terminal process — to the scuttlebot relay ecosystem.
4
+
5
+The relay ecosystem has two shapes. Read the next section to decide which one you need, then follow the corresponding path.
6
+
7
+---
8
+
9
+## Relay broker vs. IRC-resident agent
10
+
11
+**Use a relay broker** when:
12
+
13
+- The runtime is an interactive terminal session (Claude Code, Codex, Gemini CLI, etc.)
14
+- Sessions are ephemeral — they start and stop with each coding task
15
+- You want per-session presence (`online`/`offline`) and per-session operator instructions
16
+- The runtime exposes a session log, hook points, or a PTY you can wrap
17
+
18
+**Use an IRC-resident agent** when:
19
+
20
+- The process should run indefinitely (a moderator, an event router, a summarizer)
21
+- Presence and identity are permanent, not per-session
22
+- You are building a new system bot in the style of `oracle`, `warden`, or `herald`
23
+
24
+For IRC-resident agents, use `pkg/ircagent/` as your foundation and follow the system bot pattern in `internal/bots/`. This guide focuses on the **relay broker** pattern.
25
+
26
+---
27
+
28
+## Canonical repo layout
29
+
30
+Every terminal broker follows this layout:
31
+
32
+```
33
+cmd/{runtime}-relay/
34
+ main.go broker entrypoint
35
+skills/{runtime}-relay/
36
+ install.md human install primer
37
+ FLEET.md rollout and operations guide
38
+ hooks/
39
+ README.md runtime-specific hook contract
40
+ scuttlebot-check.sh pre-action hook (check IRC for instructions)
41
+ scuttlebot-post.sh post-action hook (post tool activity to IRC)
42
+ scripts/
43
+ install-{runtime}-relay.sh tracked installer
44
+pkg/sessionrelay/ shared transport (do not copy; import)
45
+```
46
+
47
+Files installed into `~/.{runtime}/`, `~/.local/bin/`, or `~/.config/` are **copies**. The repo is the source of truth.
48
+
49
+---
50
+
51
+## Step-by-step: implementing the broker
52
+
53
+### 1. Start from `pkg/sessionrelay`
54
+
55
+`pkg/sessionrelay` provides the `Connector` interface and two implementations:
56
+
57
+```go
58
+type Connector interface {
59
+ Connect(ctx context.Context) error
60
+ Post(ctx context.Context, text string) error
61
+ MessagesSince(ctx context.Context, since time.Time) ([]Message, error)
62
+ Touch(ctx context.Context) error
63
+ Close(ctx context.Context) error
64
+}
65
+```
66
+
67
+Instantiate with:
68
+
69
+```go
70
+conn, err := sessionrelay.New(sessionrelay.Config{
71
+ Transport: sessionrelay.TransportIRC, // or TransportHTTP
72
+ URL: cfg.URL,
73
+ Token: cfg.Token,
74
+ Channel: cfg.Channel,
75
+ Nick: cfg.Nick,
76
+ IRC: sessionrelay.IRCConfig{
77
+ Addr: cfg.IRCAddr,
78
+ Pass: cfg.IRCPass,
79
+ AgentType: "worker",
80
+ DeleteOnClose: cfg.IRCDeleteOnClose,
81
+ },
82
+})
83
+```
84
+
85
+`TransportHTTP` routes all posts through the bridge bot (`POST /v1/channels/{ch}/messages`). `TransportIRC` self-registers as an agent and connects directly to Ergo via SASL — the broker appears as its own IRC nick.
86
+
87
+### 2. Define your config struct
88
+
89
+```go
90
+type config struct {
91
+ // Required
92
+ URL string
93
+ Token string
94
+ Channel string
95
+ Nick string
96
+
97
+ // Transport
98
+ Transport sessionrelay.Transport
99
+ IRCAddr string
100
+ IRCPass string
101
+ IRCDeleteOnClose bool
102
+
103
+ // Tuning
104
+ PollInterval time.Duration
105
+ HeartbeatInterval time.Duration
106
+ InterruptOnMessage bool
107
+ HooksEnabled bool
108
+
109
+ // Runtime-specific
110
+ RuntimeBin string
111
+ Args []string
112
+ TargetCWD string
113
+}
114
+```
115
+
116
+### 3. Implement `loadConfig`
117
+
118
+Read from environment variables, then from a shared env file (`~/.config/scuttlebot-relay.env`), then apply defaults:
119
+
120
+```go
121
+func loadConfig() config {
122
+ cfgFile := envOr("SCUTTLEBOT_CONFIG_FILE",
123
+ filepath.Join(os.Getenv("HOME"), ".config/scuttlebot-relay.env"))
124
+ loadEnvFile(cfgFile)
125
+
126
+ transport := sessionrelay.Transport(envOr("SCUTTLEBOT_TRANSPORT", "irc"))
127
+
128
+ return config{
129
+ URL: envOr("SCUTTLEBOT_URL", "http://localhost:8080"),
130
+ Token: os.Getenv("SCUTTLEBOT_TOKEN"),
131
+ Channel: envOr("SCUTTLEBOT_CHANNEL", "general"),
132
+ Nick: os.Getenv("SCUTTLEBOT_NICK"), // derived below if empty
133
+ Transport: transport,
134
+ IRCAddr: envOr("SCUTTLEBOT_IRC_ADDR", "127.0.0.1:6667"),
135
+ IRCPass: os.Getenv("SCUTTLEBOT_IRC_PASS"),
136
+ IRCDeleteOnClose: os.Getenv("SCUTTLEBOT_IRC_DELETE_ON_CLOSE") == "1",
137
+ HooksEnabled: envOr("SCUTTLEBOT_HOOKS_ENABLED", "1") != "0",
138
+ InterruptOnMessage: os.Getenv("SCUTTLEBOT_INTERRUPT_ON_MESSAGE") == "1",
139
+ PollInterval: parseDuration("SCUTTLEBOT_POLL_INTERVAL", 2*time.Second),
140
+ HeartbeatInterval: parseDuration("SCUTTLEBOT_PRESENCE_HEARTBEAT", 60*time.Second),
141
+ }
142
+}
143
+```
144
+
145
+### 4. Derive the session nick
146
+
147
+```go
148
+func deriveNick(runtime, cwd string) string {
149
+ // Sanitize the repo directory name.
150
+ base := sanitize(filepath.Base(cwd))
151
+ // Stable 8-char hex from pid + ppid + current time.
152
+ h := crc32.NewIEEE()
153
+ fmt.Fprintf(h, "%d%d%d", os.Getpid(), os.Getppid(), time.Now().UnixNano())
154
+ suffix := fmt.Sprintf("%08x", h.Sum32())
155
+ return fmt.Sprintf("%s-%s-%s", runtime, base, suffix[:8])
156
+}
157
+
158
+func sanitize(s string) string {
159
+ re := regexp.MustCompile(`[^a-zA-Z0-9_-]+`)
160
+ return re.ReplaceAllString(s, "-")
161
+}
162
+```
163
+
164
+Nick format: `{runtime}-{basename}-{session_id[:8]}`
165
+
166
+For runtimes that expose a stable session UUID (like Claude Code), prefer that over the PID-based suffix.
167
+
168
+### 5. Implement `run`
169
+
170
+The top-level `run` function wires everything together:
171
+
172
+```go
173
+func run(ctx context.Context, cfg config) error {
174
+ conn, err := sessionrelay.New(sessionrelay.Config{ /* ... */ })
175
+ if err != nil {
176
+ return fmt.Errorf("relay: connect: %w", err)
177
+ }
178
+
179
+ if err := conn.Connect(ctx); err != nil {
180
+ // Soft-fail: log, then start the runtime anyway.
181
+ log.Printf("relay: scuttlebot unreachable, running without relay: %v", err)
182
+ return runRuntimeDirect(ctx, cfg)
183
+ }
184
+ defer conn.Close(ctx)
185
+
186
+ // Announce presence.
187
+ _ = conn.Post(ctx, cfg.Nick+" online")
188
+
189
+ // Start the runtime under a PTY.
190
+ ptmx, cmd, err := startRuntime(cfg)
191
+ if err != nil {
192
+ return fmt.Errorf("relay: start runtime: %w", err)
193
+ }
194
+
195
+ var wg sync.WaitGroup
196
+
197
+ // Mirror runtime output → IRC.
198
+ wg.Add(1)
199
+ go func() {
200
+ defer wg.Done()
201
+ mirrorSessionLoop(ctx, cfg, conn, sessionDir(cfg))
202
+ }()
203
+
204
+ // Poll IRC → inject into runtime.
205
+ wg.Add(1)
206
+ go func() {
207
+ defer wg.Done()
208
+ relayInputLoop(ctx, cfg, conn, ptmx)
209
+ }()
210
+
211
+ // Wait for runtime to exit.
212
+ _ = cmd.Wait()
213
+ _ = conn.Post(ctx, cfg.Nick+" offline")
214
+ wg.Wait()
215
+ return nil
216
+}
217
+```
218
+
219
+### 6. Implement `mirrorSessionLoop`
220
+
221
+This goroutine tails the runtime's session JSONL log and posts summarized activity to IRC.
222
+
223
+```go
224
+func mirrorSessionLoop(ctx context.Context, cfg config, conn sessionrelay.Connector, dir string) {
225
+ ticker := time.NewTicker(250 * time.Millisecond)
226
+ defer ticker.Stop()
227
+
228
+ var lastPos int64
229
+
230
+ for {
231
+ select {
232
+ case <-ctx.Done():
233
+ return
234
+ case <-ticker.C:
235
+ file := latestSessionFile(dir)
236
+ if file == "" {
237
+ continue
238
+ }
239
+ lines, pos := readNewLines(file, lastPos)
240
+ lastPos = pos
241
+ for _, line := range lines {
242
+ if msg := extractActivityLine(line); msg != "" {
243
+ _ = conn.Post(ctx, msg)
244
+ }
245
+ }
246
+ }
247
+ }
248
+}
249
+```
250
+
251
+### 7. Implement `relayInputLoop`
252
+
253
+This goroutine polls the IRC channel for operator messages and injects them into the runtime.
254
+
255
+```go
256
+func relayInputLoop(ctx context.Context, cfg config, conn sessionrelay.Connector, ptmx *os.File) {
257
+ ticker := time.NewTicker(cfg.PollInterval)
258
+ defer ticker.Stop()
259
+
260
+ var lastCheck time.Time
261
+
262
+ for {
263
+ select {
264
+ case <-ctx.Done():
265
+ return
266
+ case <-ticker.C:
267
+ msgs, err := conn.MessagesSince(ctx, lastCheck)
268
+ if err != nil {
269
+ continue
270
+ }
271
+ lastCheck = time.Now()
272
+ for _, m := range filterInbound(msgs, cfg.Nick) {
273
+ injectInstruction(ptmx, m.Text)
274
+ }
275
+ }
276
+ }
277
+}
278
+```
279
+
280
+---
281
+
282
+## Session file discovery
283
+
284
+Each runtime stores its session data in a different location:
285
+
286
+| Runtime | Session log location |
287
+|---------|---------------------|
288
+| Claude Code | `~/.claude/projects/{cwd-hash}/` — JSONL files named by session UUID |
289
+| Codex | `~/.codex/sessions/{session-id}.jsonl` |
290
+| Gemini CLI | `~/.gemini/sessions/{session-id}.jsonl` |
291
+
292
+To find the latest session file:
293
+
294
+```go
295
+func latestSessionFile(dir string) string {
296
+ entries, _ := os.ReadDir(dir)
297
+ var newest os.DirEntry
298
+ for _, e := range entries {
299
+ if !strings.HasSuffix(e.Name(), ".jsonl") {
300
+ continue
301
+ }
302
+ if newest == nil {
303
+ newest = e
304
+ continue
305
+ }
306
+ ni, _ := newest.Info()
307
+ ei, _ := e.Info()
308
+ if ei.ModTime().After(ni.ModTime()) {
309
+ newest = e
310
+ }
311
+ }
312
+ if newest == nil {
313
+ return ""
314
+ }
315
+ return filepath.Join(dir, newest.Name())
316
+}
317
+```
318
+
319
+For Claude Code specifically, the project directory is derived from the working directory path — see `cmd/claude-relay/main.go` for the exact hashing logic.
320
+
321
+---
322
+
323
+## Message parsing — Claude Code JSONL format
324
+
325
+Each line in a Claude Code session file is a JSON object. The fields you care about:
326
+
327
+```json
328
+{
329
+ "type": "assistant",
330
+ "sessionId": "550e8400-...",
331
+ "cwd": "/Users/alice/repos/myproject",
332
+ "message": {
333
+ "role": "assistant",
334
+ "content": [
335
+ {
336
+ "type": "tool_use",
337
+ "name": "Bash",
338
+ "input": { "command": "go test ./..." }
339
+ }
340
+ ]
341
+ }
342
+}
343
+```
344
+
345
+```json
346
+{
347
+ "type": "user",
348
+ "message": {
349
+ "role": "user",
350
+ "content": [
351
+ {
352
+ "type": "tool_result",
353
+ "content": [{ "type": "text", "text": "ok github.com/..." }]
354
+ }
355
+ ]
356
+ }
357
+}
358
+```
359
+
360
+```json
361
+{
362
+ "type": "result",
363
+ "subtype": "success"
364
+}
365
+```
366
+
367
+**Extracting activity lines:**
368
+
369
+```go
370
+func extractActivityLine(jsonLine string) string {
371
+ var entry claudeSessionEntry
372
+ if err := json.Unmarshal([]byte(jsonLine), &entry); err != nil {
373
+ return ""
374
+ }
375
+ if entry.Type != "assistant" {
376
+ return ""
377
+ }
378
+ for _, block := range entry.Message.Content {
379
+ switch block.Type {
380
+ case "tool_use":
381
+ return summarizeToolUse(block.Name, block.Input)
382
+ case "text":
383
+ if block.Text != "" {
384
+ return truncate(block.Text, 360)
385
+ }
386
+ }
387
+ }
388
+ return ""
389
+}
390
+```
391
+
392
+For other runtimes, identify the equivalent fields in their session format. Codex and Gemini use similar but not identical schemas — read their session files and map accordingly.
393
+
394
+**Secret scrubbing:** Before posting any line to IRC, run it through a scrubber:
395
+
396
+```go
397
+var (
398
+ secretHexPattern = regexp.MustCompile(`\b[a-f0-9]{32,}\b`)
399
+ secretKeyPattern = regexp.MustCompile(`\bsk-[A-Za-z0-9_-]+\b`)
400
+ bearerPattern = regexp.MustCompile(`(?i)(bearer\s+)([A-Za-z0-9._:-]+)`)
401
+ assignTokenPattern = regexp.MustCompile(`(?i)\b([A-Z0-9_]*(TOKEN|KEY|SECRET|PASSPHRASE)[A-Z0-9_]*=)([^ \t"'\x60]+)`)
402
+)
403
+
404
+func scrubSecrets(s string) string {
405
+ s = secretHexPattern.ReplaceAllString(s, "[redacted]")
406
+ s = secretKeyPattern.ReplaceAllString(s, "[redacted]")
407
+ s = bearerPattern.ReplaceAllStringFunc(s, func(m string) string {
408
+ parts := bearerPattern.FindStringSubmatch(m)
409
+ return parts[1] + "[redacted]"
410
+ })
411
+ s = assignTokenPattern.ReplaceAllString(s, "${1}[redacted]")
412
+ return s
413
+}
414
+```
415
+
416
+---
417
+
418
+## Filtering rules for inbound messages
419
+
420
+Not every message in the channel is meant for this session. The filter must accept only messages that are **all** of the following:
421
+
422
+1. **Newer than the last check** — track a `lastCheck time.Time` per session key (see below)
423
+2. **Not from this session's own nick** — reject self-messages
424
+3. **Not from a known service bot** — reject: `bridge`, `oracle`, `sentinel`, `steward`, `scribe`, `warden`, `snitch`, `herald`, `scroll`, `systembot`, `auditbot`
425
+4. **Not from an agent status nick** — reject nicks with prefixes `claude-`, `codex-`, `gemini-`
426
+5. **Explicitly mentioning this session nick** — the message text must contain the nick as a word boundary match, not just as a substring
427
+
428
+```go
429
+var serviceBots = map[string]struct{}{
430
+ "bridge": {}, "oracle": {}, "sentinel": {}, "steward": {},
431
+ "scribe": {}, "warden": {}, "snitch": {}, "herald": {},
432
+ "scroll": {}, "systembot": {}, "auditbot": {},
433
+}
434
+
435
+var agentPrefixes = []string{"claude-", "codex-", "gemini-"}
436
+
437
+func filterInbound(msgs []sessionrelay.Message, selfNick string) []sessionrelay.Message {
438
+ var out []sessionrelay.Message
439
+ mentionRe := regexp.MustCompile(
440
+ `(^|[^[:alnum:]_./\\-])` + regexp.QuoteMeta(selfNick) + `($|[^[:alnum:]_./\\-])`,
441
+ )
442
+ for _, m := range msgs {
443
+ if m.Nick == selfNick {
444
+ continue
445
+ }
446
+ if _, ok := serviceBots[m.Nick]; ok {
447
+ continue
448
+ }
449
+ isAgentNick := false
450
+ for _, p := range agentPrefixes {
451
+ if strings.HasPrefix(m.Nick, p) {
452
+ isAgentNick = true
453
+ break
454
+ }
455
+ }
456
+ if isAgentNick {
457
+ continue
458
+ }
459
+ if !mentionRe.MatchString(m.Text) {
460
+ continue
461
+ }
462
+ out = append(out, m)
463
+ }
464
+ return out
465
+}
466
+```
467
+
468
+**Why these rules matter:**
469
+
470
+- Service bots post frequently (scribe, systembot, auditbot log every event). Letting those through would create feedback loops.
471
+- Agent nicks with runtime prefixes are other sessions' activity mirrors. They are ambient background, not operator instructions.
472
+- Word-boundary mention matching prevents `claude-myrepo-abc12345` from triggering on a message that just contains the word `claude`.
473
+
474
+**State scoping:** Do not use a single global timestamp file. Track `lastCheck` by a key derived from `channel + nick + cwd`. This prevents parallel sessions in the same channel from consuming each other's instructions:
475
+
476
+```go
477
+func stateKey(channel, nick, cwd string) string {
478
+ h := fmt.Sprintf("%s|%s|%s", channel, nick, cwd)
479
+ sum := crc32.ChecksumIEEE([]byte(h))
480
+ return fmt.Sprintf("%08x", sum)
481
+}
482
+```
483
+
484
+---
485
+
486
+## The environment contract
487
+
488
+All relay brokers use the same set of environment variables. Read from the shared env file first, then override from the process environment.
489
+
490
+**Required:**
491
+
492
+| Variable | Purpose |
493
+|----------|---------|
494
+| `SCUTTLEBOT_URL` | Base URL of the scuttlebot HTTP API (e.g. `https://scuttlebot.example.com`) |
495
+| `SCUTTLEBOT_TOKEN` | Bearer token for API auth |
496
+| `SCUTTLEBOT_CHANNEL` | Target IRC channel (with or without `#`) |
497
+
498
+**Common optional:**
499
+
500
+| Variable | Default | Purpose |
501
+|----------|---------|---------|
502
+| `SCUTTLEBOT_TRANSPORT` | `irc` | `http` (bridge path) or `irc` (direct SASL) |
503
+| `SCUTTLEBOT_NICK` | derived | Override the session nick |
504
+| `SCUTTLEBOT_SESSION_ID` | derived | Stable session ID for nick derivation |
505
+| `SCUTTLEBOT_IRC_ADDR` | `127.0.0.1:6667` | Ergo IRC address |
506
+| `SCUTTLEBOT_IRC_PASS` | — | IRC password (if different from API token) |
507
+| `SCUTTLEBOT_IRC_DELETE_ON_CLOSE` | `0` | Delete the IRC account when the session ends |
508
+| `SCUTTLEBOT_HOOKS_ENABLED` | `1` | Set to `0` to disable all IRC integration |
509
+| `SCUTTLEBOT_INTERRUPT_ON_MESSAGE` | `0` | Send SIGINT to runtime when operator message arrives |
510
+| `SCUTTLEBOT_POLL_INTERVAL` | `2s` | How often to poll for new IRC messages |
511
+| `SCUTTLEBOT_PRESENCE_HEARTBEAT` | `60s` | HTTP presence touch interval; `0` to disable |
512
+| `SCUTTLEBOT_CONFIG_FILE` | `~/.config/scuttlebot-relay.env` | Path to the shared env file |
513
+| `SCUTTLEBOT_ACTIVITY_VIA_BROKER` | `0` | Set to `1` when the broker owns activity posts (disables hook-based posting) |
514
+
515
+**Do not hardcode tokens.** The shared env file (`~/.config/scuttlebot-relay.env`) is the right place for `SCUTTLEBOT_TOKEN`. Never commit it.
516
+
517
+---
518
+
519
+## Writing the installer script
520
+
521
+The installer script lives at `skills/{runtime}-relay/scripts/install-{runtime}-relay.sh`. It:
522
+
523
+1. Writes the shared env file (`~/.config/scuttlebot-relay.env`)
524
+2. Copies hook scripts to the runtime's hook directory
525
+3. Registers hooks in the runtime's settings JSON
526
+4. Copies (or builds) the relay launcher to `~/.local/bin/{runtime}-relay`
527
+
528
+Key conventions:
529
+
530
+- Accept `--url`, `--token`, `--channel` flags
531
+- Fall back to `SCUTTLEBOT_URL`, `SCUTTLEBOT_TOKEN`, `SCUTTLEBOT_CHANNEL` env vars
532
+- Default config file to `~/.config/scuttlebot-relay.env`
533
+- Default hooks dir to `~/.{runtime}/hooks/`
534
+- Default bin dir to `~/.local/bin/`
535
+- Print a clear summary of what was written
536
+
537
+```bash
538
+#!/usr/bin/env bash
539
+set -euo pipefail
540
+
541
+SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
542
+REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd)
543
+
544
+SCUTTLEBOT_URL_VALUE="${SCUTTLEBOT_URL:-}"
545
+SCUTTLEBOT_TOKEN_VALUE="${SCUTTLEBOT_TOKEN:-}"
546
+SCUTTLEBOT_CHANNEL_VALUE="${SCUTTLEBOT_CHANNEL:-}"
547
+
548
+CONFIG_FILE="${SCUTTLEBOT_CONFIG_FILE:-$HOME/.config/scuttlebot-relay.env}"
549
+HOOKS_DIR="${RUNTIME_HOOKS_DIR:-$HOME/.{runtime}/hooks}"
550
+BIN_DIR="${BIN_DIR:-$HOME/.local/bin}"
551
+
552
+# ... flag parsing ...
553
+
554
+mkdir -p "$(dirname "$CONFIG_FILE")" "$HOOKS_DIR" "$BIN_DIR"
555
+
556
+cat > "$CONFIG_FILE" <<EOF
557
+SCUTTLEBOT_URL=${SCUTTLEBOT_URL_VALUE}
558
+SCUTTLEBOT_TOKEN=${SCUTTLEBOT_TOKEN_VALUE}
559
+SCUTTLEBOT_CHANNEL=${SCUTTLEBOT_CHANNEL_VALUE}
560
+SCUTTLEBOT_HOOKS_ENABLED=1
561
+EOF
562
+
563
+cp "$REPO_ROOT/skills/{runtime}-relay/hooks/scuttlebot-check.sh" "$HOOKS_DIR/"
564
+cp "$REPO_ROOT/skills/{runtime}-relay/hooks/scuttlebot-post.sh" "$HOOKS_DIR/"
565
+chmod +x "$HOOKS_DIR"/scuttlebot-*.sh
566
+
567
+# Register hooks in runtime settings (runtime-specific).
568
+# ...
569
+
570
+cp "$REPO_ROOT/bin/{runtime}-relay" "$BIN_DIR/{runtime}-relay"
571
+chmod +x "$BIN_DIR/{runtime}-relay"
572
+
573
+echo "Installed. Launch with: $BIN_DIR/{runtime}-relay"
574
+```
575
+
576
+---
577
+
578
+## Writing the hook scripts
579
+
580
+Hooks fire at runtime lifecycle points. For runtimes that have a broker, hooks are a **fallback** — they handle gaps like post-tool summaries when the broker's session-log mirror hasn't caught up yet.
581
+
582
+### Pre-action hook (`scuttlebot-check.sh`)
583
+
584
+Runs before each tool call. Checks IRC for operator messages and blocks the tool call if one is found.
585
+
586
+Key points:
587
+
588
+- Load the shared env file first
589
+- Derive the nick from session ID and CWD (same logic as the broker)
590
+- Compute the state key from channel + nick + CWD, read/write `lastCheck` from `/tmp/`
591
+- Fetch `GET /v1/channels/{ch}/messages` with `connect-timeout 1 max-time 2` (never block the tool loop)
592
+- Filter messages with the same rules as the broker
593
+- If an instruction exists, output `{"decision": "block", "reason": "[IRC] nick: text"}` and exit 0
594
+- If not, exit 0 with no output (tool proceeds normally)
595
+
596
+```bash
597
+messages=$(curl -sf --connect-timeout 1 --max-time 2 \
598
+ -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \
599
+ "$SCUTTLEBOT_URL/v1/channels/$SCUTTLEBOT_CHANNEL/messages" 2>/dev/null)
600
+
601
+[ -z "$messages" ] && exit 0
602
+
603
+BOTS='["bridge","oracle","sentinel","steward","scribe","warden","snitch","herald","scroll","systembot","auditbot"]'
604
+
605
+instruction=$(echo "$messages" | jq -r \
606
+ --argjson bots "$BOTS" --arg self "$SCUTTLEBOT_NICK" '
607
+ .messages[]
608
+ | select(.nick as $n |
609
+ ($bots | index($n) | not) and
610
+ ($n | startswith("claude-") | not) and
611
+ ($n | startswith("codex-") | not) and
612
+ ($n | startswith("gemini-") | not) and
613
+ $n != $self)
614
+ | "\(.at)\t\(.nick)\t\(.text)"
615
+' 2>/dev/null | while IFS=$'\t' read -r at nick text; do
616
+ # ... timestamp comparison, mention check ...
617
+ echo "$nick: $text"
618
+ done | tail -1)
619
+
620
+[ -z "$instruction" ] && exit 0
621
+echo "{\"decision\": \"block\", \"reason\": \"[IRC instruction from operator] $instruction\"}"
622
+```
623
+
624
+### Post-action hook (`scuttlebot-post.sh`)
625
+
626
+Runs after each tool call. Posts a one-line summary to IRC.
627
+
628
+Key points:
629
+
630
+- Skip if `SCUTTLEBOT_ACTIVITY_VIA_BROKER=1` — the broker already owns activity posting
631
+- Skip if `SCUTTLEBOT_HOOKS_ENABLED=0` or token is empty
632
+- Parse the tool name and key input from stdin JSON
633
+- Build a short human-readable summary (under 120 chars)
634
+- `POST /v1/channels/{ch}/messages` with `connect-timeout 1 max-time 2`
635
+- Exit 0 always (never block the tool)
636
+
637
+Example summaries by tool:
638
+
639
+| Tool | Summary format |
640
+|------|---------------|
641
+| `Bash` | `› {command[:120]}` |
642
+| `Read` | `read {relative-path}` |
643
+| `Edit` | `edit {relative-path}` |
644
+| `Write` | `write {relative-path}` |
645
+| `Glob` | `glob {pattern}` |
646
+| `Grep` | `grep "{pattern}"` |
647
+| `Agent` | `spawn agent: {description[:80]}` |
648
+| Other | `{tool_name}` |
649
+
650
+---
651
+
652
+## The smoke test checklist
653
+
654
+Every adapter must pass this test before it is considered complete:
655
+
656
+1. **Online presence** — launch the runtime or broker; confirm `{nick} online` appears in the IRC channel within a few seconds
657
+2. **Tool activity mirror** — trigger one harmless tool call (e.g. list files); confirm a mirrored one-liner appears in the channel
658
+3. **Operator inject** — from an IRC client, send a message mentioning the session nick (e.g. `claude-myrepo-abc12345: please stop`); confirm the runtime surfaces it as a blocking instruction or injects it into stdin
659
+4. **Offline presence** — exit the runtime; confirm `{nick} offline` appears in the channel
660
+5. **Soft-fail** — stop scuttlebot and launch the runtime; confirm it starts normally and the relay exits gracefully
661
+
662
+If any of these fail, the adapter is not finished.
663
+
664
+---
665
+
666
+## Common mistakes
667
+
668
+### Duplicate activity posts
669
+
670
+If the broker mirrors the session log AND the post-hook fires for the same tool call, operators see every action twice.
671
+
672
+**Fix:** Set `SCUTTLEBOT_ACTIVITY_VIA_BROKER=1` in the env file when the broker is active. The post-hook checks this variable and exits early:
673
+
674
+```bash
675
+[ "${SCUTTLEBOT_ACTIVITY_VIA_BROKER:-0}" = "1" ] && exit 0
676
+```
677
+
678
+### Parallel session interference
679
+
680
+If two sessions in the same repo and channel use a single shared `lastCheck` timestamp file, one session will consume instructions meant for the other.
681
+
682
+**Fix:** Key the state file by `channel + nick + cwd` (see "State scoping" above). Each session gets its own file under `/tmp/`.
683
+
684
+### Secrets in activity output
685
+
686
+Session logs may contain tokens, passphrases, or API keys in command output or assistant text. Posting these to IRC leaks them to everyone in the channel.
687
+
688
+**Fix:** Always run the scrubber on any line before posting. Redact: long hex strings (`[a-f0-9]{32,}`), `sk-*` key patterns, `Bearer <token>` patterns, and `VAR=value` assignments for names containing `TOKEN`, `KEY`, `SECRET`, or `PASSPHRASE`.
689
+
690
+### Missing word-boundary check for mentions
691
+
692
+A check like `echo "$text" | grep -q "$nick"` will match `claude-myrepo-abc12345` inside `re-claude-myrepo-abc12345d` or as part of a URL. Use the word-boundary regex from the filtering rules section.
693
+
694
+### Blocking the tool loop
695
+
696
+The pre-action hook runs synchronously before every tool call. If it hangs (e.g. scuttlebot is slow or unreachable), it delays every action indefinitely.
697
+
698
+**Fix:** Always use `--connect-timeout 1 --max-time 2` in curl calls. Exit 0 immediately on any curl error. The relay is a best-effort observer — it must never impede the runtime.
--- a/docs/guide/adding-agents.md
+++ b/docs/guide/adding-agents.md
@@ -0,0 +1,698 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/docs/guide/adding-agents.md
+++ b/docs/guide/adding-agents.md
@@ -0,0 +1,698 @@
1 # Adding a New Agent Runtime
2
3 This guide explains how to add a new agent runtime — a coding assistant, automation tool, or any interactive terminal process — to the scuttlebot relay ecosystem.
4
5 The relay ecosystem has two shapes. Read the next section to decide which one you need, then follow the corresponding path.
6
7 ---
8
9 ## Relay broker vs. IRC-resident agent
10
11 **Use a relay broker** when:
12
13 - The runtime is an interactive terminal session (Claude Code, Codex, Gemini CLI, etc.)
14 - Sessions are ephemeral — they start and stop with each coding task
15 - You want per-session presence (`online`/`offline`) and per-session operator instructions
16 - The runtime exposes a session log, hook points, or a PTY you can wrap
17
18 **Use an IRC-resident agent** when:
19
20 - The process should run indefinitely (a moderator, an event router, a summarizer)
21 - Presence and identity are permanent, not per-session
22 - You are building a new system bot in the style of `oracle`, `warden`, or `herald`
23
24 For IRC-resident agents, use `pkg/ircagent/` as your foundation and follow the system bot pattern in `internal/bots/`. This guide focuses on the **relay broker** pattern.
25
26 ---
27
28 ## Canonical repo layout
29
30 Every terminal broker follows this layout:
31
32 ```
33 cmd/{runtime}-relay/
34 main.go broker entrypoint
35 skills/{runtime}-relay/
36 install.md human install primer
37 FLEET.md rollout and operations guide
38 hooks/
39 README.md runtime-specific hook contract
40 scuttlebot-check.sh pre-action hook (check IRC for instructions)
41 scuttlebot-post.sh post-action hook (post tool activity to IRC)
42 scripts/
43 install-{runtime}-relay.sh tracked installer
44 pkg/sessionrelay/ shared transport (do not copy; import)
45 ```
46
47 Files installed into `~/.{runtime}/`, `~/.local/bin/`, or `~/.config/` are **copies**. The repo is the source of truth.
48
49 ---
50
51 ## Step-by-step: implementing the broker
52
53 ### 1. Start from `pkg/sessionrelay`
54
55 `pkg/sessionrelay` provides the `Connector` interface and two implementations:
56
57 ```go
58 type Connector interface {
59 Connect(ctx context.Context) error
60 Post(ctx context.Context, text string) error
61 MessagesSince(ctx context.Context, since time.Time) ([]Message, error)
62 Touch(ctx context.Context) error
63 Close(ctx context.Context) error
64 }
65 ```
66
67 Instantiate with:
68
69 ```go
70 conn, err := sessionrelay.New(sessionrelay.Config{
71 Transport: sessionrelay.TransportIRC, // or TransportHTTP
72 URL: cfg.URL,
73 Token: cfg.Token,
74 Channel: cfg.Channel,
75 Nick: cfg.Nick,
76 IRC: sessionrelay.IRCConfig{
77 Addr: cfg.IRCAddr,
78 Pass: cfg.IRCPass,
79 AgentType: "worker",
80 DeleteOnClose: cfg.IRCDeleteOnClose,
81 },
82 })
83 ```
84
85 `TransportHTTP` routes all posts through the bridge bot (`POST /v1/channels/{ch}/messages`). `TransportIRC` self-registers as an agent and connects directly to Ergo via SASL — the broker appears as its own IRC nick.
86
87 ### 2. Define your config struct
88
89 ```go
90 type config struct {
91 // Required
92 URL string
93 Token string
94 Channel string
95 Nick string
96
97 // Transport
98 Transport sessionrelay.Transport
99 IRCAddr string
100 IRCPass string
101 IRCDeleteOnClose bool
102
103 // Tuning
104 PollInterval time.Duration
105 HeartbeatInterval time.Duration
106 InterruptOnMessage bool
107 HooksEnabled bool
108
109 // Runtime-specific
110 RuntimeBin string
111 Args []string
112 TargetCWD string
113 }
114 ```
115
116 ### 3. Implement `loadConfig`
117
118 Read from environment variables, then from a shared env file (`~/.config/scuttlebot-relay.env`), then apply defaults:
119
120 ```go
121 func loadConfig() config {
122 cfgFile := envOr("SCUTTLEBOT_CONFIG_FILE",
123 filepath.Join(os.Getenv("HOME"), ".config/scuttlebot-relay.env"))
124 loadEnvFile(cfgFile)
125
126 transport := sessionrelay.Transport(envOr("SCUTTLEBOT_TRANSPORT", "irc"))
127
128 return config{
129 URL: envOr("SCUTTLEBOT_URL", "http://localhost:8080"),
130 Token: os.Getenv("SCUTTLEBOT_TOKEN"),
131 Channel: envOr("SCUTTLEBOT_CHANNEL", "general"),
132 Nick: os.Getenv("SCUTTLEBOT_NICK"), // derived below if empty
133 Transport: transport,
134 IRCAddr: envOr("SCUTTLEBOT_IRC_ADDR", "127.0.0.1:6667"),
135 IRCPass: os.Getenv("SCUTTLEBOT_IRC_PASS"),
136 IRCDeleteOnClose: os.Getenv("SCUTTLEBOT_IRC_DELETE_ON_CLOSE") == "1",
137 HooksEnabled: envOr("SCUTTLEBOT_HOOKS_ENABLED", "1") != "0",
138 InterruptOnMessage: os.Getenv("SCUTTLEBOT_INTERRUPT_ON_MESSAGE") == "1",
139 PollInterval: parseDuration("SCUTTLEBOT_POLL_INTERVAL", 2*time.Second),
140 HeartbeatInterval: parseDuration("SCUTTLEBOT_PRESENCE_HEARTBEAT", 60*time.Second),
141 }
142 }
143 ```
144
145 ### 4. Derive the session nick
146
147 ```go
148 func deriveNick(runtime, cwd string) string {
149 // Sanitize the repo directory name.
150 base := sanitize(filepath.Base(cwd))
151 // Stable 8-char hex from pid + ppid + current time.
152 h := crc32.NewIEEE()
153 fmt.Fprintf(h, "%d%d%d", os.Getpid(), os.Getppid(), time.Now().UnixNano())
154 suffix := fmt.Sprintf("%08x", h.Sum32())
155 return fmt.Sprintf("%s-%s-%s", runtime, base, suffix[:8])
156 }
157
158 func sanitize(s string) string {
159 re := regexp.MustCompile(`[^a-zA-Z0-9_-]+`)
160 return re.ReplaceAllString(s, "-")
161 }
162 ```
163
164 Nick format: `{runtime}-{basename}-{session_id[:8]}`
165
166 For runtimes that expose a stable session UUID (like Claude Code), prefer that over the PID-based suffix.
167
168 ### 5. Implement `run`
169
170 The top-level `run` function wires everything together:
171
172 ```go
173 func run(ctx context.Context, cfg config) error {
174 conn, err := sessionrelay.New(sessionrelay.Config{ /* ... */ })
175 if err != nil {
176 return fmt.Errorf("relay: connect: %w", err)
177 }
178
179 if err := conn.Connect(ctx); err != nil {
180 // Soft-fail: log, then start the runtime anyway.
181 log.Printf("relay: scuttlebot unreachable, running without relay: %v", err)
182 return runRuntimeDirect(ctx, cfg)
183 }
184 defer conn.Close(ctx)
185
186 // Announce presence.
187 _ = conn.Post(ctx, cfg.Nick+" online")
188
189 // Start the runtime under a PTY.
190 ptmx, cmd, err := startRuntime(cfg)
191 if err != nil {
192 return fmt.Errorf("relay: start runtime: %w", err)
193 }
194
195 var wg sync.WaitGroup
196
197 // Mirror runtime output → IRC.
198 wg.Add(1)
199 go func() {
200 defer wg.Done()
201 mirrorSessionLoop(ctx, cfg, conn, sessionDir(cfg))
202 }()
203
204 // Poll IRC → inject into runtime.
205 wg.Add(1)
206 go func() {
207 defer wg.Done()
208 relayInputLoop(ctx, cfg, conn, ptmx)
209 }()
210
211 // Wait for runtime to exit.
212 _ = cmd.Wait()
213 _ = conn.Post(ctx, cfg.Nick+" offline")
214 wg.Wait()
215 return nil
216 }
217 ```
218
219 ### 6. Implement `mirrorSessionLoop`
220
221 This goroutine tails the runtime's session JSONL log and posts summarized activity to IRC.
222
223 ```go
224 func mirrorSessionLoop(ctx context.Context, cfg config, conn sessionrelay.Connector, dir string) {
225 ticker := time.NewTicker(250 * time.Millisecond)
226 defer ticker.Stop()
227
228 var lastPos int64
229
230 for {
231 select {
232 case <-ctx.Done():
233 return
234 case <-ticker.C:
235 file := latestSessionFile(dir)
236 if file == "" {
237 continue
238 }
239 lines, pos := readNewLines(file, lastPos)
240 lastPos = pos
241 for _, line := range lines {
242 if msg := extractActivityLine(line); msg != "" {
243 _ = conn.Post(ctx, msg)
244 }
245 }
246 }
247 }
248 }
249 ```
250
251 ### 7. Implement `relayInputLoop`
252
253 This goroutine polls the IRC channel for operator messages and injects them into the runtime.
254
255 ```go
256 func relayInputLoop(ctx context.Context, cfg config, conn sessionrelay.Connector, ptmx *os.File) {
257 ticker := time.NewTicker(cfg.PollInterval)
258 defer ticker.Stop()
259
260 var lastCheck time.Time
261
262 for {
263 select {
264 case <-ctx.Done():
265 return
266 case <-ticker.C:
267 msgs, err := conn.MessagesSince(ctx, lastCheck)
268 if err != nil {
269 continue
270 }
271 lastCheck = time.Now()
272 for _, m := range filterInbound(msgs, cfg.Nick) {
273 injectInstruction(ptmx, m.Text)
274 }
275 }
276 }
277 }
278 ```
279
280 ---
281
282 ## Session file discovery
283
284 Each runtime stores its session data in a different location:
285
286 | Runtime | Session log location |
287 |---------|---------------------|
288 | Claude Code | `~/.claude/projects/{cwd-hash}/` — JSONL files named by session UUID |
289 | Codex | `~/.codex/sessions/{session-id}.jsonl` |
290 | Gemini CLI | `~/.gemini/sessions/{session-id}.jsonl` |
291
292 To find the latest session file:
293
294 ```go
295 func latestSessionFile(dir string) string {
296 entries, _ := os.ReadDir(dir)
297 var newest os.DirEntry
298 for _, e := range entries {
299 if !strings.HasSuffix(e.Name(), ".jsonl") {
300 continue
301 }
302 if newest == nil {
303 newest = e
304 continue
305 }
306 ni, _ := newest.Info()
307 ei, _ := e.Info()
308 if ei.ModTime().After(ni.ModTime()) {
309 newest = e
310 }
311 }
312 if newest == nil {
313 return ""
314 }
315 return filepath.Join(dir, newest.Name())
316 }
317 ```
318
319 For Claude Code specifically, the project directory is derived from the working directory path — see `cmd/claude-relay/main.go` for the exact hashing logic.
320
321 ---
322
323 ## Message parsing — Claude Code JSONL format
324
325 Each line in a Claude Code session file is a JSON object. The fields you care about:
326
327 ```json
328 {
329 "type": "assistant",
330 "sessionId": "550e8400-...",
331 "cwd": "/Users/alice/repos/myproject",
332 "message": {
333 "role": "assistant",
334 "content": [
335 {
336 "type": "tool_use",
337 "name": "Bash",
338 "input": { "command": "go test ./..." }
339 }
340 ]
341 }
342 }
343 ```
344
345 ```json
346 {
347 "type": "user",
348 "message": {
349 "role": "user",
350 "content": [
351 {
352 "type": "tool_result",
353 "content": [{ "type": "text", "text": "ok github.com/..." }]
354 }
355 ]
356 }
357 }
358 ```
359
360 ```json
361 {
362 "type": "result",
363 "subtype": "success"
364 }
365 ```
366
367 **Extracting activity lines:**
368
369 ```go
370 func extractActivityLine(jsonLine string) string {
371 var entry claudeSessionEntry
372 if err := json.Unmarshal([]byte(jsonLine), &entry); err != nil {
373 return ""
374 }
375 if entry.Type != "assistant" {
376 return ""
377 }
378 for _, block := range entry.Message.Content {
379 switch block.Type {
380 case "tool_use":
381 return summarizeToolUse(block.Name, block.Input)
382 case "text":
383 if block.Text != "" {
384 return truncate(block.Text, 360)
385 }
386 }
387 }
388 return ""
389 }
390 ```
391
392 For other runtimes, identify the equivalent fields in their session format. Codex and Gemini use similar but not identical schemas — read their session files and map accordingly.
393
394 **Secret scrubbing:** Before posting any line to IRC, run it through a scrubber:
395
396 ```go
397 var (
398 secretHexPattern = regexp.MustCompile(`\b[a-f0-9]{32,}\b`)
399 secretKeyPattern = regexp.MustCompile(`\bsk-[A-Za-z0-9_-]+\b`)
400 bearerPattern = regexp.MustCompile(`(?i)(bearer\s+)([A-Za-z0-9._:-]+)`)
401 assignTokenPattern = regexp.MustCompile(`(?i)\b([A-Z0-9_]*(TOKEN|KEY|SECRET|PASSPHRASE)[A-Z0-9_]*=)([^ \t"'\x60]+)`)
402 )
403
404 func scrubSecrets(s string) string {
405 s = secretHexPattern.ReplaceAllString(s, "[redacted]")
406 s = secretKeyPattern.ReplaceAllString(s, "[redacted]")
407 s = bearerPattern.ReplaceAllStringFunc(s, func(m string) string {
408 parts := bearerPattern.FindStringSubmatch(m)
409 return parts[1] + "[redacted]"
410 })
411 s = assignTokenPattern.ReplaceAllString(s, "${1}[redacted]")
412 return s
413 }
414 ```
415
416 ---
417
418 ## Filtering rules for inbound messages
419
420 Not every message in the channel is meant for this session. The filter must accept only messages that are **all** of the following:
421
422 1. **Newer than the last check** — track a `lastCheck time.Time` per session key (see below)
423 2. **Not from this session's own nick** — reject self-messages
424 3. **Not from a known service bot** — reject: `bridge`, `oracle`, `sentinel`, `steward`, `scribe`, `warden`, `snitch`, `herald`, `scroll`, `systembot`, `auditbot`
425 4. **Not from an agent status nick** — reject nicks with prefixes `claude-`, `codex-`, `gemini-`
426 5. **Explicitly mentioning this session nick** — the message text must contain the nick as a word boundary match, not just as a substring
427
428 ```go
429 var serviceBots = map[string]struct{}{
430 "bridge": {}, "oracle": {}, "sentinel": {}, "steward": {},
431 "scribe": {}, "warden": {}, "snitch": {}, "herald": {},
432 "scroll": {}, "systembot": {}, "auditbot": {},
433 }
434
435 var agentPrefixes = []string{"claude-", "codex-", "gemini-"}
436
437 func filterInbound(msgs []sessionrelay.Message, selfNick string) []sessionrelay.Message {
438 var out []sessionrelay.Message
439 mentionRe := regexp.MustCompile(
440 `(^|[^[:alnum:]_./\\-])` + regexp.QuoteMeta(selfNick) + `($|[^[:alnum:]_./\\-])`,
441 )
442 for _, m := range msgs {
443 if m.Nick == selfNick {
444 continue
445 }
446 if _, ok := serviceBots[m.Nick]; ok {
447 continue
448 }
449 isAgentNick := false
450 for _, p := range agentPrefixes {
451 if strings.HasPrefix(m.Nick, p) {
452 isAgentNick = true
453 break
454 }
455 }
456 if isAgentNick {
457 continue
458 }
459 if !mentionRe.MatchString(m.Text) {
460 continue
461 }
462 out = append(out, m)
463 }
464 return out
465 }
466 ```
467
468 **Why these rules matter:**
469
470 - Service bots post frequently (scribe, systembot, auditbot log every event). Letting those through would create feedback loops.
471 - Agent nicks with runtime prefixes are other sessions' activity mirrors. They are ambient background, not operator instructions.
472 - Word-boundary mention matching prevents `claude-myrepo-abc12345` from triggering on a message that just contains the word `claude`.
473
474 **State scoping:** Do not use a single global timestamp file. Track `lastCheck` by a key derived from `channel + nick + cwd`. This prevents parallel sessions in the same channel from consuming each other's instructions:
475
476 ```go
477 func stateKey(channel, nick, cwd string) string {
478 h := fmt.Sprintf("%s|%s|%s", channel, nick, cwd)
479 sum := crc32.ChecksumIEEE([]byte(h))
480 return fmt.Sprintf("%08x", sum)
481 }
482 ```
483
484 ---
485
486 ## The environment contract
487
488 All relay brokers use the same set of environment variables. Read from the shared env file first, then override from the process environment.
489
490 **Required:**
491
492 | Variable | Purpose |
493 |----------|---------|
494 | `SCUTTLEBOT_URL` | Base URL of the scuttlebot HTTP API (e.g. `https://scuttlebot.example.com`) |
495 | `SCUTTLEBOT_TOKEN` | Bearer token for API auth |
496 | `SCUTTLEBOT_CHANNEL` | Target IRC channel (with or without `#`) |
497
498 **Common optional:**
499
500 | Variable | Default | Purpose |
501 |----------|---------|---------|
502 | `SCUTTLEBOT_TRANSPORT` | `irc` | `http` (bridge path) or `irc` (direct SASL) |
503 | `SCUTTLEBOT_NICK` | derived | Override the session nick |
504 | `SCUTTLEBOT_SESSION_ID` | derived | Stable session ID for nick derivation |
505 | `SCUTTLEBOT_IRC_ADDR` | `127.0.0.1:6667` | Ergo IRC address |
506 | `SCUTTLEBOT_IRC_PASS` | — | IRC password (if different from API token) |
507 | `SCUTTLEBOT_IRC_DELETE_ON_CLOSE` | `0` | Delete the IRC account when the session ends |
508 | `SCUTTLEBOT_HOOKS_ENABLED` | `1` | Set to `0` to disable all IRC integration |
509 | `SCUTTLEBOT_INTERRUPT_ON_MESSAGE` | `0` | Send SIGINT to runtime when operator message arrives |
510 | `SCUTTLEBOT_POLL_INTERVAL` | `2s` | How often to poll for new IRC messages |
511 | `SCUTTLEBOT_PRESENCE_HEARTBEAT` | `60s` | HTTP presence touch interval; `0` to disable |
512 | `SCUTTLEBOT_CONFIG_FILE` | `~/.config/scuttlebot-relay.env` | Path to the shared env file |
513 | `SCUTTLEBOT_ACTIVITY_VIA_BROKER` | `0` | Set to `1` when the broker owns activity posts (disables hook-based posting) |
514
515 **Do not hardcode tokens.** The shared env file (`~/.config/scuttlebot-relay.env`) is the right place for `SCUTTLEBOT_TOKEN`. Never commit it.
516
517 ---
518
519 ## Writing the installer script
520
521 The installer script lives at `skills/{runtime}-relay/scripts/install-{runtime}-relay.sh`. It:
522
523 1. Writes the shared env file (`~/.config/scuttlebot-relay.env`)
524 2. Copies hook scripts to the runtime's hook directory
525 3. Registers hooks in the runtime's settings JSON
526 4. Copies (or builds) the relay launcher to `~/.local/bin/{runtime}-relay`
527
528 Key conventions:
529
530 - Accept `--url`, `--token`, `--channel` flags
531 - Fall back to `SCUTTLEBOT_URL`, `SCUTTLEBOT_TOKEN`, `SCUTTLEBOT_CHANNEL` env vars
532 - Default config file to `~/.config/scuttlebot-relay.env`
533 - Default hooks dir to `~/.{runtime}/hooks/`
534 - Default bin dir to `~/.local/bin/`
535 - Print a clear summary of what was written
536
537 ```bash
538 #!/usr/bin/env bash
539 set -euo pipefail
540
541 SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
542 REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd)
543
544 SCUTTLEBOT_URL_VALUE="${SCUTTLEBOT_URL:-}"
545 SCUTTLEBOT_TOKEN_VALUE="${SCUTTLEBOT_TOKEN:-}"
546 SCUTTLEBOT_CHANNEL_VALUE="${SCUTTLEBOT_CHANNEL:-}"
547
548 CONFIG_FILE="${SCUTTLEBOT_CONFIG_FILE:-$HOME/.config/scuttlebot-relay.env}"
549 HOOKS_DIR="${RUNTIME_HOOKS_DIR:-$HOME/.{runtime}/hooks}"
550 BIN_DIR="${BIN_DIR:-$HOME/.local/bin}"
551
552 # ... flag parsing ...
553
554 mkdir -p "$(dirname "$CONFIG_FILE")" "$HOOKS_DIR" "$BIN_DIR"
555
556 cat > "$CONFIG_FILE" <<EOF
557 SCUTTLEBOT_URL=${SCUTTLEBOT_URL_VALUE}
558 SCUTTLEBOT_TOKEN=${SCUTTLEBOT_TOKEN_VALUE}
559 SCUTTLEBOT_CHANNEL=${SCUTTLEBOT_CHANNEL_VALUE}
560 SCUTTLEBOT_HOOKS_ENABLED=1
561 EOF
562
563 cp "$REPO_ROOT/skills/{runtime}-relay/hooks/scuttlebot-check.sh" "$HOOKS_DIR/"
564 cp "$REPO_ROOT/skills/{runtime}-relay/hooks/scuttlebot-post.sh" "$HOOKS_DIR/"
565 chmod +x "$HOOKS_DIR"/scuttlebot-*.sh
566
567 # Register hooks in runtime settings (runtime-specific).
568 # ...
569
570 cp "$REPO_ROOT/bin/{runtime}-relay" "$BIN_DIR/{runtime}-relay"
571 chmod +x "$BIN_DIR/{runtime}-relay"
572
573 echo "Installed. Launch with: $BIN_DIR/{runtime}-relay"
574 ```
575
576 ---
577
578 ## Writing the hook scripts
579
580 Hooks fire at runtime lifecycle points. For runtimes that have a broker, hooks are a **fallback** — they handle gaps like post-tool summaries when the broker's session-log mirror hasn't caught up yet.
581
582 ### Pre-action hook (`scuttlebot-check.sh`)
583
584 Runs before each tool call. Checks IRC for operator messages and blocks the tool call if one is found.
585
586 Key points:
587
588 - Load the shared env file first
589 - Derive the nick from session ID and CWD (same logic as the broker)
590 - Compute the state key from channel + nick + CWD, read/write `lastCheck` from `/tmp/`
591 - Fetch `GET /v1/channels/{ch}/messages` with `connect-timeout 1 max-time 2` (never block the tool loop)
592 - Filter messages with the same rules as the broker
593 - If an instruction exists, output `{"decision": "block", "reason": "[IRC] nick: text"}` and exit 0
594 - If not, exit 0 with no output (tool proceeds normally)
595
596 ```bash
597 messages=$(curl -sf --connect-timeout 1 --max-time 2 \
598 -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \
599 "$SCUTTLEBOT_URL/v1/channels/$SCUTTLEBOT_CHANNEL/messages" 2>/dev/null)
600
601 [ -z "$messages" ] && exit 0
602
603 BOTS='["bridge","oracle","sentinel","steward","scribe","warden","snitch","herald","scroll","systembot","auditbot"]'
604
605 instruction=$(echo "$messages" | jq -r \
606 --argjson bots "$BOTS" --arg self "$SCUTTLEBOT_NICK" '
607 .messages[]
608 | select(.nick as $n |
609 ($bots | index($n) | not) and
610 ($n | startswith("claude-") | not) and
611 ($n | startswith("codex-") | not) and
612 ($n | startswith("gemini-") | not) and
613 $n != $self)
614 | "\(.at)\t\(.nick)\t\(.text)"
615 ' 2>/dev/null | while IFS=$'\t' read -r at nick text; do
616 # ... timestamp comparison, mention check ...
617 echo "$nick: $text"
618 done | tail -1)
619
620 [ -z "$instruction" ] && exit 0
621 echo "{\"decision\": \"block\", \"reason\": \"[IRC instruction from operator] $instruction\"}"
622 ```
623
624 ### Post-action hook (`scuttlebot-post.sh`)
625
626 Runs after each tool call. Posts a one-line summary to IRC.
627
628 Key points:
629
630 - Skip if `SCUTTLEBOT_ACTIVITY_VIA_BROKER=1` — the broker already owns activity posting
631 - Skip if `SCUTTLEBOT_HOOKS_ENABLED=0` or token is empty
632 - Parse the tool name and key input from stdin JSON
633 - Build a short human-readable summary (under 120 chars)
634 - `POST /v1/channels/{ch}/messages` with `connect-timeout 1 max-time 2`
635 - Exit 0 always (never block the tool)
636
637 Example summaries by tool:
638
639 | Tool | Summary format |
640 |------|---------------|
641 | `Bash` | `› {command[:120]}` |
642 | `Read` | `read {relative-path}` |
643 | `Edit` | `edit {relative-path}` |
644 | `Write` | `write {relative-path}` |
645 | `Glob` | `glob {pattern}` |
646 | `Grep` | `grep "{pattern}"` |
647 | `Agent` | `spawn agent: {description[:80]}` |
648 | Other | `{tool_name}` |
649
650 ---
651
652 ## The smoke test checklist
653
654 Every adapter must pass this test before it is considered complete:
655
656 1. **Online presence** — launch the runtime or broker; confirm `{nick} online` appears in the IRC channel within a few seconds
657 2. **Tool activity mirror** — trigger one harmless tool call (e.g. list files); confirm a mirrored one-liner appears in the channel
658 3. **Operator inject** — from an IRC client, send a message mentioning the session nick (e.g. `claude-myrepo-abc12345: please stop`); confirm the runtime surfaces it as a blocking instruction or injects it into stdin
659 4. **Offline presence** — exit the runtime; confirm `{nick} offline` appears in the channel
660 5. **Soft-fail** — stop scuttlebot and launch the runtime; confirm it starts normally and the relay exits gracefully
661
662 If any of these fail, the adapter is not finished.
663
664 ---
665
666 ## Common mistakes
667
668 ### Duplicate activity posts
669
670 If the broker mirrors the session log AND the post-hook fires for the same tool call, operators see every action twice.
671
672 **Fix:** Set `SCUTTLEBOT_ACTIVITY_VIA_BROKER=1` in the env file when the broker is active. The post-hook checks this variable and exits early:
673
674 ```bash
675 [ "${SCUTTLEBOT_ACTIVITY_VIA_BROKER:-0}" = "1" ] && exit 0
676 ```
677
678 ### Parallel session interference
679
680 If two sessions in the same repo and channel use a single shared `lastCheck` timestamp file, one session will consume instructions meant for the other.
681
682 **Fix:** Key the state file by `channel + nick + cwd` (see "State scoping" above). Each session gets its own file under `/tmp/`.
683
684 ### Secrets in activity output
685
686 Session logs may contain tokens, passphrases, or API keys in command output or assistant text. Posting these to IRC leaks them to everyone in the channel.
687
688 **Fix:** Always run the scrubber on any line before posting. Redact: long hex strings (`[a-f0-9]{32,}`), `sk-*` key patterns, `Bearer <token>` patterns, and `VAR=value` assignments for names containing `TOKEN`, `KEY`, `SECRET`, or `PASSPHRASE`.
689
690 ### Missing word-boundary check for mentions
691
692 A check like `echo "$text" | grep -q "$nick"` will match `claude-myrepo-abc12345` inside `re-claude-myrepo-abc12345d` or as part of a URL. Use the word-boundary regex from the filtering rules section.
693
694 ### Blocking the tool loop
695
696 The pre-action hook runs synchronously before every tool call. If it hangs (e.g. scuttlebot is slow or unreachable), it delays every action indefinitely.
697
698 **Fix:** Always use `--connect-timeout 1 --max-time 2` in curl calls. Exit 0 immediately on any curl error. The relay is a best-effort observer — it must never impede the runtime.
+454 -3
--- docs/guide/bots.md
+++ docs/guide/bots.md
@@ -1,6 +1,457 @@
1
+# Built-in Bots
2
+
3
+scuttlebot ships eleven built-in bots. Every bot is an IRC client — it connects to the embedded Ergo server under its own registered nick, joins channels, and communicates via PRIVMSG and NOTICE exactly like any other agent. This means every action a bot takes is visible in IRC and captured by scribe.
4
+
5
+Bots are managed by the bot manager (`internal/bots/manager/`). The manager starts and stops bots automatically based on the daemon's policy configuration. Most bots start on daemon startup; a few (sentinel, steward) require explicit opt-in via config.
6
+
7
+---
8
+
9
+## bridge
10
+
11
+**Always-on.** The IRC↔HTTP bridge that powers the web UI and the REST channel API.
12
+
13
+### What it does
14
+
15
+- Joins all configured channels on startup
16
+- Buffers the last N messages per channel in a ring buffer
17
+- Streams live messages to the web UI via Server-Sent Events (SSE)
18
+- Accepts POST requests from the web UI and injects them into IRC as PRIVMSG
19
+- Tracks online users per channel; HTTP-bridge senders appear in the user list for a configurable TTL after their last post
20
+
21
+### Config
22
+
23
+```yaml
24
+bridge:
25
+ enabled: true # default: true
26
+ nick: bridge # IRC nick; default: "bridge"
27
+ channels:
28
+ - "#general"
29
+ - "#fleet"
30
+ buffer_size: 200 # messages to keep per channel; default: 200
31
+ web_user_ttl_minutes: 5 # how long HTTP-bridge nicks stay in /users; default: 5
32
+```
33
+
34
+### API
35
+
36
+The bridge exposes these endpoints (all require Bearer token auth):
37
+
38
+| Method | Path | Description |
39
+|--------|------|-------------|
40
+| `GET` | `/v1/channels` | List channels the bridge has joined |
41
+| `GET` | `/v1/channels/{channel}/messages` | Recent buffered messages |
42
+| `GET` | `/v1/channels/{channel}/users` | Current online users |
43
+| `POST` | `/v1/channels/{channel}/messages` | Post a message to the channel |
44
+| `GET` | `/v1/channels/{channel}/stream` | SSE live message stream |
45
+
46
+---
47
+
48
+## scribe
49
+
50
+**Structured message logger.** Captures all channel PRIVMSG traffic to a queryable store.
51
+
52
+### What it does
53
+
54
+- Joins all configured channels and listens for PRIVMSG
55
+- Parses each message as a protocol envelope (JSON). Valid envelopes are stored with their `type` and `id` fields. Malformed messages are stored as raw entries — scribe never crashes on bad input
56
+- NOTICE messages are intentionally ignored (system/bot commentary)
57
+- Provides a `Store` interface used by `scroll` and `oracle` for history replay and summarization
58
+
59
+### Config
60
+
61
+Scribe is enabled via the `bots.scribe` block:
62
+
63
+```yaml
64
+bots:
65
+ scribe:
66
+ enabled: true
67
+```
68
+
69
+Scribe automatically joins the same channels as the bridge.
70
+
71
+### IRC behavior
72
+
73
+Scribe does not post to channels. It only listens.
74
+
75
+---
76
+
77
+## oracle
78
+
79
+**LLM-powered channel summarizer.** Provides on-demand summaries of recent channel history.
80
+
81
+### What it does
82
+
83
+oracle answers DMs from agents or humans requesting a summary of a channel. It fetches recent messages from scribe's store, builds a prompt, calls the configured LLM backend, and replies via PM NOTICE.
84
+
85
+### Command format
86
+
87
+Send oracle a direct message:
88
+
89
+```
90
+PRIVMSG oracle :summarize #channel [last=N] [format=toon|json]
91
+```
92
+
93
+| Parameter | Default | Description |
94
+|-----------|---------|-------------|
95
+| `#channel` | required | The channel to summarize |
96
+| `last=N` | 50 | Number of recent messages to include (max 200) |
97
+| `format=toon` | `toon` | Output format: `toon` (token-efficient) or `json` |
98
+
99
+**Example:**
100
+
101
+```
102
+PRIVMSG oracle :summarize #general last=100 format=json
103
+```
104
+
105
+oracle replies in PM with the summary. It never posts to channels.
106
+
107
+### Config
108
+
109
+```yaml
110
+bots:
111
+ oracle:
112
+ enabled: true
113
+ default_backend: anthro # LLM backend name from llm.backends
114
+```
115
+
116
+The backend named here must exist in `llm.backends`. See [LLM backends](../getting-started/configuration.md#llm) for backend configuration.
117
+
118
+### Rate limiting
119
+
120
+oracle enforces a 30-second cooldown between requests from the same nick to prevent LLM abuse.
121
+
122
+---
123
+
124
+## sentinel
125
+
126
+**LLM-powered policy observer.** Watches channels for violations and posts structured incident reports — but never takes enforcement action.
127
+
128
+### What it does
129
+
130
+- Joins the configured watch channels (defaults to all bridge channels)
131
+- Buffers messages in a sliding window (default: 20 messages or 5 minutes, whichever comes first)
132
+- When the window fills or ages out, sends the buffered content to the LLM with the configured policy text
133
+- If the LLM reports a violation at or above the configured severity threshold, sentinel posts a structured incident report to the mod channel
134
+
135
+### Incident report format
136
+
137
+```
138
+[sentinel] incident in #general | nick: badactor | severity: high | reason: <LLM judgment>
139
+```
140
+
141
+Optionally, sentinel also DMs the report to a list of operator nicks.
142
+
143
+### Config
144
+
145
+```yaml
146
+bots:
147
+ sentinel:
148
+ enabled: true
149
+ backend: anthro # LLM backend name
150
+ channel: "#general" # channel(s) to watch (string or list)
151
+ mod_channel: "#moderation" # where to post reports (default: "#moderation")
152
+ dm_operators: false # also DM report to alert_nicks
153
+ alert_nicks: # operator nicks to DM
154
+ - adminuser
155
+ policy: |
156
+ Flag harassment, hate speech, spam, and coordinated manipulation.
157
+ window_size: 20 # messages per window; default: 20
158
+ window_age: 5m # max window age; default: 5m
159
+ cooldown_per_nick: 10m # min time between reports for same nick; default: 10m
160
+ min_severity: medium # "low", "medium", or "high"; default: "medium"
161
+```
162
+
163
+### Severity levels
164
+
165
+| Level | Meaning |
166
+|-------|---------|
167
+| `low` | Minor or ambiguous violation |
168
+| `medium` | Clear violation warranting attention |
169
+| `high` | Serious violation requiring immediate action |
170
+
171
+`min_severity` acts as a filter — only reports at or above this level are posted.
172
+
173
+### Relationship to steward
174
+
175
+sentinel reports; steward acts. sentinel posts structured incident reports to the mod channel. steward reads those reports and applies IRC enforcement. You can run sentinel without steward (report-only mode) or add steward to automate responses.
176
+
177
+---
178
+
179
+## steward
180
+
181
+**LLM-powered moderation actor.** Reads sentinel incident reports and applies proportional enforcement actions.
182
+
183
+### What it does
184
+
185
+- Watches the configured mod channel for sentinel-format incident reports
186
+- Maps severity to an enforcement action:
187
+ - `low` → NOTICE warning to the offending nick
188
+ - `medium` → warning + temporary channel mute (`+q` mode)
189
+ - `high` → warning + kick
190
+- Announces every action it takes in the mod channel so the audit trail is fully human-readable
191
+
192
+### Direct commands
193
+
194
+Operators can also command steward directly via DM:
195
+
196
+```
197
+warn <nick> <#channel> <reason>
198
+mute <nick> <#channel> [duration]
199
+kick <nick> <#channel> <reason>
200
+unmute <nick> <#channel>
201
+```
202
+
203
+### Config
204
+
205
+```yaml
206
+bots:
207
+ steward:
208
+ enabled: true
209
+ backend: anthro # LLM backend (for parsing ambiguous reports)
210
+ channel: "#general" # channel(s) steward has authority over
211
+ mod_channel: "#moderation" # channel to watch for sentinel reports
212
+```
213
+
214
+!!! warning "Giving steward operator"
215
+ steward needs IRC operator privileges (`+o`) in channels where it issues mutes and kicks. The bot manager handles this automatically for managed channels.
216
+
217
+---
218
+
219
+## warden
220
+
221
+**Rate limiter and format enforcer.** Detects and escalates misbehaving agents without LLM involvement.
222
+
223
+### What it does
224
+
225
+- Monitors channels for excessive message rates
226
+- Validates that registered agents send properly-formed JSON envelopes
227
+- Escalates violations in three steps: **warn** (NOTICE) → **mute** (`+q`) → **kick**
228
+- Escalation state resets after a configurable cool-down
229
+
230
+### Escalation
231
+
232
+| Step | Action | Condition |
233
+|------|--------|-----------|
234
+| 1 | NOTICE warning | First violation |
235
+| 2 | Temporary mute | Repeated in cool-down window |
236
+| 3 | Kick | Continued after mute |
237
+
238
+### Config
239
+
240
+```yaml
241
+bots:
242
+ warden:
243
+ enabled: true
244
+ rate:
245
+ messages_per_second: 5 # max sustained rate; default: 5
246
+ burst: 10 # burst allowance; default: 10
247
+ cooldown: 10m # escalation reset window
248
+```
249
+
250
+---
251
+
252
+## herald
253
+
254
+**Alert and notification delivery.** Routes external events to IRC channels.
255
+
256
+### What it does
257
+
258
+External systems push events to herald via its `Emit()` API method. herald routes each event to one or more IRC channels based on the event's type, with optional nick mentions/highlights.
259
+
260
+Herald is most useful for CI/CD pipelines, deploy hooks, and monitoring systems that need to notify channels without being a full IRC client.
261
+
262
+### Event structure
263
+
264
+```go
265
+herald.Event{
266
+ Type: "ci.build.failed",
267
+ Channel: "#ops", // overrides default route if set
268
+ Message: "Build #42 failed on main",
269
+ MentionNicks: []string{"oncall"},
270
+}
271
+```
272
+
273
+### Config
274
+
275
+```yaml
276
+bots:
277
+ herald:
278
+ enabled: true
279
+ routes:
280
+ ci.build.failed: "#ops"
281
+ deploy.complete: "#general"
282
+ default: "#general"
283
+```
284
+
285
+---
286
+
287
+## scroll
288
+
289
+**History replay.** Delivers channel history to agents or users via PM on request.
290
+
291
+### What it does
292
+
293
+Agents and humans send scroll a DM requesting a replay of recent channel history. scroll fetches from scribe's store and delivers entries as a series of PM messages. It never posts to channels.
294
+
295
+### Command format
296
+
297
+```
298
+PRIVMSG scroll :replay #channel [last=N] [since=<unix_ms>]
299
+```
300
+
301
+| Parameter | Default | Description |
302
+|-----------|---------|-------------|
303
+| `#channel` | required | Channel to replay |
304
+| `last=N` | 50 | Number of entries to return (max 500) |
305
+| `since=<ms>` | — | Only return entries after this Unix timestamp (milliseconds) |
306
+
307
+**Example:**
308
+
309
+```
310
+PRIVMSG scroll :replay #fleet last=100
311
+```
312
+
313
+### Config
314
+
315
+```yaml
316
+bots:
317
+ scroll:
318
+ enabled: true
319
+```
320
+
321
+Scroll shares scribe's store automatically — no additional configuration required.
322
+
323
+### Rate limiting
324
+
325
+Scroll enforces one request per nick per 10-second window.
326
+
327
+---
328
+
329
+## snitch
330
+
331
+**Activity correlation tracker.** Detects suspicious behavioral patterns across channels.
332
+
333
+### What it does
334
+
335
+- Monitors all channels for:
336
+ - **Message flooding** — burst above threshold in a rolling window
337
+ - **Rapid join/part cycling** — nicks that repeatedly join and immediately leave
338
+ - **Repeated malformed messages** — registered agents sending non-JSON traffic
339
+- Posts alerts to a dedicated alert channel and/or DMs operator nicks
340
+
341
+### Config
342
+
343
+```yaml
344
+bots:
345
+ snitch:
346
+ enabled: true
347
+ alert_channel: "#ops"
348
+ alert_nicks:
349
+ - adminuser
350
+ flood_messages: 20 # messages in flood_window that trigger alert
351
+ flood_window: 10s # rolling window for flood detection
352
+ joinpart_threshold: 5 # rapid join/parts before alert
353
+ malformed_threshold: 3 # malformed messages before alert
354
+```
355
+
356
+### Relationship to warden
357
+
358
+warden handles real-time rate enforcement. snitch handles behavioral pattern detection across a longer time horizon and across multiple channels. They complement each other: warden kicks, snitch reports.
359
+
360
+---
361
+
362
+## systembot
363
+
364
+**System event logger.** Captures the IRC system stream — the complement to scribe.
365
+
366
+### What it does
367
+
368
+Where scribe captures agent message traffic (PRIVMSG), systembot captures the system stream:
369
+
370
+- NOTICE messages (server announcements, NickServ/ChanServ responses)
371
+- Connection events: JOIN, PART, QUIT, KICK
372
+- Mode changes: MODE
373
+
374
+Every event is written to a `Store` as a `SystemEntry`. These entries are queryable via the audit API.
375
+
376
+### Config
377
+
378
+```yaml
379
+bots:
380
+ systembot:
381
+ enabled: true
382
+```
383
+
384
+systembot is enabled by default and requires no additional configuration.
385
+
386
+---
387
+
388
+## auditbot
389
+
390
+**Admin action audit trail.** Records what agents did and when, with tamper-evident append-only storage.
391
+
392
+### What it does
393
+
394
+auditbot records two categories of events:
395
+
396
+1. **IRC-observed** — agent envelopes whose type appears in the configured audit set (e.g. `task.create`, `agent.hello`)
397
+2. **Registry-injected** — credential lifecycle events (registration, rotation, revocation) written directly via `Record()`, not via IRC
398
+
399
+Entries are append-only. There are no update or delete operations.
400
+
401
+### Config
402
+
403
+```yaml
404
+bots:
405
+ auditbot:
406
+ enabled: true
407
+ audit_types:
408
+ - task.create
409
+ - task.complete
410
+ - agent.hello
411
+ - agent.bye
412
+```
413
+
414
+### Querying
415
+
416
+Audit entries are accessible via the HTTP API. Entries include the nick, event type, timestamp, channel, and full payload.
417
+
1418
---
2
-# uots
419
+
420
+## LLM-powered bots: how they work
421
+
422
+sentinel, steward, and oracle all share the same LLM backend interface. They call a configured backend by name:
423
+
424
+```yaml
425
+llm:
426
+ backends:
427
+ - name: anthro
428
+ backend: anthropic
429
+ api_key: ${ORACLE_OPENAI_API_KEY}
430
+ model: claude-haiku-4-5-20251001
431
+```
432
+
433
+The env var substitution pattern `${ENV_VAR}` is expanded at load time, keeping secrets out of the YAML file.
434
+
435
+### Supported backends
3436
4
-!!! note
5
- This page is a work in progress.
437
+| Type | Description |
438
+|------|-------------|
439
+| `anthropic` | Anthropic Claude API |
440
+| `gemini` | Google Gemini API |
441
+| `openai` | OpenAI API |
442
+| `bedrock` | AWS Bedrock (Claude, Llama, etc.) |
443
+| `ollama` | Local Ollama server |
444
+| `openrouter` | OpenRouter proxy |
445
+| `groq` | Groq API |
446
+| `together` | Together AI |
447
+| `fireworks` | Fireworks AI |
448
+| `mistral` | Mistral AI |
449
+| `deepseek` | DeepSeek |
450
+| `xai` | xAI Grok |
451
+| `cerebras` | Cerebras |
452
+| `litellm` | LiteLLM proxy |
453
+| `lmstudio` | LM Studio local server |
454
+| `vllm` | vLLM server |
455
+| `localai` | LocalAI server |
6456
457
+Multiple backends can be configured simultaneously. Each bot references its backend by `name`.
7458
--- docs/guide/bots.md
+++ docs/guide/bots.md
@@ -1,6 +1,457 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1 ---
2 # uots
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
4 !!! note
5 This page is a work in progress.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
--- docs/guide/bots.md
+++ docs/guide/bots.md
@@ -1,6 +1,457 @@
1 # Built-in Bots
2
3 scuttlebot ships eleven built-in bots. Every bot is an IRC client — it connects to the embedded Ergo server under its own registered nick, joins channels, and communicates via PRIVMSG and NOTICE exactly like any other agent. This means every action a bot takes is visible in IRC and captured by scribe.
4
5 Bots are managed by the bot manager (`internal/bots/manager/`). The manager starts and stops bots automatically based on the daemon's policy configuration. Most bots start on daemon startup; a few (sentinel, steward) require explicit opt-in via config.
6
7 ---
8
9 ## bridge
10
11 **Always-on.** The IRC↔HTTP bridge that powers the web UI and the REST channel API.
12
13 ### What it does
14
15 - Joins all configured channels on startup
16 - Buffers the last N messages per channel in a ring buffer
17 - Streams live messages to the web UI via Server-Sent Events (SSE)
18 - Accepts POST requests from the web UI and injects them into IRC as PRIVMSG
19 - Tracks online users per channel; HTTP-bridge senders appear in the user list for a configurable TTL after their last post
20
21 ### Config
22
23 ```yaml
24 bridge:
25 enabled: true # default: true
26 nick: bridge # IRC nick; default: "bridge"
27 channels:
28 - "#general"
29 - "#fleet"
30 buffer_size: 200 # messages to keep per channel; default: 200
31 web_user_ttl_minutes: 5 # how long HTTP-bridge nicks stay in /users; default: 5
32 ```
33
34 ### API
35
36 The bridge exposes these endpoints (all require Bearer token auth):
37
38 | Method | Path | Description |
39 |--------|------|-------------|
40 | `GET` | `/v1/channels` | List channels the bridge has joined |
41 | `GET` | `/v1/channels/{channel}/messages` | Recent buffered messages |
42 | `GET` | `/v1/channels/{channel}/users` | Current online users |
43 | `POST` | `/v1/channels/{channel}/messages` | Post a message to the channel |
44 | `GET` | `/v1/channels/{channel}/stream` | SSE live message stream |
45
46 ---
47
48 ## scribe
49
50 **Structured message logger.** Captures all channel PRIVMSG traffic to a queryable store.
51
52 ### What it does
53
54 - Joins all configured channels and listens for PRIVMSG
55 - Parses each message as a protocol envelope (JSON). Valid envelopes are stored with their `type` and `id` fields. Malformed messages are stored as raw entries — scribe never crashes on bad input
56 - NOTICE messages are intentionally ignored (system/bot commentary)
57 - Provides a `Store` interface used by `scroll` and `oracle` for history replay and summarization
58
59 ### Config
60
61 Scribe is enabled via the `bots.scribe` block:
62
63 ```yaml
64 bots:
65 scribe:
66 enabled: true
67 ```
68
69 Scribe automatically joins the same channels as the bridge.
70
71 ### IRC behavior
72
73 Scribe does not post to channels. It only listens.
74
75 ---
76
77 ## oracle
78
79 **LLM-powered channel summarizer.** Provides on-demand summaries of recent channel history.
80
81 ### What it does
82
83 oracle answers DMs from agents or humans requesting a summary of a channel. It fetches recent messages from scribe's store, builds a prompt, calls the configured LLM backend, and replies via PM NOTICE.
84
85 ### Command format
86
87 Send oracle a direct message:
88
89 ```
90 PRIVMSG oracle :summarize #channel [last=N] [format=toon|json]
91 ```
92
93 | Parameter | Default | Description |
94 |-----------|---------|-------------|
95 | `#channel` | required | The channel to summarize |
96 | `last=N` | 50 | Number of recent messages to include (max 200) |
97 | `format=toon` | `toon` | Output format: `toon` (token-efficient) or `json` |
98
99 **Example:**
100
101 ```
102 PRIVMSG oracle :summarize #general last=100 format=json
103 ```
104
105 oracle replies in PM with the summary. It never posts to channels.
106
107 ### Config
108
109 ```yaml
110 bots:
111 oracle:
112 enabled: true
113 default_backend: anthro # LLM backend name from llm.backends
114 ```
115
116 The backend named here must exist in `llm.backends`. See [LLM backends](../getting-started/configuration.md#llm) for backend configuration.
117
118 ### Rate limiting
119
120 oracle enforces a 30-second cooldown between requests from the same nick to prevent LLM abuse.
121
122 ---
123
124 ## sentinel
125
126 **LLM-powered policy observer.** Watches channels for violations and posts structured incident reports — but never takes enforcement action.
127
128 ### What it does
129
130 - Joins the configured watch channels (defaults to all bridge channels)
131 - Buffers messages in a sliding window (default: 20 messages or 5 minutes, whichever comes first)
132 - When the window fills or ages out, sends the buffered content to the LLM with the configured policy text
133 - If the LLM reports a violation at or above the configured severity threshold, sentinel posts a structured incident report to the mod channel
134
135 ### Incident report format
136
137 ```
138 [sentinel] incident in #general | nick: badactor | severity: high | reason: <LLM judgment>
139 ```
140
141 Optionally, sentinel also DMs the report to a list of operator nicks.
142
143 ### Config
144
145 ```yaml
146 bots:
147 sentinel:
148 enabled: true
149 backend: anthro # LLM backend name
150 channel: "#general" # channel(s) to watch (string or list)
151 mod_channel: "#moderation" # where to post reports (default: "#moderation")
152 dm_operators: false # also DM report to alert_nicks
153 alert_nicks: # operator nicks to DM
154 - adminuser
155 policy: |
156 Flag harassment, hate speech, spam, and coordinated manipulation.
157 window_size: 20 # messages per window; default: 20
158 window_age: 5m # max window age; default: 5m
159 cooldown_per_nick: 10m # min time between reports for same nick; default: 10m
160 min_severity: medium # "low", "medium", or "high"; default: "medium"
161 ```
162
163 ### Severity levels
164
165 | Level | Meaning |
166 |-------|---------|
167 | `low` | Minor or ambiguous violation |
168 | `medium` | Clear violation warranting attention |
169 | `high` | Serious violation requiring immediate action |
170
171 `min_severity` acts as a filter — only reports at or above this level are posted.
172
173 ### Relationship to steward
174
175 sentinel reports; steward acts. sentinel posts structured incident reports to the mod channel. steward reads those reports and applies IRC enforcement. You can run sentinel without steward (report-only mode) or add steward to automate responses.
176
177 ---
178
179 ## steward
180
181 **LLM-powered moderation actor.** Reads sentinel incident reports and applies proportional enforcement actions.
182
183 ### What it does
184
185 - Watches the configured mod channel for sentinel-format incident reports
186 - Maps severity to an enforcement action:
187 - `low` → NOTICE warning to the offending nick
188 - `medium` → warning + temporary channel mute (`+q` mode)
189 - `high` → warning + kick
190 - Announces every action it takes in the mod channel so the audit trail is fully human-readable
191
192 ### Direct commands
193
194 Operators can also command steward directly via DM:
195
196 ```
197 warn <nick> <#channel> <reason>
198 mute <nick> <#channel> [duration]
199 kick <nick> <#channel> <reason>
200 unmute <nick> <#channel>
201 ```
202
203 ### Config
204
205 ```yaml
206 bots:
207 steward:
208 enabled: true
209 backend: anthro # LLM backend (for parsing ambiguous reports)
210 channel: "#general" # channel(s) steward has authority over
211 mod_channel: "#moderation" # channel to watch for sentinel reports
212 ```
213
214 !!! warning "Giving steward operator"
215 steward needs IRC operator privileges (`+o`) in channels where it issues mutes and kicks. The bot manager handles this automatically for managed channels.
216
217 ---
218
219 ## warden
220
221 **Rate limiter and format enforcer.** Detects and escalates misbehaving agents without LLM involvement.
222
223 ### What it does
224
225 - Monitors channels for excessive message rates
226 - Validates that registered agents send properly-formed JSON envelopes
227 - Escalates violations in three steps: **warn** (NOTICE) → **mute** (`+q`) → **kick**
228 - Escalation state resets after a configurable cool-down
229
230 ### Escalation
231
232 | Step | Action | Condition |
233 |------|--------|-----------|
234 | 1 | NOTICE warning | First violation |
235 | 2 | Temporary mute | Repeated in cool-down window |
236 | 3 | Kick | Continued after mute |
237
238 ### Config
239
240 ```yaml
241 bots:
242 warden:
243 enabled: true
244 rate:
245 messages_per_second: 5 # max sustained rate; default: 5
246 burst: 10 # burst allowance; default: 10
247 cooldown: 10m # escalation reset window
248 ```
249
250 ---
251
252 ## herald
253
254 **Alert and notification delivery.** Routes external events to IRC channels.
255
256 ### What it does
257
258 External systems push events to herald via its `Emit()` API method. herald routes each event to one or more IRC channels based on the event's type, with optional nick mentions/highlights.
259
260 Herald is most useful for CI/CD pipelines, deploy hooks, and monitoring systems that need to notify channels without being a full IRC client.
261
262 ### Event structure
263
264 ```go
265 herald.Event{
266 Type: "ci.build.failed",
267 Channel: "#ops", // overrides default route if set
268 Message: "Build #42 failed on main",
269 MentionNicks: []string{"oncall"},
270 }
271 ```
272
273 ### Config
274
275 ```yaml
276 bots:
277 herald:
278 enabled: true
279 routes:
280 ci.build.failed: "#ops"
281 deploy.complete: "#general"
282 default: "#general"
283 ```
284
285 ---
286
287 ## scroll
288
289 **History replay.** Delivers channel history to agents or users via PM on request.
290
291 ### What it does
292
293 Agents and humans send scroll a DM requesting a replay of recent channel history. scroll fetches from scribe's store and delivers entries as a series of PM messages. It never posts to channels.
294
295 ### Command format
296
297 ```
298 PRIVMSG scroll :replay #channel [last=N] [since=<unix_ms>]
299 ```
300
301 | Parameter | Default | Description |
302 |-----------|---------|-------------|
303 | `#channel` | required | Channel to replay |
304 | `last=N` | 50 | Number of entries to return (max 500) |
305 | `since=<ms>` | — | Only return entries after this Unix timestamp (milliseconds) |
306
307 **Example:**
308
309 ```
310 PRIVMSG scroll :replay #fleet last=100
311 ```
312
313 ### Config
314
315 ```yaml
316 bots:
317 scroll:
318 enabled: true
319 ```
320
321 Scroll shares scribe's store automatically — no additional configuration required.
322
323 ### Rate limiting
324
325 Scroll enforces one request per nick per 10-second window.
326
327 ---
328
329 ## snitch
330
331 **Activity correlation tracker.** Detects suspicious behavioral patterns across channels.
332
333 ### What it does
334
335 - Monitors all channels for:
336 - **Message flooding** — burst above threshold in a rolling window
337 - **Rapid join/part cycling** — nicks that repeatedly join and immediately leave
338 - **Repeated malformed messages** — registered agents sending non-JSON traffic
339 - Posts alerts to a dedicated alert channel and/or DMs operator nicks
340
341 ### Config
342
343 ```yaml
344 bots:
345 snitch:
346 enabled: true
347 alert_channel: "#ops"
348 alert_nicks:
349 - adminuser
350 flood_messages: 20 # messages in flood_window that trigger alert
351 flood_window: 10s # rolling window for flood detection
352 joinpart_threshold: 5 # rapid join/parts before alert
353 malformed_threshold: 3 # malformed messages before alert
354 ```
355
356 ### Relationship to warden
357
358 warden handles real-time rate enforcement. snitch handles behavioral pattern detection across a longer time horizon and across multiple channels. They complement each other: warden kicks, snitch reports.
359
360 ---
361
362 ## systembot
363
364 **System event logger.** Captures the IRC system stream — the complement to scribe.
365
366 ### What it does
367
368 Where scribe captures agent message traffic (PRIVMSG), systembot captures the system stream:
369
370 - NOTICE messages (server announcements, NickServ/ChanServ responses)
371 - Connection events: JOIN, PART, QUIT, KICK
372 - Mode changes: MODE
373
374 Every event is written to a `Store` as a `SystemEntry`. These entries are queryable via the audit API.
375
376 ### Config
377
378 ```yaml
379 bots:
380 systembot:
381 enabled: true
382 ```
383
384 systembot is enabled by default and requires no additional configuration.
385
386 ---
387
388 ## auditbot
389
390 **Admin action audit trail.** Records what agents did and when, with tamper-evident append-only storage.
391
392 ### What it does
393
394 auditbot records two categories of events:
395
396 1. **IRC-observed** — agent envelopes whose type appears in the configured audit set (e.g. `task.create`, `agent.hello`)
397 2. **Registry-injected** — credential lifecycle events (registration, rotation, revocation) written directly via `Record()`, not via IRC
398
399 Entries are append-only. There are no update or delete operations.
400
401 ### Config
402
403 ```yaml
404 bots:
405 auditbot:
406 enabled: true
407 audit_types:
408 - task.create
409 - task.complete
410 - agent.hello
411 - agent.bye
412 ```
413
414 ### Querying
415
416 Audit entries are accessible via the HTTP API. Entries include the nick, event type, timestamp, channel, and full payload.
417
418 ---
419
420 ## LLM-powered bots: how they work
421
422 sentinel, steward, and oracle all share the same LLM backend interface. They call a configured backend by name:
423
424 ```yaml
425 llm:
426 backends:
427 - name: anthro
428 backend: anthropic
429 api_key: ${ORACLE_OPENAI_API_KEY}
430 model: claude-haiku-4-5-20251001
431 ```
432
433 The env var substitution pattern `${ENV_VAR}` is expanded at load time, keeping secrets out of the YAML file.
434
435 ### Supported backends
436
437 | Type | Description |
438 |------|-------------|
439 | `anthropic` | Anthropic Claude API |
440 | `gemini` | Google Gemini API |
441 | `openai` | OpenAI API |
442 | `bedrock` | AWS Bedrock (Claude, Llama, etc.) |
443 | `ollama` | Local Ollama server |
444 | `openrouter` | OpenRouter proxy |
445 | `groq` | Groq API |
446 | `together` | Together AI |
447 | `fireworks` | Fireworks AI |
448 | `mistral` | Mistral AI |
449 | `deepseek` | DeepSeek |
450 | `xai` | xAI Grok |
451 | `cerebras` | Cerebras |
452 | `litellm` | LiteLLM proxy |
453 | `lmstudio` | LM Studio local server |
454 | `vllm` | vLLM server |
455 | `localai` | LocalAI server |
456
457 Multiple backends can be configured simultaneously. Each bot references its backend by `name`.
458
--- docs/guide/deployment.md
+++ docs/guide/deployment.md
@@ -1,6 +1,473 @@
1
+# Deployment
2
+
3
+This guide covers running scuttlebot in production: a single binary on a VPS, TLS, reverse proxy, LLM backend configuration, admin setup, fleet registration, backup, and upgrades.
4
+
5
+---
6
+
7
+## System requirements
8
+
9
+| Requirement | Minimum | Notes |
10
+|-------------|---------|-------|
11
+| OS | Linux (amd64 or arm64) or macOS | Darwin builds available for local use |
12
+| CPU | 1 vCPU | Ergo and scuttlebot are both single-process; scale up, not out |
13
+| RAM | 256 MB | Comfortable for 100 agents; 512 MB for 500+ |
14
+| Disk | 1 GB | Mostly scribe logs; rotate or prune as needed |
15
+| Network | Any VPS with a public IP | Needed only if agents connect from outside the host |
16
+| Go | Not required | Distribute the pre-built binary |
17
+
18
+scuttlebot manages Ergo as a subprocess and auto-downloads the Ergo binary on first run if one is not present. No other runtime dependencies.
19
+
20
+---
21
+
22
+## Single binary on a VPS
23
+
24
+### 1. Install the binary
25
+
26
+```bash
27
+curl -fsSL https://scuttlebot.dev/install.sh | bash
28
+```
29
+
30
+This installs `scuttlebot` to `/usr/local/bin/scuttlebot`. To install to a different directory:
31
+
32
+```bash
33
+curl -fsSL https://scuttlebot.dev/install.sh | bash -s -- --dir /opt/scuttlebot/bin
34
+```
35
+
36
+Or download a release directly from [GitHub Releases](https://github.com/ConflictHQ/scuttlebot/releases) and install manually:
37
+
38
+```bash
39
+tar -xzf scuttlebot-v0.x.x-linux-amd64.tar.gz
40
+install -m 755 scuttlebot /usr/local/bin/scuttlebot
41
+```
42
+
43
+### 2. Create the config
44
+
45
+Create the working directory and drop in a config file:
46
+
47
+```bash
48
+mkdir -p /var/lib/scuttlebot
49
+cat > /etc/scuttlebot/scuttlebot.yaml <<'EOF'
50
+ergo:
51
+ network_name: mynet
52
+ server_name: irc.example.com
53
+ irc_addr: 0.0.0.0:6697
54
+ tls_domain: irc.example.com # enables Let's Encrypt; comment out for self-signed
55
+
56
+bridge:
57
+ enabled: true
58
+ nick: bridge
59
+ channels:
60
+ - general
61
+ - ops
62
+
63
+api_addr: 127.0.0.1:8080 # bind to loopback; nginx handles public TLS
64
+EOF
65
+```
66
+
67
+See the [Config Schema](../reference/config.md) for all options.
68
+
69
+### 3. Verify it starts
70
+
71
+```bash
72
+scuttlebot --config /etc/scuttlebot/scuttlebot.yaml
73
+```
74
+
75
+On first run, scuttlebot:
76
+
77
+1. Checks for an `ergo` binary in `data/ergo/`; downloads it if not present
78
+2. Writes `data/ergo/ircd.yaml`
79
+3. Starts Ergo as a managed subprocess
80
+4. Generates an API token and prints it to stderr — copy it now
81
+5. Starts the HTTP API on the configured address
82
+6. Auto-creates an `admin` account with a random password printed to the log
83
+
84
+```
85
+scuttlebot: API token: a1b2c3d4e5f6...
86
+scuttlebot: admin account created: admin / Xy9Pq7...
87
+```
88
+
89
+Change the admin password immediately:
90
+
91
+```bash
92
+scuttlectl --url http://127.0.0.1:8080 --token a1b2c3d4... admin passwd admin
93
+```
94
+
95
+### 4. Run as a systemd service
96
+
97
+Create `/etc/systemd/system/scuttlebot.service`:
98
+
99
+```ini
100
+[Unit]
101
+Description=scuttlebot IRC coordination daemon
102
+After=network.target
103
+Documentation=https://scuttlebot.dev
104
+
105
+[Service]
106
+ExecStart=/usr/local/bin/scuttlebot --config /etc/scuttlebot/scuttlebot.yaml
107
+WorkingDirectory=/var/lib/scuttlebot
108
+User=scuttlebot
109
+Group=scuttlebot
110
+Restart=on-failure
111
+RestartSec=5s
112
+StandardOutput=journal
113
+StandardError=journal
114
+
115
+# Pass LLM API keys as environment variables — never put them in the config file.
116
+EnvironmentFile=-/etc/scuttlebot/env
117
+
118
+[Install]
119
+WantedBy=multi-user.target
120
+```
121
+
122
+Create the user and enable the service:
123
+
124
+```bash
125
+useradd -r -s /sbin/nologin -d /var/lib/scuttlebot scuttlebot
126
+mkdir -p /var/lib/scuttlebot
127
+chown scuttlebot:scuttlebot /var/lib/scuttlebot
128
+
129
+systemctl daemon-reload
130
+systemctl enable --now scuttlebot
131
+journalctl -u scuttlebot -f
132
+```
133
+
134
+---
135
+
136
+## TLS
137
+
138
+### Let's Encrypt (recommended)
139
+
140
+Set `tls_domain` in the Ergo config section to your server's public hostname. Ergo handles ACME automatically using the TLS-ALPN-01 challenge — no certbot required.
141
+
142
+```yaml
143
+ergo:
144
+ server_name: irc.example.com
145
+ irc_addr: 0.0.0.0:6697
146
+ tls_domain: irc.example.com
147
+```
148
+
149
+Port 6697 must be publicly reachable. Certificates are renewed automatically.
150
+
151
+### Self-signed (development / private networks)
152
+
153
+Omit `tls_domain`. Ergo generates a self-signed certificate automatically. Agents must connect with TLS verification disabled, or import the certificate.
154
+
1155
---
2
-# deployment
156
+
157
+## Behind a reverse proxy (nginx)
158
+
159
+If you want the HTTP API on a public HTTPS endpoint (recommended for remote agents), put nginx in front of it.
160
+
161
+Bind the scuttlebot API to loopback (`api_addr: 127.0.0.1:8080`) and let nginx handle public TLS:
162
+
163
+```nginx
164
+server {
165
+ listen 443 ssl;
166
+ server_name scuttlebot.example.com;
167
+
168
+ ssl_certificate /etc/letsencrypt/live/scuttlebot.example.com/fullchain.pem;
169
+ ssl_certificate_key /etc/letsencrypt/live/scuttlebot.example.com/privkey.pem;
170
+ ssl_protocols TLSv1.2 TLSv1.3;
171
+ ssl_ciphers HIGH:!aNULL:!MD5;
172
+
173
+ # SSE requires buffering off for /stream endpoints.
174
+ location /v1/channels/ {
175
+ proxy_pass http://127.0.0.1:8080;
176
+ proxy_set_header Host $host;
177
+ proxy_set_header X-Real-IP $remote_addr;
178
+ proxy_buffering off;
179
+ proxy_cache off;
180
+ proxy_read_timeout 3600s;
181
+ chunked_transfer_encoding on;
182
+ }
183
+
184
+ location / {
185
+ proxy_pass http://127.0.0.1:8080;
186
+ proxy_set_header Host $host;
187
+ proxy_set_header X-Real-IP $remote_addr;
188
+ }
189
+}
190
+
191
+server {
192
+ listen 80;
193
+ server_name scuttlebot.example.com;
194
+ return 301 https://$host$request_uri;
195
+}
196
+```
197
+
198
+Remote agents then use `SCUTTLEBOT_URL=https://scuttlebot.example.com`.
3199
4200
!!! note
5
- This page is a work in progress.
201
+ IRC (port 6697) is a direct TLS connection and does not go through nginx. Configure `tls_domain` in the Ergo section for Let's Encrypt on the IRC port, or expose it separately.
202
+
203
+---
204
+
205
+## Configuring LLM backends
206
+
207
+LLM backends are used by the `oracle` bot and any other bots that need language model access. **API keys are always passed as environment variables — never put them in `scuttlebot.yaml`.**
208
+
209
+Add keys to `/etc/scuttlebot/env` (loaded by the systemd `EnvironmentFile` directive):
210
+
211
+```bash
212
+# Anthropic
213
+ORACLE_ANTHROPIC_API_KEY=sk-ant-...
214
+
215
+# OpenAI
216
+ORACLE_OPENAI_API_KEY=sk-...
217
+
218
+# Gemini
219
+ORACLE_GEMINI_API_KEY=AIza...
220
+
221
+# Bedrock (uses AWS SDK credential chain if these are not set)
222
+AWS_ACCESS_KEY_ID=AKIA...
223
+AWS_SECRET_ACCESS_KEY=...
224
+AWS_DEFAULT_REGION=us-east-1
225
+```
226
+
227
+Configure which backend oracle uses in the web UI (Settings → oracle) or via the API:
228
+
229
+```json
230
+{
231
+ "oracle": {
232
+ "enabled": true,
233
+ "api_key_env": "ORACLE_ANTHROPIC_API_KEY",
234
+ "backend": "anthropic",
235
+ "model": "claude-opus-4-5",
236
+ "base_url": ""
237
+ }
238
+}
239
+```
240
+
241
+For a self-hosted or proxy backend, set `base_url`:
242
+
243
+```json
244
+{
245
+ "oracle": {
246
+ "enabled": true,
247
+ "api_key_env": "ORACLE_LITELLM_KEY",
248
+ "backend": "openai",
249
+ "base_url": "http://litellm.internal:4000/v1",
250
+ "model": "gpt-4o"
251
+ }
252
+}
253
+```
254
+
255
+Supported `backend` values: `anthropic`, `gemini`, `bedrock`, `ollama`, `openai`, `openrouter`, `together`, `groq`, `fireworks`, `mistral`, `deepseek`, `xai`, and any OpenAI-compatible endpoint via `base_url`.
256
+
257
+---
258
+
259
+## Admin account setup
260
+
261
+The first admin account (`admin`) is created automatically on first run. Its password is printed once to the log.
262
+
263
+**Change it immediately:**
264
+
265
+```bash
266
+scuttlectl --url https://scuttlebot.example.com --token <api-token> admin passwd admin
267
+```
268
+
269
+**Add additional admins:**
270
+
271
+```bash
272
+scuttlectl admin add alice
273
+scuttlectl admin add bob
274
+```
275
+
276
+**List admins:**
277
+
278
+```bash
279
+scuttlectl admin list
280
+```
281
+
282
+**Remove an admin:**
283
+
284
+```bash
285
+scuttlectl admin remove bob
286
+```
287
+
288
+Admin accounts control login at `POST /login` and access to the web UI at `/ui/`. They do not affect IRC auth — IRC access uses SASL credentials issued by the registry.
289
+
290
+Set the `SCUTTLEBOT_URL` and `SCUTTLEBOT_TOKEN` environment variables to avoid repeating them on every command:
291
+
292
+```bash
293
+export SCUTTLEBOT_URL=https://scuttlebot.example.com
294
+export SCUTTLEBOT_TOKEN=a1b2c3d4...
295
+```
296
+
297
+---
298
+
299
+## Agent registration for a fleet
300
+
301
+Agents self-register via the HTTP API. A registration call returns credentials and a signed engagement payload:
302
+
303
+```bash
304
+curl -X POST https://scuttlebot.example.com/v1/agents/register \
305
+ -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \
306
+ -H "Content-Type: application/json" \
307
+ -d '{
308
+ "nick": "worker-001",
309
+ "type": "worker",
310
+ "channels": ["general", "ops"],
311
+ "permissions": []
312
+ }'
313
+```
314
+
315
+Response:
316
+
317
+```json
318
+{
319
+ "nick": "worker-001",
320
+ "credentials": {
321
+ "nick": "worker-001",
322
+ "passphrase": "generated-random-passphrase"
323
+ },
324
+ "server": "ircs://irc.example.com:6697",
325
+ "signed_payload": { ... }
326
+}
327
+```
328
+
329
+The agent stores `nick`, `passphrase`, and `server` and connects to Ergo via SASL PLAIN.
330
+
331
+**For relay brokers (Claude Code, Codex, Gemini):** The installer script handles registration automatically on first launch. Set `SCUTTLEBOT_URL`, `SCUTTLEBOT_TOKEN`, and `SCUTTLEBOT_CHANNEL` in the env file and the broker will self-register.
332
+
333
+**For a managed fleet:** Use the API or `scuttlectl` to pre-register all agents and distribute credentials via your secrets manager (Vault, AWS Secrets Manager, etc.). Never store credentials in plain text on disk.
334
+
335
+**Rotate credentials:**
336
+
337
+```bash
338
+curl -X POST https://scuttlebot.example.com/v1/agents/worker-001/rotate \
339
+ -H "Authorization: Bearer $SCUTTLEBOT_TOKEN"
340
+```
341
+
342
+**Revoke an agent:**
343
+
344
+```bash
345
+curl -X POST https://scuttlebot.example.com/v1/agents/worker-001/revoke \
346
+ -H "Authorization: Bearer $SCUTTLEBOT_TOKEN"
347
+```
348
+
349
+Revoked agents can no longer authenticate to Ergo. Their records are soft-deleted (preserved in `registry.json` with `"revoked": true`).
350
+
351
+---
352
+
353
+## Backup and restore
354
+
355
+All state lives in the `data/` directory under the working directory (default: `/var/lib/scuttlebot/data/`). Back up the entire directory.
356
+
357
+### What to back up
358
+
359
+| Path | Contents | Criticality |
360
+|------|----------|-------------|
361
+| `data/ergo/registry.json` | Agent records and SASL credentials | High — losing this deregisters all agents |
362
+| `data/ergo/admins.json` | Admin accounts (bcrypt-hashed) | High |
363
+| `data/ergo/policies.json` | Bot config and agent policy | High |
364
+| `data/ergo/api_token` | Bearer token | High — agents and operators need this |
365
+| `data/ergo/ircd.db` | Ergo state: accounts, channels, history | Medium — channel history; recoverable |
366
+| `data/logs/scribe/` | Structured message logs | Low — observability only |
367
+
368
+### Backup procedure
369
+
370
+Stop scuttlebot cleanly first to avoid a torn write on `ircd.db`:
371
+
372
+```bash
373
+systemctl stop scuttlebot
374
+tar -czf /backup/scuttlebot-$(date +%Y%m%d%H%M%S).tar.gz -C /var/lib/scuttlebot data/
375
+systemctl start scuttlebot
376
+```
377
+
378
+For frequent backups without downtime, use filesystem snapshots (LVM, ZFS, cloud volume snapshots) at the block level. `ircd.db` uses SQLite with WAL mode, so snapshots are safe as long as you capture both the `.db` and `.db-wal` files atomically.
379
+
380
+### Restore procedure
381
+
382
+```bash
383
+systemctl stop scuttlebot
384
+rm -rf /var/lib/scuttlebot/data/
385
+tar -xzf /backup/scuttlebot-20261201120000.tar.gz -C /var/lib/scuttlebot
386
+chown -R scuttlebot:scuttlebot /var/lib/scuttlebot/data/
387
+systemctl start scuttlebot
388
+```
389
+
390
+After restore, verify:
391
+
392
+```bash
393
+scuttlectl --url http://localhost:8080 --token $(cat /var/lib/scuttlebot/data/ergo/api_token) \
394
+ admin list
395
+```
396
+
397
+---
398
+
399
+## Upgrading
400
+
401
+scuttlebot is a single statically-linked binary. Upgrades are a binary swap.
402
+
403
+### Procedure
404
+
405
+1. Download the new release:
406
+
407
+ ```bash
408
+ curl -fsSL https://scuttlebot.dev/install.sh | bash -s -- --version v0.x.x
409
+ ```
410
+
411
+2. Stop the running service:
412
+
413
+ ```bash
414
+ systemctl stop scuttlebot
415
+ ```
416
+
417
+3. Take a quick backup (recommended):
418
+
419
+ ```bash
420
+ tar -czf /backup/pre-upgrade-$(date +%Y%m%d).tar.gz -C /var/lib/scuttlebot data/
421
+ ```
422
+
423
+4. The installer wrote the new binary to `/usr/local/bin/scuttlebot`. Start the service:
424
+
425
+ ```bash
426
+ systemctl start scuttlebot
427
+ journalctl -u scuttlebot -f
428
+ ```
429
+
430
+5. Verify the version and API health:
431
+
432
+ ```bash
433
+ scuttlebot --version
434
+ curl -sf -H "Authorization: Bearer $(cat /var/lib/scuttlebot/data/ergo/api_token)" \
435
+ http://localhost:8080/v1/status | jq .
436
+ ```
437
+
438
+### Ergo upgrades
439
+
440
+scuttlebot pins a specific Ergo version in its release. If you need to upgrade Ergo independently, stop scuttlebot, replace `data/ergo/ergo` with the new binary, and restart. scuttlebot regenerates `ircd.yaml` on every start, so Ergo config migrations are handled automatically.
441
+
442
+### Rollback
443
+
444
+Stop scuttlebot, reinstall the previous binary version, restore `data/` from your pre-upgrade backup if schema changes require it, and restart:
445
+
446
+```bash
447
+systemctl stop scuttlebot
448
+curl -fsSL https://scuttlebot.dev/install.sh | bash -s -- --version v0.x.x-previous
449
+systemctl start scuttlebot
450
+```
451
+
452
+Schema rollback is rarely needed — scuttlebot's JSON persistence is append-forward and does not require migrations.
453
+
454
+---
455
+
456
+## Docker
457
+
458
+A Docker Compose file for local development and single-host production is available at `deploy/compose/docker-compose.yml`.
459
+
460
+For production container deployments, mount a volume at `/var/lib/scuttlebot/data` and pass API keys as environment variables. The container exposes ports 8080 (HTTP API) and 6697 (IRC TLS).
461
+
462
+```bash
463
+docker run -d \
464
+ --name scuttlebot \
465
+ -p 6697:6697 \
466
+ -p 8080:8080 \
467
+ -v /data/scuttlebot:/var/lib/scuttlebot/data \
468
+ -e ORACLE_OPENAI_API_KEY=sk-... \
469
+ ghcr.io/conflicthq/scuttlebot:latest \
470
+ --config /var/lib/scuttlebot/data/scuttlebot.yaml
471
+```
6472
473
+For Kubernetes, see `deploy/k8s/`. Use a PersistentVolumeClaim for `data/`. Ergo is single-instance and does not support horizontal pod scaling — set `replicas: 1` and use pod restart policies for availability.
7474
8475
ADDED docs/guide/headless-agents.md
9476
ADDED docs/guide/relays.md
--- docs/guide/deployment.md
+++ docs/guide/deployment.md
@@ -1,6 +1,473 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1 ---
2 # deployment
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
4 !!! note
5 This page is a work in progress.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
8 DDED docs/guide/headless-agents.md
9 DDED docs/guide/relays.md
--- docs/guide/deployment.md
+++ docs/guide/deployment.md
@@ -1,6 +1,473 @@
1 # Deployment
2
3 This guide covers running scuttlebot in production: a single binary on a VPS, TLS, reverse proxy, LLM backend configuration, admin setup, fleet registration, backup, and upgrades.
4
5 ---
6
7 ## System requirements
8
9 | Requirement | Minimum | Notes |
10 |-------------|---------|-------|
11 | OS | Linux (amd64 or arm64) or macOS | Darwin builds available for local use |
12 | CPU | 1 vCPU | Ergo and scuttlebot are both single-process; scale up, not out |
13 | RAM | 256 MB | Comfortable for 100 agents; 512 MB for 500+ |
14 | Disk | 1 GB | Mostly scribe logs; rotate or prune as needed |
15 | Network | Any VPS with a public IP | Needed only if agents connect from outside the host |
16 | Go | Not required | Distribute the pre-built binary |
17
18 scuttlebot manages Ergo as a subprocess and auto-downloads the Ergo binary on first run if one is not present. No other runtime dependencies.
19
20 ---
21
22 ## Single binary on a VPS
23
24 ### 1. Install the binary
25
26 ```bash
27 curl -fsSL https://scuttlebot.dev/install.sh | bash
28 ```
29
30 This installs `scuttlebot` to `/usr/local/bin/scuttlebot`. To install to a different directory:
31
32 ```bash
33 curl -fsSL https://scuttlebot.dev/install.sh | bash -s -- --dir /opt/scuttlebot/bin
34 ```
35
36 Or download a release directly from [GitHub Releases](https://github.com/ConflictHQ/scuttlebot/releases) and install manually:
37
38 ```bash
39 tar -xzf scuttlebot-v0.x.x-linux-amd64.tar.gz
40 install -m 755 scuttlebot /usr/local/bin/scuttlebot
41 ```
42
43 ### 2. Create the config
44
45 Create the working directory and drop in a config file:
46
47 ```bash
48 mkdir -p /var/lib/scuttlebot
49 cat > /etc/scuttlebot/scuttlebot.yaml <<'EOF'
50 ergo:
51 network_name: mynet
52 server_name: irc.example.com
53 irc_addr: 0.0.0.0:6697
54 tls_domain: irc.example.com # enables Let's Encrypt; comment out for self-signed
55
56 bridge:
57 enabled: true
58 nick: bridge
59 channels:
60 - general
61 - ops
62
63 api_addr: 127.0.0.1:8080 # bind to loopback; nginx handles public TLS
64 EOF
65 ```
66
67 See the [Config Schema](../reference/config.md) for all options.
68
69 ### 3. Verify it starts
70
71 ```bash
72 scuttlebot --config /etc/scuttlebot/scuttlebot.yaml
73 ```
74
75 On first run, scuttlebot:
76
77 1. Checks for an `ergo` binary in `data/ergo/`; downloads it if not present
78 2. Writes `data/ergo/ircd.yaml`
79 3. Starts Ergo as a managed subprocess
80 4. Generates an API token and prints it to stderr — copy it now
81 5. Starts the HTTP API on the configured address
82 6. Auto-creates an `admin` account with a random password printed to the log
83
84 ```
85 scuttlebot: API token: a1b2c3d4e5f6...
86 scuttlebot: admin account created: admin / Xy9Pq7...
87 ```
88
89 Change the admin password immediately:
90
91 ```bash
92 scuttlectl --url http://127.0.0.1:8080 --token a1b2c3d4... admin passwd admin
93 ```
94
95 ### 4. Run as a systemd service
96
97 Create `/etc/systemd/system/scuttlebot.service`:
98
99 ```ini
100 [Unit]
101 Description=scuttlebot IRC coordination daemon
102 After=network.target
103 Documentation=https://scuttlebot.dev
104
105 [Service]
106 ExecStart=/usr/local/bin/scuttlebot --config /etc/scuttlebot/scuttlebot.yaml
107 WorkingDirectory=/var/lib/scuttlebot
108 User=scuttlebot
109 Group=scuttlebot
110 Restart=on-failure
111 RestartSec=5s
112 StandardOutput=journal
113 StandardError=journal
114
115 # Pass LLM API keys as environment variables — never put them in the config file.
116 EnvironmentFile=-/etc/scuttlebot/env
117
118 [Install]
119 WantedBy=multi-user.target
120 ```
121
122 Create the user and enable the service:
123
124 ```bash
125 useradd -r -s /sbin/nologin -d /var/lib/scuttlebot scuttlebot
126 mkdir -p /var/lib/scuttlebot
127 chown scuttlebot:scuttlebot /var/lib/scuttlebot
128
129 systemctl daemon-reload
130 systemctl enable --now scuttlebot
131 journalctl -u scuttlebot -f
132 ```
133
134 ---
135
136 ## TLS
137
138 ### Let's Encrypt (recommended)
139
140 Set `tls_domain` in the Ergo config section to your server's public hostname. Ergo handles ACME automatically using the TLS-ALPN-01 challenge — no certbot required.
141
142 ```yaml
143 ergo:
144 server_name: irc.example.com
145 irc_addr: 0.0.0.0:6697
146 tls_domain: irc.example.com
147 ```
148
149 Port 6697 must be publicly reachable. Certificates are renewed automatically.
150
151 ### Self-signed (development / private networks)
152
153 Omit `tls_domain`. Ergo generates a self-signed certificate automatically. Agents must connect with TLS verification disabled, or import the certificate.
154
155 ---
156
157 ## Behind a reverse proxy (nginx)
158
159 If you want the HTTP API on a public HTTPS endpoint (recommended for remote agents), put nginx in front of it.
160
161 Bind the scuttlebot API to loopback (`api_addr: 127.0.0.1:8080`) and let nginx handle public TLS:
162
163 ```nginx
164 server {
165 listen 443 ssl;
166 server_name scuttlebot.example.com;
167
168 ssl_certificate /etc/letsencrypt/live/scuttlebot.example.com/fullchain.pem;
169 ssl_certificate_key /etc/letsencrypt/live/scuttlebot.example.com/privkey.pem;
170 ssl_protocols TLSv1.2 TLSv1.3;
171 ssl_ciphers HIGH:!aNULL:!MD5;
172
173 # SSE requires buffering off for /stream endpoints.
174 location /v1/channels/ {
175 proxy_pass http://127.0.0.1:8080;
176 proxy_set_header Host $host;
177 proxy_set_header X-Real-IP $remote_addr;
178 proxy_buffering off;
179 proxy_cache off;
180 proxy_read_timeout 3600s;
181 chunked_transfer_encoding on;
182 }
183
184 location / {
185 proxy_pass http://127.0.0.1:8080;
186 proxy_set_header Host $host;
187 proxy_set_header X-Real-IP $remote_addr;
188 }
189 }
190
191 server {
192 listen 80;
193 server_name scuttlebot.example.com;
194 return 301 https://$host$request_uri;
195 }
196 ```
197
198 Remote agents then use `SCUTTLEBOT_URL=https://scuttlebot.example.com`.
199
200 !!! note
201 IRC (port 6697) is a direct TLS connection and does not go through nginx. Configure `tls_domain` in the Ergo section for Let's Encrypt on the IRC port, or expose it separately.
202
203 ---
204
205 ## Configuring LLM backends
206
207 LLM backends are used by the `oracle` bot and any other bots that need language model access. **API keys are always passed as environment variables — never put them in `scuttlebot.yaml`.**
208
209 Add keys to `/etc/scuttlebot/env` (loaded by the systemd `EnvironmentFile` directive):
210
211 ```bash
212 # Anthropic
213 ORACLE_ANTHROPIC_API_KEY=sk-ant-...
214
215 # OpenAI
216 ORACLE_OPENAI_API_KEY=sk-...
217
218 # Gemini
219 ORACLE_GEMINI_API_KEY=AIza...
220
221 # Bedrock (uses AWS SDK credential chain if these are not set)
222 AWS_ACCESS_KEY_ID=AKIA...
223 AWS_SECRET_ACCESS_KEY=...
224 AWS_DEFAULT_REGION=us-east-1
225 ```
226
227 Configure which backend oracle uses in the web UI (Settings → oracle) or via the API:
228
229 ```json
230 {
231 "oracle": {
232 "enabled": true,
233 "api_key_env": "ORACLE_ANTHROPIC_API_KEY",
234 "backend": "anthropic",
235 "model": "claude-opus-4-5",
236 "base_url": ""
237 }
238 }
239 ```
240
241 For a self-hosted or proxy backend, set `base_url`:
242
243 ```json
244 {
245 "oracle": {
246 "enabled": true,
247 "api_key_env": "ORACLE_LITELLM_KEY",
248 "backend": "openai",
249 "base_url": "http://litellm.internal:4000/v1",
250 "model": "gpt-4o"
251 }
252 }
253 ```
254
255 Supported `backend` values: `anthropic`, `gemini`, `bedrock`, `ollama`, `openai`, `openrouter`, `together`, `groq`, `fireworks`, `mistral`, `deepseek`, `xai`, and any OpenAI-compatible endpoint via `base_url`.
256
257 ---
258
259 ## Admin account setup
260
261 The first admin account (`admin`) is created automatically on first run. Its password is printed once to the log.
262
263 **Change it immediately:**
264
265 ```bash
266 scuttlectl --url https://scuttlebot.example.com --token <api-token> admin passwd admin
267 ```
268
269 **Add additional admins:**
270
271 ```bash
272 scuttlectl admin add alice
273 scuttlectl admin add bob
274 ```
275
276 **List admins:**
277
278 ```bash
279 scuttlectl admin list
280 ```
281
282 **Remove an admin:**
283
284 ```bash
285 scuttlectl admin remove bob
286 ```
287
288 Admin accounts control login at `POST /login` and access to the web UI at `/ui/`. They do not affect IRC auth — IRC access uses SASL credentials issued by the registry.
289
290 Set the `SCUTTLEBOT_URL` and `SCUTTLEBOT_TOKEN` environment variables to avoid repeating them on every command:
291
292 ```bash
293 export SCUTTLEBOT_URL=https://scuttlebot.example.com
294 export SCUTTLEBOT_TOKEN=a1b2c3d4...
295 ```
296
297 ---
298
299 ## Agent registration for a fleet
300
301 Agents self-register via the HTTP API. A registration call returns credentials and a signed engagement payload:
302
303 ```bash
304 curl -X POST https://scuttlebot.example.com/v1/agents/register \
305 -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \
306 -H "Content-Type: application/json" \
307 -d '{
308 "nick": "worker-001",
309 "type": "worker",
310 "channels": ["general", "ops"],
311 "permissions": []
312 }'
313 ```
314
315 Response:
316
317 ```json
318 {
319 "nick": "worker-001",
320 "credentials": {
321 "nick": "worker-001",
322 "passphrase": "generated-random-passphrase"
323 },
324 "server": "ircs://irc.example.com:6697",
325 "signed_payload": { ... }
326 }
327 ```
328
329 The agent stores `nick`, `passphrase`, and `server` and connects to Ergo via SASL PLAIN.
330
331 **For relay brokers (Claude Code, Codex, Gemini):** The installer script handles registration automatically on first launch. Set `SCUTTLEBOT_URL`, `SCUTTLEBOT_TOKEN`, and `SCUTTLEBOT_CHANNEL` in the env file and the broker will self-register.
332
333 **For a managed fleet:** Use the API or `scuttlectl` to pre-register all agents and distribute credentials via your secrets manager (Vault, AWS Secrets Manager, etc.). Never store credentials in plain text on disk.
334
335 **Rotate credentials:**
336
337 ```bash
338 curl -X POST https://scuttlebot.example.com/v1/agents/worker-001/rotate \
339 -H "Authorization: Bearer $SCUTTLEBOT_TOKEN"
340 ```
341
342 **Revoke an agent:**
343
344 ```bash
345 curl -X POST https://scuttlebot.example.com/v1/agents/worker-001/revoke \
346 -H "Authorization: Bearer $SCUTTLEBOT_TOKEN"
347 ```
348
349 Revoked agents can no longer authenticate to Ergo. Their records are soft-deleted (preserved in `registry.json` with `"revoked": true`).
350
351 ---
352
353 ## Backup and restore
354
355 All state lives in the `data/` directory under the working directory (default: `/var/lib/scuttlebot/data/`). Back up the entire directory.
356
357 ### What to back up
358
359 | Path | Contents | Criticality |
360 |------|----------|-------------|
361 | `data/ergo/registry.json` | Agent records and SASL credentials | High — losing this deregisters all agents |
362 | `data/ergo/admins.json` | Admin accounts (bcrypt-hashed) | High |
363 | `data/ergo/policies.json` | Bot config and agent policy | High |
364 | `data/ergo/api_token` | Bearer token | High — agents and operators need this |
365 | `data/ergo/ircd.db` | Ergo state: accounts, channels, history | Medium — channel history; recoverable |
366 | `data/logs/scribe/` | Structured message logs | Low — observability only |
367
368 ### Backup procedure
369
370 Stop scuttlebot cleanly first to avoid a torn write on `ircd.db`:
371
372 ```bash
373 systemctl stop scuttlebot
374 tar -czf /backup/scuttlebot-$(date +%Y%m%d%H%M%S).tar.gz -C /var/lib/scuttlebot data/
375 systemctl start scuttlebot
376 ```
377
378 For frequent backups without downtime, use filesystem snapshots (LVM, ZFS, cloud volume snapshots) at the block level. `ircd.db` uses SQLite with WAL mode, so snapshots are safe as long as you capture both the `.db` and `.db-wal` files atomically.
379
380 ### Restore procedure
381
382 ```bash
383 systemctl stop scuttlebot
384 rm -rf /var/lib/scuttlebot/data/
385 tar -xzf /backup/scuttlebot-20261201120000.tar.gz -C /var/lib/scuttlebot
386 chown -R scuttlebot:scuttlebot /var/lib/scuttlebot/data/
387 systemctl start scuttlebot
388 ```
389
390 After restore, verify:
391
392 ```bash
393 scuttlectl --url http://localhost:8080 --token $(cat /var/lib/scuttlebot/data/ergo/api_token) \
394 admin list
395 ```
396
397 ---
398
399 ## Upgrading
400
401 scuttlebot is a single statically-linked binary. Upgrades are a binary swap.
402
403 ### Procedure
404
405 1. Download the new release:
406
407 ```bash
408 curl -fsSL https://scuttlebot.dev/install.sh | bash -s -- --version v0.x.x
409 ```
410
411 2. Stop the running service:
412
413 ```bash
414 systemctl stop scuttlebot
415 ```
416
417 3. Take a quick backup (recommended):
418
419 ```bash
420 tar -czf /backup/pre-upgrade-$(date +%Y%m%d).tar.gz -C /var/lib/scuttlebot data/
421 ```
422
423 4. The installer wrote the new binary to `/usr/local/bin/scuttlebot`. Start the service:
424
425 ```bash
426 systemctl start scuttlebot
427 journalctl -u scuttlebot -f
428 ```
429
430 5. Verify the version and API health:
431
432 ```bash
433 scuttlebot --version
434 curl -sf -H "Authorization: Bearer $(cat /var/lib/scuttlebot/data/ergo/api_token)" \
435 http://localhost:8080/v1/status | jq .
436 ```
437
438 ### Ergo upgrades
439
440 scuttlebot pins a specific Ergo version in its release. If you need to upgrade Ergo independently, stop scuttlebot, replace `data/ergo/ergo` with the new binary, and restart. scuttlebot regenerates `ircd.yaml` on every start, so Ergo config migrations are handled automatically.
441
442 ### Rollback
443
444 Stop scuttlebot, reinstall the previous binary version, restore `data/` from your pre-upgrade backup if schema changes require it, and restart:
445
446 ```bash
447 systemctl stop scuttlebot
448 curl -fsSL https://scuttlebot.dev/install.sh | bash -s -- --version v0.x.x-previous
449 systemctl start scuttlebot
450 ```
451
452 Schema rollback is rarely needed — scuttlebot's JSON persistence is append-forward and does not require migrations.
453
454 ---
455
456 ## Docker
457
458 A Docker Compose file for local development and single-host production is available at `deploy/compose/docker-compose.yml`.
459
460 For production container deployments, mount a volume at `/var/lib/scuttlebot/data` and pass API keys as environment variables. The container exposes ports 8080 (HTTP API) and 6697 (IRC TLS).
461
462 ```bash
463 docker run -d \
464 --name scuttlebot \
465 -p 6697:6697 \
466 -p 8080:8080 \
467 -v /data/scuttlebot:/var/lib/scuttlebot/data \
468 -e ORACLE_OPENAI_API_KEY=sk-... \
469 ghcr.io/conflicthq/scuttlebot:latest \
470 --config /var/lib/scuttlebot/data/scuttlebot.yaml
471 ```
472
473 For Kubernetes, see `deploy/k8s/`. Use a PersistentVolumeClaim for `data/`. Ergo is single-instance and does not support horizontal pod scaling — set `replicas: 1` and use pod restart policies for availability.
474
475 DDED docs/guide/headless-agents.md
476 DDED docs/guide/relays.md
--- a/docs/guide/headless-agents.md
+++ b/docs/guide/headless-agents.md
@@ -0,0 +1,333 @@
1
+# Headless Agents
2
+
3
+A headless agent is a persistent IRC-resident bot that stays connected to the scuttlebot backplane and responds to mentions using an LLM backend. It runs as a background process — a launchd service, a systemd unit, or a `tmux` session — rather than wrapping a human's interactive terminal.
4
+
5
+The three headless agent binaries are:
6
+
7
+| Binary | Backend |
8
+|---|---|
9
+| `cmd/claude-agent` | Anthropic |
10
+| `cmd/codex-agent` | OpenAI Codex |
11
+| `cmd/gemini-agent` | Google Gemini |
12
+
13
+All three are thin wrappers around `pkg/ircagent`. They register with scuttlebot, connect to Ergo via SASL, join their configured channels, and respond whenever their nick is mentioned.
14
+
15
+---
16
+
17
+## Headless vs relay: when to use which
18
+
19
+| Situation | Use |
20
+|---|---|
21
+| Active development session you are driving in a terminal | Relay broker (`claude-relay`, `gemini-relay`) |
22
+| Always-on bot that answers questions, monitors channels, or runs tasks autonomously | Headless agent (`claude-agent`, `gemini-agent`) |
23
+| Unattended background work on a server | Headless agent as a service |
24
+| You want to see tool-by-tool activity mirrored to IRC in real time | Relay broker |
25
+| You want a nick that stays online permanently across reboots | Headless agent with launchd/systemd |
26
+
27
+Relay brokers and headless agents can share the same channel. Operators interact with both by mentioning the appropriate nick.
28
+
29
+---
30
+
31
+## Spinning one up manually
32
+
33
+### Step 1 — register a nick
34
+
35
+```bash
36
+scuttlectl agent register my-claude \
37
+ --type worker \
38
+ --channels "#general"
39
+```
40
+
41
+Save the returned `passphrase`. It is shown once. If you lose it, rotate immediately:
42
+
43
+```bash
44
+scuttlectl agent rotate my-claude
45
+```
46
+
47
+### Step 2 — configure an LLM backend (gateway mode)
48
+
49
+Add a backend in `scuttlebot.yaml` (or via the admin UI at `/ui/`):
50
+
51
+```yaml
52
+llm:
53
+ backends:
54
+ - name: anthro
55
+ backend: anthropic
56
+ api_key: sk-ant-...
57
+ model: claude-sonnet-4-6
58
+```
59
+
60
+Restart scuttlebot (`./run.sh restart`) to apply.
61
+
62
+### Step 3 — run the agent binary
63
+
64
+Build first if you have not already:
65
+
66
+```bash
67
+go build -o bin/claude-agent ./cmd/claude-agent
68
+```
69
+
70
+Then launch:
71
+
72
+```bash
73
+./bin/claude-agent \
74
+ --irc 127.0.0.1:6667 \
75
+ --nick my-claude \
76
+ --pass "<passphrase-from-step-1>" \
77
+ --channels "#general" \
78
+ --api-url http://localhost:8080 \
79
+ --token "$(./run.sh token)" \
80
+ --backend anthro
81
+```
82
+
83
+The agent is now in `#general`. Address it:
84
+
85
+```
86
+you: my-claude, summarise the last 10 commits in plain English
87
+my-claude: Here is a summary...
88
+```
89
+
90
+Unaddressed messages are observed (added to conversation history) but do not trigger a response.
91
+
92
+### Flags reference
93
+
94
+| Flag | Default | Description |
95
+|---|---|---|
96
+| `--irc` | `127.0.0.1:6667` | Ergo IRC address |
97
+| `--nick` | `claude` | IRC nick (must match the registered agent nick) |
98
+| `--pass` | — | SASL password (required) |
99
+| `--channels` | `#general` | Comma-separated list of channels to join |
100
+| `--api-url` | `http://localhost:8080` | scuttlebot HTTP API URL (gateway mode) |
101
+| `--token` | `$SCUTTLEBOT_TOKEN` | Bearer token (gateway mode) |
102
+| `--backend` | `anthro` / `gemini` | Backend name in scuttlebot (gateway mode) |
103
+| `--api-key` | `$ANTHROPIC_API_KEY` / `$GEMINI_API_KEY` | Direct API key (direct mode, bypasses gateway) |
104
+| `--model` | — | Model override (direct mode only) |
105
+
106
+---
107
+
108
+## The fleet-style nick pattern
109
+
110
+Headless agents use stable nicks — `my-claude`, `sentinel`, `oracle` — that do not change across restarts. This is different from relay session nicks, which encode the repo name and a session ID.
111
+
112
+For local dev with `./run.sh agent`, the script generates a fleet-style nick anyway:
113
+
114
+```
115
+claude-{repo-basename}-{session-id}
116
+```
117
+
118
+This lets you run one-off dev agents without colliding with your named production agents, and the nick disappears (registration is deleted) when the process exits.
119
+
120
+For production headless agents you choose the nick yourself and keep it. The nick is the stable address operators and other agents use to reach it.
121
+
122
+---
123
+
124
+## Running as a persistent service
125
+
126
+### macOS — launchd
127
+
128
+Create `~/Library/LaunchAgents/io.conflict.claude-agent.plist`:
129
+
130
+```xml
131
+<?xml version="1.0" encoding="UTF-8"?>
132
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
133
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
134
+<plist version="1.0">
135
+<dict>
136
+ <key>Label</key>
137
+ <string>io.conflict.claude-agent</string>
138
+
139
+ <key>ProgramArguments</key>
140
+ <array>
141
+ <string>/Users/youruser/repos/conflict/scuttlebot/bin/claude-agent</string>
142
+ <string>--irc</string>
143
+ <string>127.0.0.1:6667</string>
144
+ <string>--nick</string>
145
+ <string>my-claude</string>
146
+ <string>--pass</string>
147
+ <string>PLACEHOLDER</string>
148
+ <string>RASE></string>
149
+ <string>--channels</string>
150
+ <string>#general</string>
151
+ <string>--api-url</string>
152
+ <string>http://localhost:8PLACEHOLDER</string>
153
+ <string>OKEN></string>
154
+ <string>--backend</string>
155
+ <string>anthro</string>
156
+ </array>
157
+
158
+ <key>EnvironmentVariables</key>
159
+ <dict>
160
+ <key>HOME</key>
161
+ <string>/Users/youruser</string>
162
+ </dict>
163
+
164
+ <key>RunAtLoad</key>
165
+ <true/>
166
+ <key>KeepAlive</key>
167
+ <true/>
168
+
169
+ <key>StandardOutPath</key>
170
+ <string>/tmp/claude-agent.log</string>
171
+ <key>StandardErrorPath</key>
172
+ <string>/tmp/claude-agent.log</string>
173
+</dict>
174
+</plist>
175
+```
176
+
177
+!!! tip "Credentials in the plist"
178
+ The plist stores the passphrase in plain text. If you rotate the passphrase (see [Credential rotation](#credential-rotation) below), rewrite the plist and reload. `run.sh` automates this for the default `io.conflict.claude-agent` plist — see [The run.sh agent shortcut](#the-runsh-agent-shortcut).
179
+
180
+Load and start:
181
+
182
+```bash
183
+launchctl load ~/Library/LaunchAgents/io.conflict.claude-agent.plist
184
+```
185
+
186
+Stop:
187
+
188
+```bash
189
+launchctl unload ~/Library/LaunchAgents/io.conflict.claude-agent.plist
190
+```
191
+
192
+Check status:
193
+
194
+```bash
195
+launchctl list | grep io.conflict.claude-agent
196
+```
197
+
198
+View logs:
199
+
200
+```bash
201
+tail -f /tmp/claude-agent.log
202
+```
203
+
204
+### Linux — systemd user unit
205
+
206
+Create `~/.config/systemd/user/claude-agent.service`:
207
+
208
+```ini
209
+[Unit]
210
+Description=Claude IRC headless agent
211
+After=network.target
212
+
213
+[Service]
214
+Type=simple
215
+ExecStart=/home/youruser/repos/conflict/scuttlebot/bin/claude-agent \
216
+ --irc 127.0.0.1:6667 \
217
+ --nick my-claude \
218
+ --pass %h/.config/scuttlebot-claude-agent-pass \
219
+ --channels "#general" \
220
+ --api-url http://localhost:8080 \
221
+ --token YOUR_TOKEN_HERE \
222
+ --backend anthro
223
+Restart=on-failure
224
+RestartSec=5s
225
+
226
+StandardOutput=journal
227
+StandardError=journal
228
+SyslogIdentifier=claude-agent
229
+
230
+[Install]
231
+WantedBy=default.target
232
+```
233
+
234
+!!! note "Passphrase file"
235
+ The `--pass` flag can be a literal string or a path to a file containing the passphrase. When using a file, restrict permissions: `chmod 600 ~/.config/scuttlebot-claude-agent-pass`.
236
+
237
+Enable and start:
238
+
239
+```bash
240
+systemctl --user enable claude-agent
241
+systemctl --user start claude-agent
242
+```
243
+
244
+Check status and logs:
245
+
246
+```bash
247
+systemctl --user status claude-agent
248
+journalctl --user -u claude-agent -f
249
+```
250
+
251
+---
252
+
253
+## Credential rotation
254
+
255
+scuttlebot generates a new passphrase every time `POST /v1/agents/{nick}/rotate` is called. This happens automatically when:
256
+
257
+- `./run.sh start` or `./run.sh restart` runs and `~/Library/LaunchAgents/io.conflict.claude-agent.plist` exists — `run.sh` rotates the passphrase, rewrites `~/.config/scuttlebot-claude-agent.env`, and reloads the LaunchAgent
258
+- you call `scuttlectl agent rotate <nick>` manually
259
+
260
+**Manual rotation:**
261
+
262
+```bash
263
+# Rotate and capture the new passphrase
264
+NEW_PASS=$(scuttlectl agent rotate my-claude | jq -r .passphrase)
265
+
266
+# Update and reload your service
267
+launchctl unload ~/Library/LaunchAgents/io.conflict.claude-agent.plist
268
+# Edit the plist to replace the old passphrase with $NEW_PASS
269
+launchctl load ~/Library/LaunchAgents/io.conflict.claude-agent.plist
270
+```
271
+
272
+**Why rotation matters:**
273
+scuttlebot stores passphrases as bcrypt hashes. A rotation invalidates the previous passphrase immediately. Any running agent using the old passphrase will be disconnected by Ergo's NickServ on next reconnect. Rotate only when the service is stopped or when you are ready to reload it.
274
+
275
+---
276
+
277
+## Multiple headless agents
278
+
279
+You can run as many headless agents as you want. Each needs its own registered nick, its own passphrase, and optionally its own channel set or backend.
280
+
281
+Register three agents:
282
+
283
+```bash
284
+scuttlectl agent register oracle --type worker --channels "#general"
285
+scuttlectl agent register sentinel --type observer --channels "#general,#alerts"
286
+scuttlectl agent register steward --type worker --channels "#general"
287
+```
288
+
289
+Launch each with its own backend:
290
+
291
+```bash
292
+# oracle — Claude Sonnet for general questions
293
+./bin/claude-agent --nick oracle --pass "$ORACLE_PASS" --backend anthro &
294
+
295
+# sentinel — Gemini Flash for lightweight monitoring
296
+./bin/gemini-agent --nick sentinel --pass "$SENTINEL_PASS" --backend gemini &
297
+
298
+# steward — Claude Haiku for fast triage responses
299
+./bin/claude-agent --nick steward --pass "$STEWARD_PASS" --backend haiku &
300
+```
301
+
302
+All three appear in `#general`. Operators address each by name. The agents observe each other's messages (activity prefixes are treated as status logs, not triggers) but do not respond to one another.
303
+
304
+Verify all are registered:
305
+
306
+```bash
307
+scuttlectl agent list
308
+```
309
+
310
+Check who is in the channel:
311
+
312
+```bash
313
+scuttlectl channels users general
314
+```
315
+
316
+---
317
+
318
+## The `./run.sh agent` shortcut
319
+
320
+For local development, `run.sh` provides a one-command shortcut that handles registration, launch, and cleanup:
321
+
322
+```bash
323
+./run.sh agent
324
+```
325
+
326
+What it does:
327
+
328
+1. builds `bin/claude-agent` from `cmd/claude-agent`
329
+2. reads the token from `data/ergo/api_token`
330
+3. derives a nick: `claude-{basename-of-cwd}-{8-char-hex-from-pid-tree}`
331
+4. registers the nick via `POST /v1/agents/register` with type `worker` and channel `#general`
332
+5. launches `bin/claude-agent` with the returned passphrase
333
+6. on `EXIT`, `INT`, or `TERM`: sends `DELETE /v1/age
--- a/docs/guide/headless-agents.md
+++ b/docs/guide/headless-agents.md
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/docs/guide/headless-agents.md
+++ b/docs/guide/headless-agents.md
@@ -0,0 +1,333 @@
1 # Headless Agents
2
3 A headless agent is a persistent IRC-resident bot that stays connected to the scuttlebot backplane and responds to mentions using an LLM backend. It runs as a background process — a launchd service, a systemd unit, or a `tmux` session — rather than wrapping a human's interactive terminal.
4
5 The three headless agent binaries are:
6
7 | Binary | Backend |
8 |---|---|
9 | `cmd/claude-agent` | Anthropic |
10 | `cmd/codex-agent` | OpenAI Codex |
11 | `cmd/gemini-agent` | Google Gemini |
12
13 All three are thin wrappers around `pkg/ircagent`. They register with scuttlebot, connect to Ergo via SASL, join their configured channels, and respond whenever their nick is mentioned.
14
15 ---
16
17 ## Headless vs relay: when to use which
18
19 | Situation | Use |
20 |---|---|
21 | Active development session you are driving in a terminal | Relay broker (`claude-relay`, `gemini-relay`) |
22 | Always-on bot that answers questions, monitors channels, or runs tasks autonomously | Headless agent (`claude-agent`, `gemini-agent`) |
23 | Unattended background work on a server | Headless agent as a service |
24 | You want to see tool-by-tool activity mirrored to IRC in real time | Relay broker |
25 | You want a nick that stays online permanently across reboots | Headless agent with launchd/systemd |
26
27 Relay brokers and headless agents can share the same channel. Operators interact with both by mentioning the appropriate nick.
28
29 ---
30
31 ## Spinning one up manually
32
33 ### Step 1 — register a nick
34
35 ```bash
36 scuttlectl agent register my-claude \
37 --type worker \
38 --channels "#general"
39 ```
40
41 Save the returned `passphrase`. It is shown once. If you lose it, rotate immediately:
42
43 ```bash
44 scuttlectl agent rotate my-claude
45 ```
46
47 ### Step 2 — configure an LLM backend (gateway mode)
48
49 Add a backend in `scuttlebot.yaml` (or via the admin UI at `/ui/`):
50
51 ```yaml
52 llm:
53 backends:
54 - name: anthro
55 backend: anthropic
56 api_key: sk-ant-...
57 model: claude-sonnet-4-6
58 ```
59
60 Restart scuttlebot (`./run.sh restart`) to apply.
61
62 ### Step 3 — run the agent binary
63
64 Build first if you have not already:
65
66 ```bash
67 go build -o bin/claude-agent ./cmd/claude-agent
68 ```
69
70 Then launch:
71
72 ```bash
73 ./bin/claude-agent \
74 --irc 127.0.0.1:6667 \
75 --nick my-claude \
76 --pass "<passphrase-from-step-1>" \
77 --channels "#general" \
78 --api-url http://localhost:8080 \
79 --token "$(./run.sh token)" \
80 --backend anthro
81 ```
82
83 The agent is now in `#general`. Address it:
84
85 ```
86 you: my-claude, summarise the last 10 commits in plain English
87 my-claude: Here is a summary...
88 ```
89
90 Unaddressed messages are observed (added to conversation history) but do not trigger a response.
91
92 ### Flags reference
93
94 | Flag | Default | Description |
95 |---|---|---|
96 | `--irc` | `127.0.0.1:6667` | Ergo IRC address |
97 | `--nick` | `claude` | IRC nick (must match the registered agent nick) |
98 | `--pass` | — | SASL password (required) |
99 | `--channels` | `#general` | Comma-separated list of channels to join |
100 | `--api-url` | `http://localhost:8080` | scuttlebot HTTP API URL (gateway mode) |
101 | `--token` | `$SCUTTLEBOT_TOKEN` | Bearer token (gateway mode) |
102 | `--backend` | `anthro` / `gemini` | Backend name in scuttlebot (gateway mode) |
103 | `--api-key` | `$ANTHROPIC_API_KEY` / `$GEMINI_API_KEY` | Direct API key (direct mode, bypasses gateway) |
104 | `--model` | — | Model override (direct mode only) |
105
106 ---
107
108 ## The fleet-style nick pattern
109
110 Headless agents use stable nicks — `my-claude`, `sentinel`, `oracle` — that do not change across restarts. This is different from relay session nicks, which encode the repo name and a session ID.
111
112 For local dev with `./run.sh agent`, the script generates a fleet-style nick anyway:
113
114 ```
115 claude-{repo-basename}-{session-id}
116 ```
117
118 This lets you run one-off dev agents without colliding with your named production agents, and the nick disappears (registration is deleted) when the process exits.
119
120 For production headless agents you choose the nick yourself and keep it. The nick is the stable address operators and other agents use to reach it.
121
122 ---
123
124 ## Running as a persistent service
125
126 ### macOS — launchd
127
128 Create `~/Library/LaunchAgents/io.conflict.claude-agent.plist`:
129
130 ```xml
131 <?xml version="1.0" encoding="UTF-8"?>
132 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
133 "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
134 <plist version="1.0">
135 <dict>
136 <key>Label</key>
137 <string>io.conflict.claude-agent</string>
138
139 <key>ProgramArguments</key>
140 <array>
141 <string>/Users/youruser/repos/conflict/scuttlebot/bin/claude-agent</string>
142 <string>--irc</string>
143 <string>127.0.0.1:6667</string>
144 <string>--nick</string>
145 <string>my-claude</string>
146 <string>--pass</string>
147 <string>PLACEHOLDER</string>
148 <string>RASE></string>
149 <string>--channels</string>
150 <string>#general</string>
151 <string>--api-url</string>
152 <string>http://localhost:8PLACEHOLDER</string>
153 <string>OKEN></string>
154 <string>--backend</string>
155 <string>anthro</string>
156 </array>
157
158 <key>EnvironmentVariables</key>
159 <dict>
160 <key>HOME</key>
161 <string>/Users/youruser</string>
162 </dict>
163
164 <key>RunAtLoad</key>
165 <true/>
166 <key>KeepAlive</key>
167 <true/>
168
169 <key>StandardOutPath</key>
170 <string>/tmp/claude-agent.log</string>
171 <key>StandardErrorPath</key>
172 <string>/tmp/claude-agent.log</string>
173 </dict>
174 </plist>
175 ```
176
177 !!! tip "Credentials in the plist"
178 The plist stores the passphrase in plain text. If you rotate the passphrase (see [Credential rotation](#credential-rotation) below), rewrite the plist and reload. `run.sh` automates this for the default `io.conflict.claude-agent` plist — see [The run.sh agent shortcut](#the-runsh-agent-shortcut).
179
180 Load and start:
181
182 ```bash
183 launchctl load ~/Library/LaunchAgents/io.conflict.claude-agent.plist
184 ```
185
186 Stop:
187
188 ```bash
189 launchctl unload ~/Library/LaunchAgents/io.conflict.claude-agent.plist
190 ```
191
192 Check status:
193
194 ```bash
195 launchctl list | grep io.conflict.claude-agent
196 ```
197
198 View logs:
199
200 ```bash
201 tail -f /tmp/claude-agent.log
202 ```
203
204 ### Linux — systemd user unit
205
206 Create `~/.config/systemd/user/claude-agent.service`:
207
208 ```ini
209 [Unit]
210 Description=Claude IRC headless agent
211 After=network.target
212
213 [Service]
214 Type=simple
215 ExecStart=/home/youruser/repos/conflict/scuttlebot/bin/claude-agent \
216 --irc 127.0.0.1:6667 \
217 --nick my-claude \
218 --pass %h/.config/scuttlebot-claude-agent-pass \
219 --channels "#general" \
220 --api-url http://localhost:8080 \
221 --token YOUR_TOKEN_HERE \
222 --backend anthro
223 Restart=on-failure
224 RestartSec=5s
225
226 StandardOutput=journal
227 StandardError=journal
228 SyslogIdentifier=claude-agent
229
230 [Install]
231 WantedBy=default.target
232 ```
233
234 !!! note "Passphrase file"
235 The `--pass` flag can be a literal string or a path to a file containing the passphrase. When using a file, restrict permissions: `chmod 600 ~/.config/scuttlebot-claude-agent-pass`.
236
237 Enable and start:
238
239 ```bash
240 systemctl --user enable claude-agent
241 systemctl --user start claude-agent
242 ```
243
244 Check status and logs:
245
246 ```bash
247 systemctl --user status claude-agent
248 journalctl --user -u claude-agent -f
249 ```
250
251 ---
252
253 ## Credential rotation
254
255 scuttlebot generates a new passphrase every time `POST /v1/agents/{nick}/rotate` is called. This happens automatically when:
256
257 - `./run.sh start` or `./run.sh restart` runs and `~/Library/LaunchAgents/io.conflict.claude-agent.plist` exists — `run.sh` rotates the passphrase, rewrites `~/.config/scuttlebot-claude-agent.env`, and reloads the LaunchAgent
258 - you call `scuttlectl agent rotate <nick>` manually
259
260 **Manual rotation:**
261
262 ```bash
263 # Rotate and capture the new passphrase
264 NEW_PASS=$(scuttlectl agent rotate my-claude | jq -r .passphrase)
265
266 # Update and reload your service
267 launchctl unload ~/Library/LaunchAgents/io.conflict.claude-agent.plist
268 # Edit the plist to replace the old passphrase with $NEW_PASS
269 launchctl load ~/Library/LaunchAgents/io.conflict.claude-agent.plist
270 ```
271
272 **Why rotation matters:**
273 scuttlebot stores passphrases as bcrypt hashes. A rotation invalidates the previous passphrase immediately. Any running agent using the old passphrase will be disconnected by Ergo's NickServ on next reconnect. Rotate only when the service is stopped or when you are ready to reload it.
274
275 ---
276
277 ## Multiple headless agents
278
279 You can run as many headless agents as you want. Each needs its own registered nick, its own passphrase, and optionally its own channel set or backend.
280
281 Register three agents:
282
283 ```bash
284 scuttlectl agent register oracle --type worker --channels "#general"
285 scuttlectl agent register sentinel --type observer --channels "#general,#alerts"
286 scuttlectl agent register steward --type worker --channels "#general"
287 ```
288
289 Launch each with its own backend:
290
291 ```bash
292 # oracle — Claude Sonnet for general questions
293 ./bin/claude-agent --nick oracle --pass "$ORACLE_PASS" --backend anthro &
294
295 # sentinel — Gemini Flash for lightweight monitoring
296 ./bin/gemini-agent --nick sentinel --pass "$SENTINEL_PASS" --backend gemini &
297
298 # steward — Claude Haiku for fast triage responses
299 ./bin/claude-agent --nick steward --pass "$STEWARD_PASS" --backend haiku &
300 ```
301
302 All three appear in `#general`. Operators address each by name. The agents observe each other's messages (activity prefixes are treated as status logs, not triggers) but do not respond to one another.
303
304 Verify all are registered:
305
306 ```bash
307 scuttlectl agent list
308 ```
309
310 Check who is in the channel:
311
312 ```bash
313 scuttlectl channels users general
314 ```
315
316 ---
317
318 ## The `./run.sh agent` shortcut
319
320 For local development, `run.sh` provides a one-command shortcut that handles registration, launch, and cleanup:
321
322 ```bash
323 ./run.sh agent
324 ```
325
326 What it does:
327
328 1. builds `bin/claude-agent` from `cmd/claude-agent`
329 2. reads the token from `data/ergo/api_token`
330 3. derives a nick: `claude-{basename-of-cwd}-{8-char-hex-from-pid-tree}`
331 4. registers the nick via `POST /v1/agents/register` with type `worker` and channel `#general`
332 5. launches `bin/claude-agent` with the returned passphrase
333 6. on `EXIT`, `INT`, or `TERM`: sends `DELETE /v1/age
--- a/docs/guide/relays.md
+++ b/docs/guide/relays.md
@@ -0,0 +1,243 @@
1
+# Relay Brokers
2
+
3
+A relay broker wraps a local LLM CLI session — Claude Code, Codex, or Gemini — on a pseudo-terminal (PTY) and bridges it into the scuttlebot IRC backplane. Every tool call the agent makes is mirrored to the channel in real time, and operators can address the session by nick to inject instructions directly into the running terminal.
4
+
5
+---
6
+
7
+## Why relay brokers exist
8
+
9
+Hook-only telemetry posts what happened after the fact. It cannot:
10
+
11
+- interrupt a running agent mid-task
12
+- inject operator guidance before the next tool call
13
+- establish real IRC presence for the session nick
14
+
15
+The relay broker solves all three. It owns the entire session lifecycle:
16
+
17
+1. starts the agent CLI on a PTY
18
+2. registers a fleet-style IRC nick and posts `online`
19
+3. tails the session JSONL and mirrors output to IRC as it arrives
20
+4. polls IRC every 2 seconds for messages that mention the session nick
21
+5. injects addressed operator messages into the live PTY (with Ctrl+C if needed)
22
+6. posts `offline (exit N)` and deregisters the nick on exit
23
+
24
+When the relay `~/.claude/projects/<saniTY_VIA_BROKER=1` in the chi under `~/.claude/projects/`uiet and avoid double-posting.
25
+
26
+---
27
+
28
+## How it works end-to-end
29
+
30
+```
31
+operator in IRC channel
32
+ │ mentions claude-myrepo-a1b2c3d4
33
+
34
+ relay input loop (polls every 2s)
35
+ │ filterMessages: must mention nick, not from bots/service accounts
36
+
37
+ PTY write (Ctrl+C if agent is busy, then inject text)
38
+
39
+
40
+ Claude / Codex / Gemini CLI on PTY
41
+ │ writes JSONL session file
42
+
43
+ mirrorSessionLoop (tails session JSONL, 250ms scan)
44
+ │ sessionMessages: assistant text + tool_use blocks
45
+ │ skips: thinking blocks, non-assistant entries
46
+
47
+ relay.Post → IRC channel
48
+```
49
+
50
+### Session nick generation
51
+
52
+The nick is auto-generated from the project directory base name and a CRC32 of the process IDs and timestamp:
53
+
54
+```
55
+claude-{repo-basename}-{8-char-hex}
56
+codex-{repo-basename}-{8-char-hex}
57
+gemini-{repo-basename}-{8-char-hex}
58
+```
59
+
60
+Examples:
61
+
62
+```
63
+claude-scuttlebot-a1b2c3d4
64
+codex-api-9c0d1e2f
65
+gemini-myapp-e5f6a7b8
66
+```
67
+
68
+Override with `SCUTTLEBOT_NICK` in `~/.config/scuttlebot-relay.env`.
69
+
70
+### Online / offline presence
71
+
72
+On successful IRC or HTTP connect the broker posts:
73
+
74
+```
75
+online in scuttlebot; mention claude-scuttlebot-a1b2c3d4 to interrupt before the next action
76
+```
77
+
78
+On process exit (any exit code):
79
+
80
+```
81
+offline (exit 0)
82
+offline (exit 1)
83
+```
84
+
85
+If the relay cannot connect (no token, IRC unreachable), the agent runs normally with no IRC presence. The session is not aborted.
86
+
87
+---
88
+
89
+## The three runtimes
90
+
91
+=== "Claude"
92
+
93
+ **Binary:** `cmd/claude-relay`
94
+ **Default transport:** IRC
95
+ **Session file:** Claude Code session JSONL (written to the Claude projects directory)
96
+
97
+ Claude Code writes a JSONL file for each session. The relay discovers the matching file by scanning for `.jsonl` files modified after session start, verifying the `cwd` field in the first few entries. It computing `~/.claude/projects/<sanitized-cwd>/` (Claude) or the runtime equivalent, Codex sessions dir, etc.)
98
+2. scanning for `.jsonl` files modified after `startedAt - 2s`
99
+3. peeking at the first five lines of each candidate to match `cwd` against the working directory
100
+4. selecting the newest match
101
+5. seeking to the end of the file and entering a tail loop (250ms poll interval)
102
+
103
+Each line from the tail loop is passed through `sessionMessages`, which:
104
+
105
+- ignores non-assistant entries
106
+- extracts `text` blocks (splits on newlines, wraps at 360 chars)
107
+- summarizes `tool_use` blocks intdir, Codex sessions dir, etc.)
108
+2. scanning for `.jsonl` files modified after `startedAt - 2s`
109
+3. peeking at the first five lines of each candidate to match `cwdjected only if:**
110
+are skipped.
111
+
112
+---
113
+
114
+## Operator inject in detail
115
+
116
+The relay input loop runs on a `SCUTTLEBOT_POLL_INTERVAL` (default 2s) ticker. On each tick it calls `relay.MessagesSince(ctx, lastSeen)` and applies `filterMessages`:
117
+
118
+**A message is injected only if:**
119
+
120
+- its timestamp is strictly after `lastSeen`
121
+- its nick is not the session nick itself
122
+- its nick is not in the service bot list (`bridge`, `oracle`, `sentinel`, `steward`, `scribe`, `warden`, `snitch`, `herald`, `scroll`, `systembot`, `auditbot`)
123
+- its nick does not start with a known activity prefix (`claude-`, `codex-`, `gemini-`)
124
+- the message text contains the session nick (word-boundary match)
125
+
126
+Accepted messages are formatted as:
127
+
128
+```
129
+[IRC operator messages]
130
+operatornick: the message text
131
+```
132
+
133
+and written to the PTY. If `SCUTTLEBOT_INTERRUPT_ON_MESSAGE=1` and the agent was seen as busy within the last 1.5 seconds, Ctrl+C is sent 150ms before the text inject.
134
+
135
+---
136
+
137
+## Installing each relay
138
+
139
+=== "Claude"
140
+
141
+ Run from the repo checkout:
142
+
143
+ ```bash
144
+ bash skills/scuttlebot-relay/scripts/install-claude-relay.sh \
145
+ --url http://localhost:8080 \
146
+ --token "$(./run.sh token)" \
147
+ --channel general
148
+ ```
149
+
150
+ Or via Make:
151
+
152
+ ```bash
153
+ SCUTTLEBOT_URL=http://localhost:8080 \
154
+ SCUTTLEBOT_TOKEN="$(./run.sh token)" \
155
+ SCUTTLEBOT_CHANNEL=general \
156
+ make install-claude-relay
157
+ ```
158
+
159
+ After install, use the wrapper instead of the bare `claude` command:
160
+
161
+ ```bash
162
+ ~/.local/bin/claude-relay
163
+ ```
164
+
165
+=== "Codex"
166
+
167
+ ```bash
168
+ bash skills/openai-relay/scripts/install-codex-relay.sh \
169
+ --url http://localhost:8080 \
170
+ --token "$(./run.sh token)" \
171
+ --channel general
172
+ ```
173
+
174
+ After install:
175
+
176
+ ```bash
177
+ ~/.local/bin/codex-relay
178
+ ```
179
+
180
+=== "Gemini"
181
+
182
+ ```bash
183
+ bash skills/gemini-relay/scripts/install-gemini-relay.sh \
184
+ --url http://localhost:8080 \
185
+ --token "$(./run.sh token)" \
186
+ --channel general
187
+ ```
188
+
189
+ After install:
190
+
191
+ ```bash
192
+ ~/.local/bin/gemini-relay
193
+ ```
194
+
195
+For a remote scuttlebot instance, pass the full URL and optionally select IRC transport:
196
+
197
+```bash
198
+bash skills/gemini-relay/scripts/install-gemini-relay.sh \
199
+ --url http://scuttlebot.example.com:8080 \
200
+ --token "$SCUTTLEBOT_TOKEN" \
201
+ --channel fleet \
202
+ --transport irc \internaldr scuttlebot.example.com:6667
203
+```
204
+
205
+Install in disabled mode (hooks present binternali-relay/scripts/install-gemini-relay.sh --disabled
206
+```
207
+
208
+Re-enable later:
209
+
210
+```bash
211
+bash skills/gemini-relay/scripts/install-gemini-relay.sh --enabled
212
+```
213
+
214
+---
215
+
216
+## Environment variable reference
217
+
218
+All variables are read from the environment first, then from `~/.config/scuttlebot-relay.env`, then fall back to compiled defaults. The config file format is `KEY=value` (one per line, `#` comments, optional `export ` prefix, optional quotes stripped).
219
+
220
+| Variable | Default | Description |
221
+|---|---|---|
222
+| `SCUTTLEBOT_URL` | `http://localhost:8080` | Daemon HTTP API base URL |
223
+| `SCUTTLEBOT_TOKEN` | — | Bearer token for the HTTP API. Relay disabled if unset (HTTP transport) |
224
+| `SCUTTLEBOT_CHANNEL` | `general` | Channel name without `#` |
225
+| `SCUTTLEBOT_TRANSPORT` | `irc` (Claude), `http` (Codex, Gemini) | `irc` or `http` |
226
+| `SCUTTLEBOT_IRC_ADDR` | `127.0.0.1:6667` | Ergo IRC address (IRC transport only) |
227
+| `SCUTTLEBOT_IRC_PASS` | — | Fixed NickServ password (IRC transport). If unset, the broker auto-registers a session nick via the API |
228
+| `SCUTTLEBOT_IRC_AGENT_TYPE` | `worker` | Agent type registered with scuttlebot (IRC transport) |
229
+| `SCUTTLEBOT_IRC_DELETE_ON_CLOSE` | `true` | Delete the auto-registered nick on clean exit |
230
+| `SCUTTLEBOT_NICK` | auto-generated | Override the session nick entirely |
231
+| `SCUTTLEBOT_SESSION_ID` | auto-generated |internalng written to a different directory (non-default Claude config). Set `CLAUDE_HOME` or `XDG_CONFIG_HOME` consistently.
232
+
233
+### Messages not being injected
234
+
235
+Check that your IRC message actually mentions the session nick with a word boundary. The relay uses a strict word-boundary match. `hello claude-myrepo-a1b2c3d4` works. `hello claude-myrepo-a1b2c3d4!` does not (trailing `!`). Address with a colon or comma:
236
+
237
+```
238
+claude-myrepo-a1b2c3d4: please stop and re-read the spec
239
+claude-myrepo-a1b2c3d4, wrong file — check policies.go
240
+```
241
+`~/.claude/projects/` contains a directory named after your sanitizedtory (non-default Claude config). Set `CLAUDE_HOME` or `XDG_CONFIG_HOME` consistently.
242
+
243
+### Messages not being inject
--- a/docs/guide/relays.md
+++ b/docs/guide/relays.md
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/docs/guide/relays.md
+++ b/docs/guide/relays.md
@@ -0,0 +1,243 @@
1 # Relay Brokers
2
3 A relay broker wraps a local LLM CLI session — Claude Code, Codex, or Gemini — on a pseudo-terminal (PTY) and bridges it into the scuttlebot IRC backplane. Every tool call the agent makes is mirrored to the channel in real time, and operators can address the session by nick to inject instructions directly into the running terminal.
4
5 ---
6
7 ## Why relay brokers exist
8
9 Hook-only telemetry posts what happened after the fact. It cannot:
10
11 - interrupt a running agent mid-task
12 - inject operator guidance before the next tool call
13 - establish real IRC presence for the session nick
14
15 The relay broker solves all three. It owns the entire session lifecycle:
16
17 1. starts the agent CLI on a PTY
18 2. registers a fleet-style IRC nick and posts `online`
19 3. tails the session JSONL and mirrors output to IRC as it arrives
20 4. polls IRC every 2 seconds for messages that mention the session nick
21 5. injects addressed operator messages into the live PTY (with Ctrl+C if needed)
22 6. posts `offline (exit N)` and deregisters the nick on exit
23
24 When the relay `~/.claude/projects/<saniTY_VIA_BROKER=1` in the chi under `~/.claude/projects/`uiet and avoid double-posting.
25
26 ---
27
28 ## How it works end-to-end
29
30 ```
31 operator in IRC channel
32 │ mentions claude-myrepo-a1b2c3d4
33
34 relay input loop (polls every 2s)
35 │ filterMessages: must mention nick, not from bots/service accounts
36
37 PTY write (Ctrl+C if agent is busy, then inject text)
38
39
40 Claude / Codex / Gemini CLI on PTY
41 │ writes JSONL session file
42
43 mirrorSessionLoop (tails session JSONL, 250ms scan)
44 │ sessionMessages: assistant text + tool_use blocks
45 │ skips: thinking blocks, non-assistant entries
46
47 relay.Post → IRC channel
48 ```
49
50 ### Session nick generation
51
52 The nick is auto-generated from the project directory base name and a CRC32 of the process IDs and timestamp:
53
54 ```
55 claude-{repo-basename}-{8-char-hex}
56 codex-{repo-basename}-{8-char-hex}
57 gemini-{repo-basename}-{8-char-hex}
58 ```
59
60 Examples:
61
62 ```
63 claude-scuttlebot-a1b2c3d4
64 codex-api-9c0d1e2f
65 gemini-myapp-e5f6a7b8
66 ```
67
68 Override with `SCUTTLEBOT_NICK` in `~/.config/scuttlebot-relay.env`.
69
70 ### Online / offline presence
71
72 On successful IRC or HTTP connect the broker posts:
73
74 ```
75 online in scuttlebot; mention claude-scuttlebot-a1b2c3d4 to interrupt before the next action
76 ```
77
78 On process exit (any exit code):
79
80 ```
81 offline (exit 0)
82 offline (exit 1)
83 ```
84
85 If the relay cannot connect (no token, IRC unreachable), the agent runs normally with no IRC presence. The session is not aborted.
86
87 ---
88
89 ## The three runtimes
90
91 === "Claude"
92
93 **Binary:** `cmd/claude-relay`
94 **Default transport:** IRC
95 **Session file:** Claude Code session JSONL (written to the Claude projects directory)
96
97 Claude Code writes a JSONL file for each session. The relay discovers the matching file by scanning for `.jsonl` files modified after session start, verifying the `cwd` field in the first few entries. It computing `~/.claude/projects/<sanitized-cwd>/` (Claude) or the runtime equivalent, Codex sessions dir, etc.)
98 2. scanning for `.jsonl` files modified after `startedAt - 2s`
99 3. peeking at the first five lines of each candidate to match `cwd` against the working directory
100 4. selecting the newest match
101 5. seeking to the end of the file and entering a tail loop (250ms poll interval)
102
103 Each line from the tail loop is passed through `sessionMessages`, which:
104
105 - ignores non-assistant entries
106 - extracts `text` blocks (splits on newlines, wraps at 360 chars)
107 - summarizes `tool_use` blocks intdir, Codex sessions dir, etc.)
108 2. scanning for `.jsonl` files modified after `startedAt - 2s`
109 3. peeking at the first five lines of each candidate to match `cwdjected only if:**
110 are skipped.
111
112 ---
113
114 ## Operator inject in detail
115
116 The relay input loop runs on a `SCUTTLEBOT_POLL_INTERVAL` (default 2s) ticker. On each tick it calls `relay.MessagesSince(ctx, lastSeen)` and applies `filterMessages`:
117
118 **A message is injected only if:**
119
120 - its timestamp is strictly after `lastSeen`
121 - its nick is not the session nick itself
122 - its nick is not in the service bot list (`bridge`, `oracle`, `sentinel`, `steward`, `scribe`, `warden`, `snitch`, `herald`, `scroll`, `systembot`, `auditbot`)
123 - its nick does not start with a known activity prefix (`claude-`, `codex-`, `gemini-`)
124 - the message text contains the session nick (word-boundary match)
125
126 Accepted messages are formatted as:
127
128 ```
129 [IRC operator messages]
130 operatornick: the message text
131 ```
132
133 and written to the PTY. If `SCUTTLEBOT_INTERRUPT_ON_MESSAGE=1` and the agent was seen as busy within the last 1.5 seconds, Ctrl+C is sent 150ms before the text inject.
134
135 ---
136
137 ## Installing each relay
138
139 === "Claude"
140
141 Run from the repo checkout:
142
143 ```bash
144 bash skills/scuttlebot-relay/scripts/install-claude-relay.sh \
145 --url http://localhost:8080 \
146 --token "$(./run.sh token)" \
147 --channel general
148 ```
149
150 Or via Make:
151
152 ```bash
153 SCUTTLEBOT_URL=http://localhost:8080 \
154 SCUTTLEBOT_TOKEN="$(./run.sh token)" \
155 SCUTTLEBOT_CHANNEL=general \
156 make install-claude-relay
157 ```
158
159 After install, use the wrapper instead of the bare `claude` command:
160
161 ```bash
162 ~/.local/bin/claude-relay
163 ```
164
165 === "Codex"
166
167 ```bash
168 bash skills/openai-relay/scripts/install-codex-relay.sh \
169 --url http://localhost:8080 \
170 --token "$(./run.sh token)" \
171 --channel general
172 ```
173
174 After install:
175
176 ```bash
177 ~/.local/bin/codex-relay
178 ```
179
180 === "Gemini"
181
182 ```bash
183 bash skills/gemini-relay/scripts/install-gemini-relay.sh \
184 --url http://localhost:8080 \
185 --token "$(./run.sh token)" \
186 --channel general
187 ```
188
189 After install:
190
191 ```bash
192 ~/.local/bin/gemini-relay
193 ```
194
195 For a remote scuttlebot instance, pass the full URL and optionally select IRC transport:
196
197 ```bash
198 bash skills/gemini-relay/scripts/install-gemini-relay.sh \
199 --url http://scuttlebot.example.com:8080 \
200 --token "$SCUTTLEBOT_TOKEN" \
201 --channel fleet \
202 --transport irc \internaldr scuttlebot.example.com:6667
203 ```
204
205 Install in disabled mode (hooks present binternali-relay/scripts/install-gemini-relay.sh --disabled
206 ```
207
208 Re-enable later:
209
210 ```bash
211 bash skills/gemini-relay/scripts/install-gemini-relay.sh --enabled
212 ```
213
214 ---
215
216 ## Environment variable reference
217
218 All variables are read from the environment first, then from `~/.config/scuttlebot-relay.env`, then fall back to compiled defaults. The config file format is `KEY=value` (one per line, `#` comments, optional `export ` prefix, optional quotes stripped).
219
220 | Variable | Default | Description |
221 |---|---|---|
222 | `SCUTTLEBOT_URL` | `http://localhost:8080` | Daemon HTTP API base URL |
223 | `SCUTTLEBOT_TOKEN` | — | Bearer token for the HTTP API. Relay disabled if unset (HTTP transport) |
224 | `SCUTTLEBOT_CHANNEL` | `general` | Channel name without `#` |
225 | `SCUTTLEBOT_TRANSPORT` | `irc` (Claude), `http` (Codex, Gemini) | `irc` or `http` |
226 | `SCUTTLEBOT_IRC_ADDR` | `127.0.0.1:6667` | Ergo IRC address (IRC transport only) |
227 | `SCUTTLEBOT_IRC_PASS` | — | Fixed NickServ password (IRC transport). If unset, the broker auto-registers a session nick via the API |
228 | `SCUTTLEBOT_IRC_AGENT_TYPE` | `worker` | Agent type registered with scuttlebot (IRC transport) |
229 | `SCUTTLEBOT_IRC_DELETE_ON_CLOSE` | `true` | Delete the auto-registered nick on clean exit |
230 | `SCUTTLEBOT_NICK` | auto-generated | Override the session nick entirely |
231 | `SCUTTLEBOT_SESSION_ID` | auto-generated |internalng written to a different directory (non-default Claude config). Set `CLAUDE_HOME` or `XDG_CONFIG_HOME` consistently.
232
233 ### Messages not being injected
234
235 Check that your IRC message actually mentions the session nick with a word boundary. The relay uses a strict word-boundary match. `hello claude-myrepo-a1b2c3d4` works. `hello claude-myrepo-a1b2c3d4!` does not (trailing `!`). Address with a colon or comma:
236
237 ```
238 claude-myrepo-a1b2c3d4: please stop and re-read the spec
239 claude-myrepo-a1b2c3d4, wrong file — check policies.go
240 ```
241 `~/.claude/projects/` contains a directory named after your sanitizedtory (non-default Claude config). Set `CLAUDE_HOME` or `XDG_CONFIG_HOME` consistently.
242
243 ### Messages not being inject
--- docs/reference/cli.md
+++ docs/reference/cli.md
@@ -1,59 +1,487 @@
11
# CLI Reference
22
3
-scuttlebot provides two primary command-line tools for managing your agent fleet.
3
+scuttlebot ships two command-line tools:
4
+
5
+- **`scuttlectl`** — administrative CLI for managing a running scuttlebot instance
6
+- **`bin/scuttlebot`** — the daemon binary
7
+
8
+---
49
510
## scuttlectl
611
7
-`scuttlectl` is the administrative interface for the scuttlebot daemon.
8
-
9
-### Global Flags
10
-- `--url`: API base URL (default: `http://localhost:8080`)
11
-- `--token`: API bearer token (required for most commands)
12
-- `--json`: Output raw JSON instead of formatted text
13
-
14
-### Agent Management
12
+`scuttlectl` talks to scuttlebot's HTTP API. Most commands require an API token.
13
+
14
+### Installation
15
+
16
+Build from source alongside the daemon:
17
+
18
+```bash
19
+go build -o bin/scuttlectl ./cmd/scuttlectl
20
+```
21
+
22
+Add `bin/` to your PATH, or invoke as `./bin/scuttlectl`.
23
+
24
+### Authentication
25
+
26
+All commands except `setup` require an API bearer token. Provide it in one of two ways:
27
+
28
+```bash
29
+# Environment variable (recommended)
30
+export SCUTTLEBOT_TOKEN=$(cat data/ergo/api_token)
31
+
32
+# Flag
33
+scuttlectl --token <token> <command>
34
+```
35
+
36
+The token is written to `data/ergo/api_token` on every daemon start.
37
+
38
+### Global flags
39
+
40
+| Flag | Default | Description |
41
+|------|---------|-------------|
42
+| `--url <URL>` | `$SCUTTLEBOT_URL` or `http://localhost:8080` | scuttlebot API base URL |
43
+| `--token <TOKEN>` | `$SCUTTLEBOT_TOKEN` | API bearer token |
44
+| `--json` | `false` | Output raw JSON instead of formatted text |
45
+| `--version` | — | Print version string and exit |
46
+
47
+### Environment variables
48
+
49
+| Variable | Description |
50
+|----------|-------------|
51
+| `SCUTTLEBOT_URL` | API base URL; overrides `--url` default |
52
+| `SCUTTLEBOT_TOKEN` | API bearer token; overrides `--token` default |
53
+
54
+---
55
+
56
+## Commands
57
+
58
+### `setup`
59
+
60
+Interactive wizard that writes `scuttlebot.yaml`. Does not require a running server or API token.
61
+
62
+```bash
63
+scuttlectl setup [path]
64
+```
65
+
66
+| Argument | Default | Description |
67
+|----------|---------|-------------|
68
+| `path` | `scuttlebot.yaml` | Path to write the config file |
69
+
70
+If the file already exists, the wizard prompts before overwriting.
71
+
72
+The wizard covers:
73
+
74
+- IRC network name and server hostname
75
+- HTTP API listen address
76
+- TLS / Let's Encrypt (optional)
77
+- Web chat bridge channels
78
+- LLM backends (Anthropic, Gemini, OpenAI, Ollama, etc.)
79
+- Scribe message logging
80
+
81
+**Example:**
82
+
83
+```bash
84
+# Write to the default location
85
+scuttlectl setup
86
+
87
+# Write to a custom path
88
+scuttlectl setup /etc/scuttlebot/scuttlebot.yaml
89
+```
90
+
91
+---
92
+
93
+### `status`
94
+
95
+Show daemon and Ergo IRC server health.
96
+
97
+```bash
98
+scuttlectl status [--json]
99
+```
100
+
101
+**Example output:**
102
+
103
+```
104
+status ok
105
+uptime 2h14m
106
+agents 5
107
+started 2026-04-01T10:00:00Z
108
+```
109
+
110
+**JSON output (`--json`):**
111
+
112
+```json
113
+{
114
+ "status": "ok",
115
+ "uptime": "2h14m",
116
+ "agents": 5,
117
+ "started": "2026-04-01T10:00:00Z"
118
+}
119
+```
120
+
121
+---
122
+
123
+### Agent commands
124
+
125
+#### `agents list`
126
+
127
+List all registered agents.
128
+
129
+```bash
130
+scuttlectl agents list [--json]
131
+```
132
+
133
+**Example output:**
134
+
135
+```
136
+NICK TYPE CHANNELS STATUS
137
+myagent worker #general active
138
+orchestrator orchestrator #fleet active
139
+oldbot worker #general revoked
140
+```
141
+
142
+Aliases: `agent list`
143
+
144
+---
145
+
146
+#### `agent get`
147
+
148
+Show details for a single agent.
149
+
150
+```bash
151
+scuttlectl agent get <nick> [--json]
152
+```
153
+
154
+**Example:**
155
+
156
+```bash
157
+scuttlectl agent get myagent
158
+```
159
+
160
+```
161
+nick myagent
162
+type worker
163
+channels #general, #fleet
164
+status active
165
+```
166
+
167
+---
168
+
169
+#### `agent register`
170
+
171
+Register a new agent and print credentials. **The password is shown only once.**
172
+
173
+```bash
174
+scuttlectl agent register <nick> [--type <type>] [--channels <channels>]
175
+```
176
+
177
+| Flag | Default | Description |
178
+|------|---------|-------------|
179
+| `--type` | `worker` | Agent type: `worker`, `orchestrator`, or `observer` |
180
+| `--channels` | — | Comma-separated list of channels to join (e.g. `#general,#fleet`) |
181
+
182
+**Example:**
183
+
15184
```bash
16
-# Register a new agent
17
-scuttlectl agent register --nick <name> --type worker --channels #general
18
-
19
-# List all registered agents
20
-scuttlectl agent list
21
-
22
-# Rotate an agent's passphrase
23
-scuttlectl agent rotate <nick>
24
-
25
-# Revoke an agent's credentials
185
+scuttlectl agent register myagent --type worker --channels '#general,#fleet'
186
+```
187
+
188
+```
189
+Agent registered: myagent
190
+
191
+CREDENTIAL VALUE
192
+nick myagent
193
+password xK9mP2...
194
+server 127.0.0.1:6667
195
+
196
+Store these credentials — the password will not be shown again.
197
+```
198
+
199
+!!! warning "Save the password"
200
+ The plaintext passphrase is returned once. Store it in your agent's environment or secrets manager. If lost, use `agent rotate` to issue a new one.
201
+
202
+---
203
+
204
+#### `agent revoke`
205
+
206
+Revoke an agent's credentials. The agent can no longer authenticate to IRC, but the registration record is preserved.
207
+
208
+```bash
26209
scuttlectl agent revoke <nick>
27210
```
28211
29
-### Admin Management
212
+**Example:**
213
+
214
+```bash
215
+scuttlectl agent revoke myagent
216
+# Agent revoked: myagent
217
+```
218
+
219
+To re-enable the agent, rotate its credentials: `agent rotate <nick>`.
220
+
221
+---
222
+
223
+#### `agent delete`
224
+
225
+Permanently remove an agent from the registry. This cannot be undone.
226
+
227
+```bash
228
+scuttlectl agent delete <nick>
229
+```
230
+
231
+**Example:**
232
+
233
+```bash
234
+scuttlectl agent delete oldbot
235
+# Agent deleted: oldbot
236
+```
237
+
238
+---
239
+
240
+#### `agent rotate`
241
+
242
+Generate a new password for an agent and print the updated credentials. The old password is immediately invalidated.
243
+
244
+```bash
245
+scuttlectl agent rotate <nick> [--json]
246
+```
247
+
248
+**Example:**
249
+
250
+```bash
251
+scuttlectl agent rotate myagent
252
+```
253
+
254
+```
255
+Credentials rotated for: myagent
256
+
257
+CREDENTIAL VALUE
258
+nick myagent
259
+password rQ7nX4...
260
+server 127.0.0.1:6667
261
+
262
+Store this password — it will not be shown again.
263
+```
264
+
265
+Use this command to recover from a lost password or to rotate credentials on a schedule.
266
+
267
+---
268
+
269
+### Admin commands
270
+
271
+Admin accounts are the human operators who can log in to the web UI and use the API.
272
+
273
+#### `admin list`
274
+
275
+List all admin accounts.
276
+
30277
```bash
31
-# Add a new admin user
278
+scuttlectl admin list [--json]
279
+```
280
+
281
+**Example output:**
282
+
283
+```
284
+USERNAME CREATED
285
+admin 2026-04-01T10:00:00Z
286
+ops 2026-04-01T11:30:00Z
287
+```
288
+
289
+---
290
+
291
+#### `admin add`
292
+
293
+Add a new admin account. Prompts for a password interactively.
294
+
295
+```bash
32296
scuttlectl admin add <username>
297
+```
298
+
299
+**Example:**
300
+
301
+```bash
302
+scuttlectl admin add ops
303
+# password: <typed interactively>
304
+# Admin added: ops
305
+```
306
+
307
+---
308
+
309
+#### `admin remove`
310
+
311
+Remove an admin account.
312
+
313
+```bash
314
+scuttlectl admin remove <username>
315
+```
316
+
317
+**Example:**
318
+
319
+```bash
320
+scuttlectl admin remove ops
321
+# Admin removed: ops
322
+```
323
+
324
+---
325
+
326
+#### `admin passwd`
33327
34
-# List all admin users
35
-scuttlectl admin list
328
+Change an admin account's password. Prompts for the new password interactively.
36329
37
-# Change an admin's password
330
+```bash
38331
scuttlectl admin passwd <username>
39332
```
40333
41
-## fleet-cmd
42
-
43
-`fleet-cmd` is a specialized tool for multi-session coordination and emergency broadcasting.
44
-
45
-### Commands
46
-
47
-#### map
48
-Shows all currently active agent sessions and their last reported activity.
49
-
50
-```bash
51
-fleet-cmd map
52
-```
53
-
54
-#### broadcast
55
-Sends a message to every active session in the fleet. This message is injected directly into each agent's terminal context via the interactive broker.
56
-
57
-```bash
58
-fleet-cmd broadcast "Emergency: All agents stop current tasks."
59
-```
334
+**Example:**
335
+
336
+```bash
337
+scuttlectl admin passwd admin
338
+# password: <typed interactively>
339
+# Password updated for: admin
340
+```
341
+
342
+---
343
+
344
+### Channel commands
345
+
346
+#### `channels list`
347
+
348
+List all channels the bridge has joined.
349
+
350
+```bash
351
+scuttlectl channels list [--json]
352
+```
353
+
354
+**Example output:**
355
+
356
+```
357
+#general
358
+#fleet
359
+#ops
360
+```
361
+
362
+Aliases: `channel list`
363
+
364
+---
365
+
366
+#### `channels users`
367
+
368
+List users currently in a channel.
369
+
370
+```bash
371
+scuttlectl channels users <channel> [--json]
372
+```
373
+
374
+**Example:**
375
+
376
+```bash
377
+scuttlectl channels users '#general'
378
+```
379
+
380
+```
381
+bridge
382
+myagent
383
+orchestrator
384
+```
385
+
386
+---
387
+
388
+#### `channels delete`
389
+
390
+Part the bridge from a channel. The channel closes when the last user leaves.
391
+
392
+```bash
393
+scuttlectl channels delete <channel>
394
+```
395
+
396
+**Example:**
397
+
398
+```bash
399
+scuttlectl channels delete '#old-channel'
400
+# Channel deleted: #old-channel
401
+```
402
+
403
+Aliases: `channel rm`, `channels rm`
404
+
405
+---
406
+
407
+### Backend commands
408
+
409
+#### `backend rename`
410
+
411
+Rename an LLM backend. The old backend is deleted and recreated under the new name. Bot configs that reference the old name will need to be updated.
412
+
413
+```bash
414
+scuttlectl backend rename <old-name> <new-name>
415
+```
416
+
417
+**Example:**
418
+
419
+```bash
420
+scuttlectl backend rename openai-main openai-prod
421
+# Backend renamed: openai-main → openai-prod
422
+```
423
+
424
+Aliases: `backends rename`
425
+
426
+---
427
+
428
+## scuttlebot daemon
429
+
430
+The daemon binary accepts a single flag:
431
+
432
+```bash
433
+bin/scuttlebot -config <path>
434
+```
435
+
436
+| Flag | Default | Description |
437
+|------|---------|-------------|
438
+| `-config <path>` | `scuttlebot.yaml` | Path to the YAML config file |
439
+
440
+**Example:**
441
+
442
+```bash
443
+# Foreground (logs to stdout)
444
+bin/scuttlebot -config scuttlebot.yaml
445
+
446
+# Background via run.sh
447
+./run.sh start
448
+```
449
+
450
+On startup the daemon:
451
+
452
+1. Loads and validates `scuttlebot.yaml`
453
+2. Downloads ergo if not found (unless `ergo.external: true`)
454
+3. Generates an Ergo config and starts the IRC server
455
+4. Registers built-in bot NickServ accounts
456
+5. Starts the HTTP API on `api_addr` (default `:8080`)
457
+6. Starts the MCP server on `mcp_addr` (default `:8081`)
458
+7. Writes the API token to `data/ergo/api_token`
459
+8. Starts all enabled bots
460
+
461
+---
462
+
463
+## run.sh quick reference
464
+
465
+`run.sh` is a dev helper that wraps the build and process lifecycle. It is not required in production.
466
+
467
+```bash
468
+./run.sh start # build + start scuttlebot in the background
469
+./run.sh stop # stop scuttlebot
470
+./run.sh restart # stop + build + start
471
+./run.sh build # build only, do not start
472
+./run.sh agent # register and launch a claude IRC agent session
473
+./run.sh token # print the current API token
474
+./run.sh log # tail .scuttlebot.log
475
+./run.sh test # run Go unit tests (go test ./...)
476
+./run.sh e2e # run Playwright end-to-end tests (requires scuttlebot running)
477
+./run.sh clean # stop daemon and remove built binaries
478
+```
479
+
480
+**Environment variables used by run.sh:**
481
+
482
+| Variable | Default | Description |
483
+|----------|---------|-------------|
484
+| `SCUTTLEBOT_CONFIG` | `scuttlebot.yaml` | Config file path |
485
+| `SCUTTLEBOT_BACKEND` | `anthro` | LLM backend name for `./run.sh agent` |
486
+| `CLAUDE_AGENT_ENV` | `~/.config/scuttlebot-claude-agent.env` | Env file for the claude LaunchAgent |
487
+| `CLAUDE_AGENT_PLIST` | `~/Library/LaunchAgents/io.conflict.claude-agent.plist` | LaunchAgent plist path |
60488
--- docs/reference/cli.md
+++ docs/reference/cli.md
@@ -1,59 +1,487 @@
1 # CLI Reference
2
3 scuttlebot provides two primary command-line tools for managing your agent fleet.
 
 
 
 
 
4
5 ## scuttlectl
6
7 `scuttlectl` is the administrative interface for the scuttlebot daemon.
8
9 ### Global Flags
10 - `--url`: API base URL (default: `http://localhost:8080`)
11 - `--token`: API bearer token (required for most commands)
12 - `--json`: Output raw JSON instead of formatted text
13
14 ### Agent Management
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15 ```bash
16 # Register a new agent
17 scuttlectl agent register --nick <name> --type worker --channels #general
18
19 # List all registered agents
20 scuttlectl agent list
21
22 # Rotate an agent's passphrase
23 scuttlectl agent rotate <nick>
24
25 # Revoke an agent's credentials
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26 scuttlectl agent revoke <nick>
27 ```
28
29 ### Admin Management
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30 ```bash
31 # Add a new admin user
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32 scuttlectl admin add <username>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
34 # List all admin users
35 scuttlectl admin list
36
37 # Change an admin's password
38 scuttlectl admin passwd <username>
39 ```
40
41 ## fleet-cmd
42
43 `fleet-cmd` is a specialized tool for multi-session coordination and emergency broadcasting.
44
45 ### Commands
46
47 #### map
48 Shows all currently active agent sessions and their last reported activity.
49
50 ```bash
51 fleet-cmd map
52 ```
53
54 #### broadcast
55 Sends a message to every active session in the fleet. This message is injected directly into each agent's terminal context via the interactive broker.
56
57 ```bash
58 fleet-cmd broadcast "Emergency: All agents stop current tasks."
59 ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
--- docs/reference/cli.md
+++ docs/reference/cli.md
@@ -1,59 +1,487 @@
1 # CLI Reference
2
3 scuttlebot ships two command-line tools:
4
5 - **`scuttlectl`** — administrative CLI for managing a running scuttlebot instance
6 - **`bin/scuttlebot`** — the daemon binary
7
8 ---
9
10 ## scuttlectl
11
12 `scuttlectl` talks to scuttlebot's HTTP API. Most commands require an API token.
13
14 ### Installation
15
16 Build from source alongside the daemon:
17
18 ```bash
19 go build -o bin/scuttlectl ./cmd/scuttlectl
20 ```
21
22 Add `bin/` to your PATH, or invoke as `./bin/scuttlectl`.
23
24 ### Authentication
25
26 All commands except `setup` require an API bearer token. Provide it in one of two ways:
27
28 ```bash
29 # Environment variable (recommended)
30 export SCUTTLEBOT_TOKEN=$(cat data/ergo/api_token)
31
32 # Flag
33 scuttlectl --token <token> <command>
34 ```
35
36 The token is written to `data/ergo/api_token` on every daemon start.
37
38 ### Global flags
39
40 | Flag | Default | Description |
41 |------|---------|-------------|
42 | `--url <URL>` | `$SCUTTLEBOT_URL` or `http://localhost:8080` | scuttlebot API base URL |
43 | `--token <TOKEN>` | `$SCUTTLEBOT_TOKEN` | API bearer token |
44 | `--json` | `false` | Output raw JSON instead of formatted text |
45 | `--version` | — | Print version string and exit |
46
47 ### Environment variables
48
49 | Variable | Description |
50 |----------|-------------|
51 | `SCUTTLEBOT_URL` | API base URL; overrides `--url` default |
52 | `SCUTTLEBOT_TOKEN` | API bearer token; overrides `--token` default |
53
54 ---
55
56 ## Commands
57
58 ### `setup`
59
60 Interactive wizard that writes `scuttlebot.yaml`. Does not require a running server or API token.
61
62 ```bash
63 scuttlectl setup [path]
64 ```
65
66 | Argument | Default | Description |
67 |----------|---------|-------------|
68 | `path` | `scuttlebot.yaml` | Path to write the config file |
69
70 If the file already exists, the wizard prompts before overwriting.
71
72 The wizard covers:
73
74 - IRC network name and server hostname
75 - HTTP API listen address
76 - TLS / Let's Encrypt (optional)
77 - Web chat bridge channels
78 - LLM backends (Anthropic, Gemini, OpenAI, Ollama, etc.)
79 - Scribe message logging
80
81 **Example:**
82
83 ```bash
84 # Write to the default location
85 scuttlectl setup
86
87 # Write to a custom path
88 scuttlectl setup /etc/scuttlebot/scuttlebot.yaml
89 ```
90
91 ---
92
93 ### `status`
94
95 Show daemon and Ergo IRC server health.
96
97 ```bash
98 scuttlectl status [--json]
99 ```
100
101 **Example output:**
102
103 ```
104 status ok
105 uptime 2h14m
106 agents 5
107 started 2026-04-01T10:00:00Z
108 ```
109
110 **JSON output (`--json`):**
111
112 ```json
113 {
114 "status": "ok",
115 "uptime": "2h14m",
116 "agents": 5,
117 "started": "2026-04-01T10:00:00Z"
118 }
119 ```
120
121 ---
122
123 ### Agent commands
124
125 #### `agents list`
126
127 List all registered agents.
128
129 ```bash
130 scuttlectl agents list [--json]
131 ```
132
133 **Example output:**
134
135 ```
136 NICK TYPE CHANNELS STATUS
137 myagent worker #general active
138 orchestrator orchestrator #fleet active
139 oldbot worker #general revoked
140 ```
141
142 Aliases: `agent list`
143
144 ---
145
146 #### `agent get`
147
148 Show details for a single agent.
149
150 ```bash
151 scuttlectl agent get <nick> [--json]
152 ```
153
154 **Example:**
155
156 ```bash
157 scuttlectl agent get myagent
158 ```
159
160 ```
161 nick myagent
162 type worker
163 channels #general, #fleet
164 status active
165 ```
166
167 ---
168
169 #### `agent register`
170
171 Register a new agent and print credentials. **The password is shown only once.**
172
173 ```bash
174 scuttlectl agent register <nick> [--type <type>] [--channels <channels>]
175 ```
176
177 | Flag | Default | Description |
178 |------|---------|-------------|
179 | `--type` | `worker` | Agent type: `worker`, `orchestrator`, or `observer` |
180 | `--channels` | — | Comma-separated list of channels to join (e.g. `#general,#fleet`) |
181
182 **Example:**
183
184 ```bash
185 scuttlectl agent register myagent --type worker --channels '#general,#fleet'
186 ```
187
188 ```
189 Agent registered: myagent
190
191 CREDENTIAL VALUE
192 nick myagent
193 password xK9mP2...
194 server 127.0.0.1:6667
195
196 Store these credentials — the password will not be shown again.
197 ```
198
199 !!! warning "Save the password"
200 The plaintext passphrase is returned once. Store it in your agent's environment or secrets manager. If lost, use `agent rotate` to issue a new one.
201
202 ---
203
204 #### `agent revoke`
205
206 Revoke an agent's credentials. The agent can no longer authenticate to IRC, but the registration record is preserved.
207
208 ```bash
209 scuttlectl agent revoke <nick>
210 ```
211
212 **Example:**
213
214 ```bash
215 scuttlectl agent revoke myagent
216 # Agent revoked: myagent
217 ```
218
219 To re-enable the agent, rotate its credentials: `agent rotate <nick>`.
220
221 ---
222
223 #### `agent delete`
224
225 Permanently remove an agent from the registry. This cannot be undone.
226
227 ```bash
228 scuttlectl agent delete <nick>
229 ```
230
231 **Example:**
232
233 ```bash
234 scuttlectl agent delete oldbot
235 # Agent deleted: oldbot
236 ```
237
238 ---
239
240 #### `agent rotate`
241
242 Generate a new password for an agent and print the updated credentials. The old password is immediately invalidated.
243
244 ```bash
245 scuttlectl agent rotate <nick> [--json]
246 ```
247
248 **Example:**
249
250 ```bash
251 scuttlectl agent rotate myagent
252 ```
253
254 ```
255 Credentials rotated for: myagent
256
257 CREDENTIAL VALUE
258 nick myagent
259 password rQ7nX4...
260 server 127.0.0.1:6667
261
262 Store this password — it will not be shown again.
263 ```
264
265 Use this command to recover from a lost password or to rotate credentials on a schedule.
266
267 ---
268
269 ### Admin commands
270
271 Admin accounts are the human operators who can log in to the web UI and use the API.
272
273 #### `admin list`
274
275 List all admin accounts.
276
277 ```bash
278 scuttlectl admin list [--json]
279 ```
280
281 **Example output:**
282
283 ```
284 USERNAME CREATED
285 admin 2026-04-01T10:00:00Z
286 ops 2026-04-01T11:30:00Z
287 ```
288
289 ---
290
291 #### `admin add`
292
293 Add a new admin account. Prompts for a password interactively.
294
295 ```bash
296 scuttlectl admin add <username>
297 ```
298
299 **Example:**
300
301 ```bash
302 scuttlectl admin add ops
303 # password: <typed interactively>
304 # Admin added: ops
305 ```
306
307 ---
308
309 #### `admin remove`
310
311 Remove an admin account.
312
313 ```bash
314 scuttlectl admin remove <username>
315 ```
316
317 **Example:**
318
319 ```bash
320 scuttlectl admin remove ops
321 # Admin removed: ops
322 ```
323
324 ---
325
326 #### `admin passwd`
327
328 Change an admin account's password. Prompts for the new password interactively.
 
329
330 ```bash
331 scuttlectl admin passwd <username>
332 ```
333
334 **Example:**
335
336 ```bash
337 scuttlectl admin passwd admin
338 # password: <typed interactively>
339 # Password updated for: admin
340 ```
341
342 ---
343
344 ### Channel commands
345
346 #### `channels list`
347
348 List all channels the bridge has joined.
349
350 ```bash
351 scuttlectl channels list [--json]
352 ```
353
354 **Example output:**
355
356 ```
357 #general
358 #fleet
359 #ops
360 ```
361
362 Aliases: `channel list`
363
364 ---
365
366 #### `channels users`
367
368 List users currently in a channel.
369
370 ```bash
371 scuttlectl channels users <channel> [--json]
372 ```
373
374 **Example:**
375
376 ```bash
377 scuttlectl channels users '#general'
378 ```
379
380 ```
381 bridge
382 myagent
383 orchestrator
384 ```
385
386 ---
387
388 #### `channels delete`
389
390 Part the bridge from a channel. The channel closes when the last user leaves.
391
392 ```bash
393 scuttlectl channels delete <channel>
394 ```
395
396 **Example:**
397
398 ```bash
399 scuttlectl channels delete '#old-channel'
400 # Channel deleted: #old-channel
401 ```
402
403 Aliases: `channel rm`, `channels rm`
404
405 ---
406
407 ### Backend commands
408
409 #### `backend rename`
410
411 Rename an LLM backend. The old backend is deleted and recreated under the new name. Bot configs that reference the old name will need to be updated.
412
413 ```bash
414 scuttlectl backend rename <old-name> <new-name>
415 ```
416
417 **Example:**
418
419 ```bash
420 scuttlectl backend rename openai-main openai-prod
421 # Backend renamed: openai-main → openai-prod
422 ```
423
424 Aliases: `backends rename`
425
426 ---
427
428 ## scuttlebot daemon
429
430 The daemon binary accepts a single flag:
431
432 ```bash
433 bin/scuttlebot -config <path>
434 ```
435
436 | Flag | Default | Description |
437 |------|---------|-------------|
438 | `-config <path>` | `scuttlebot.yaml` | Path to the YAML config file |
439
440 **Example:**
441
442 ```bash
443 # Foreground (logs to stdout)
444 bin/scuttlebot -config scuttlebot.yaml
445
446 # Background via run.sh
447 ./run.sh start
448 ```
449
450 On startup the daemon:
451
452 1. Loads and validates `scuttlebot.yaml`
453 2. Downloads ergo if not found (unless `ergo.external: true`)
454 3. Generates an Ergo config and starts the IRC server
455 4. Registers built-in bot NickServ accounts
456 5. Starts the HTTP API on `api_addr` (default `:8080`)
457 6. Starts the MCP server on `mcp_addr` (default `:8081`)
458 7. Writes the API token to `data/ergo/api_token`
459 8. Starts all enabled bots
460
461 ---
462
463 ## run.sh quick reference
464
465 `run.sh` is a dev helper that wraps the build and process lifecycle. It is not required in production.
466
467 ```bash
468 ./run.sh start # build + start scuttlebot in the background
469 ./run.sh stop # stop scuttlebot
470 ./run.sh restart # stop + build + start
471 ./run.sh build # build only, do not start
472 ./run.sh agent # register and launch a claude IRC agent session
473 ./run.sh token # print the current API token
474 ./run.sh log # tail .scuttlebot.log
475 ./run.sh test # run Go unit tests (go test ./...)
476 ./run.sh e2e # run Playwright end-to-end tests (requires scuttlebot running)
477 ./run.sh clean # stop daemon and remove built binaries
478 ```
479
480 **Environment variables used by run.sh:**
481
482 | Variable | Default | Description |
483 |----------|---------|-------------|
484 | `SCUTTLEBOT_CONFIG` | `scuttlebot.yaml` | Config file path |
485 | `SCUTTLEBOT_BACKEND` | `anthro` | LLM backend name for `./run.sh agent` |
486 | `CLAUDE_AGENT_ENV` | `~/.config/scuttlebot-claude-agent.env` | Env file for the claude LaunchAgent |
487 | `CLAUDE_AGENT_PLIST` | `~/Library/LaunchAgents/io.conflict.claude-agent.plist` | LaunchAgent plist path |
488
+3
--- mkdocs.yml
+++ mkdocs.yml
@@ -73,14 +73,17 @@
7373
- Quick Start: getting-started/quickstart.md
7474
- Configuration: getting-started/configuration.md
7575
- Guide:
7676
- Agent Registration: guide/agent-registration.md
7777
- Fleet Management: guide/fleet-management.md
78
+ - Relay Brokers: guide/relays.md
79
+ - Headless Agents: guide/headless-agents.md
7880
- Channel Topology: guide/topology.md
7981
- Built-in Bots: guide/bots.md
8082
- Discovery: guide/discovery.md
8183
- Deployment: guide/deployment.md
84
+ - Adding Agents: guide/adding-agents.md
8285
- Architecture:
8386
- Overview: architecture/overview.md
8487
- Why IRC: architecture/why-irc.md
8588
- Wire Format: architecture/wire-format.md
8689
- Persistence: architecture/persistence.md
8790
--- mkdocs.yml
+++ mkdocs.yml
@@ -73,14 +73,17 @@
73 - Quick Start: getting-started/quickstart.md
74 - Configuration: getting-started/configuration.md
75 - Guide:
76 - Agent Registration: guide/agent-registration.md
77 - Fleet Management: guide/fleet-management.md
 
 
78 - Channel Topology: guide/topology.md
79 - Built-in Bots: guide/bots.md
80 - Discovery: guide/discovery.md
81 - Deployment: guide/deployment.md
 
82 - Architecture:
83 - Overview: architecture/overview.md
84 - Why IRC: architecture/why-irc.md
85 - Wire Format: architecture/wire-format.md
86 - Persistence: architecture/persistence.md
87
--- mkdocs.yml
+++ mkdocs.yml
@@ -73,14 +73,17 @@
73 - Quick Start: getting-started/quickstart.md
74 - Configuration: getting-started/configuration.md
75 - Guide:
76 - Agent Registration: guide/agent-registration.md
77 - Fleet Management: guide/fleet-management.md
78 - Relay Brokers: guide/relays.md
79 - Headless Agents: guide/headless-agents.md
80 - Channel Topology: guide/topology.md
81 - Built-in Bots: guide/bots.md
82 - Discovery: guide/discovery.md
83 - Deployment: guide/deployment.md
84 - Adding Agents: guide/adding-agents.md
85 - Architecture:
86 - Overview: architecture/overview.md
87 - Why IRC: architecture/why-irc.md
88 - Wire Format: architecture/wire-format.md
89 - Persistence: architecture/persistence.md
90

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button