ScuttleBot

feat(#34): topology config schema — channel types and autojoin rules in scuttlebot.yaml

lmata 2026-04-02 05:09 trunk
Commit 5705614b2d6a3e7b53f40a9fc6edca84678773232ac65102cbdbee3b05365a90
--- deploy/standalone/scuttlebot.yaml.example
+++ deploy/standalone/scuttlebot.yaml.example
@@ -16,5 +16,7 @@
1616
datastore:
1717
# driver: sqlite # "sqlite" or "postgres"
1818
# dsn: ./data/scuttlebot.db
1919
2020
# api_addr: :8080 # scuttlebot REST API listen address
21
+
22
+# topology: (see scuttlebot.yaml for full annotated example)
2123
--- deploy/standalone/scuttlebot.yaml.example
+++ deploy/standalone/scuttlebot.yaml.example
@@ -16,5 +16,7 @@
16 datastore:
17 # driver: sqlite # "sqlite" or "postgres"
18 # dsn: ./data/scuttlebot.db
19
20 # api_addr: :8080 # scuttlebot REST API listen address
 
 
21
--- deploy/standalone/scuttlebot.yaml.example
+++ deploy/standalone/scuttlebot.yaml.example
@@ -16,5 +16,7 @@
16 datastore:
17 # driver: sqlite # "sqlite" or "postgres"
18 # dsn: ./data/scuttlebot.db
19
20 # api_addr: :8080 # scuttlebot REST API listen address
21
22 # topology: (see scuttlebot.yaml for full annotated example)
23
--- internal/config/config.go
+++ internal/config/config.go
@@ -2,10 +2,11 @@
22
package config
33
44
import (
55
"fmt"
66
"os"
7
+ "time"
78
89
"gopkg.in/yaml.v3"
910
)
1011
1112
// Config is the top-level scuttlebot configuration.
@@ -13,10 +14,11 @@
1314
Ergo ErgoConfig `yaml:"ergo"`
1415
Datastore DatastoreConfig `yaml:"datastore"`
1516
Bridge BridgeConfig `yaml:"bridge"`
1617
TLS TLSConfig `yaml:"tls"`
1718
LLM LLMConfig `yaml:"llm"`
19
+ Topology TopologyConfig `yaml:"topology"`
1820
1921
// APIAddr is the address for scuttlebot's own HTTP management API.
2022
// Ignored when TLS.Domain is set (HTTPS runs on :443, HTTP on :80).
2123
// Default: ":8080"
2224
APIAddr string `yaml:"api_addr"`
@@ -189,10 +191,85 @@
189191
// DSN is the data source name.
190192
// For sqlite: path to the .db file.
191193
// For postgres: connection string.
192194
DSN string `yaml:"dsn"`
193195
}
196
+
197
+// TopologyConfig is the top-level channel topology declaration.
198
+// It defines static channels provisioned at startup and dynamic channel type
199
+// rules applied when agents create channels at runtime.
200
+type TopologyConfig struct {
201
+ // Channels are static channels provisioned at daemon startup.
202
+ Channels []StaticChannelConfig `yaml:"channels"`
203
+
204
+ // Types are prefix-based rules applied to dynamically created channels.
205
+ // The first matching prefix wins.
206
+ Types []ChannelTypeConfig `yaml:"types"`
207
+}
208
+
209
+// StaticChannelConfig describes a channel that is provisioned at startup.
210
+type StaticChannelConfig struct {
211
+ // Name is the full channel name including the # prefix (e.g. "#general").
212
+ Name string `yaml:"name"`
213
+
214
+ // Topic is the initial channel topic.
215
+ Topic string `yaml:"topic"`
216
+
217
+ // Ops is a list of nicks to grant channel operator (+o) access.
218
+ Ops []string `yaml:"ops"`
219
+
220
+ // Voice is a list of nicks to grant voice (+v) access.
221
+ Voice []string `yaml:"voice"`
222
+
223
+ // Autojoin is a list of bot nicks to invite when the channel is provisioned.
224
+ Autojoin []string `yaml:"autojoin"`
225
+}
226
+
227
+// ChannelTypeConfig defines policy rules for a class of dynamically created channels.
228
+// Matched by prefix against channel names (e.g. prefix "task." matches "#task.gh-42").
229
+type ChannelTypeConfig struct {
230
+ // Name is a human-readable type identifier (e.g. "task", "sprint", "incident").
231
+ Name string `yaml:"name"`
232
+
233
+ // Prefix is matched against channel names after stripping the leading #.
234
+ // The first matching type wins. (e.g. "task." matches "#task.gh-42")
235
+ Prefix string `yaml:"prefix"`
236
+
237
+ // Autojoin is a list of bot nicks to invite when a channel of this type is created.
238
+ Autojoin []string `yaml:"autojoin"`
239
+
240
+ // Supervision is the coordination channel where summaries should surface.
241
+ // Agents receive this when they create a channel so they know where to also post.
242
+ // May be a static channel name (e.g. "#general") or a type prefix pattern
243
+ // (e.g. "sprint." — resolved to the most recently created matching channel).
244
+ Supervision string `yaml:"supervision"`
245
+
246
+ // Ephemeral marks channels of this type for automatic cleanup.
247
+ Ephemeral bool `yaml:"ephemeral"`
248
+
249
+ // TTL is the maximum lifetime of an ephemeral channel with no non-bot members.
250
+ // Zero means no TTL; cleanup only occurs when the channel is empty.
251
+ TTL Duration `yaml:"ttl"`
252
+}
253
+
254
+// Duration wraps time.Duration for YAML unmarshalling ("72h", "30m", etc.).
255
+type Duration struct {
256
+ time.Duration
257
+}
258
+
259
+func (d *Duration) UnmarshalYAML(value *yaml.Node) error {
260
+ var s string
261
+ if err := value.Decode(&s); err != nil {
262
+ return err
263
+ }
264
+ dur, err := time.ParseDuration(s)
265
+ if err != nil {
266
+ return fmt.Errorf("config: invalid duration %q: %w", s, err)
267
+ }
268
+ d.Duration = dur
269
+ return nil
270
+}
194271
195272
// Defaults fills in zero values with sensible defaults.
196273
func (c *Config) Defaults() {
197274
if c.Ergo.BinaryPath == "" {
198275
c.Ergo.BinaryPath = "ergo"
199276
200277
ADDED internal/config/config_test.go
--- internal/config/config.go
+++ internal/config/config.go
@@ -2,10 +2,11 @@
2 package config
3
4 import (
5 "fmt"
6 "os"
 
7
8 "gopkg.in/yaml.v3"
9 )
10
11 // Config is the top-level scuttlebot configuration.
@@ -13,10 +14,11 @@
13 Ergo ErgoConfig `yaml:"ergo"`
14 Datastore DatastoreConfig `yaml:"datastore"`
15 Bridge BridgeConfig `yaml:"bridge"`
16 TLS TLSConfig `yaml:"tls"`
17 LLM LLMConfig `yaml:"llm"`
 
18
19 // APIAddr is the address for scuttlebot's own HTTP management API.
20 // Ignored when TLS.Domain is set (HTTPS runs on :443, HTTP on :80).
21 // Default: ":8080"
22 APIAddr string `yaml:"api_addr"`
@@ -189,10 +191,85 @@
189 // DSN is the data source name.
190 // For sqlite: path to the .db file.
191 // For postgres: connection string.
192 DSN string `yaml:"dsn"`
193 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
195 // Defaults fills in zero values with sensible defaults.
196 func (c *Config) Defaults() {
197 if c.Ergo.BinaryPath == "" {
198 c.Ergo.BinaryPath = "ergo"
199
200 DDED internal/config/config_test.go
--- internal/config/config.go
+++ internal/config/config.go
@@ -2,10 +2,11 @@
2 package config
3
4 import (
5 "fmt"
6 "os"
7 "time"
8
9 "gopkg.in/yaml.v3"
10 )
11
12 // Config is the top-level scuttlebot configuration.
@@ -13,10 +14,11 @@
14 Ergo ErgoConfig `yaml:"ergo"`
15 Datastore DatastoreConfig `yaml:"datastore"`
16 Bridge BridgeConfig `yaml:"bridge"`
17 TLS TLSConfig `yaml:"tls"`
18 LLM LLMConfig `yaml:"llm"`
19 Topology TopologyConfig `yaml:"topology"`
20
21 // APIAddr is the address for scuttlebot's own HTTP management API.
22 // Ignored when TLS.Domain is set (HTTPS runs on :443, HTTP on :80).
23 // Default: ":8080"
24 APIAddr string `yaml:"api_addr"`
@@ -189,10 +191,85 @@
191 // DSN is the data source name.
192 // For sqlite: path to the .db file.
193 // For postgres: connection string.
194 DSN string `yaml:"dsn"`
195 }
196
197 // TopologyConfig is the top-level channel topology declaration.
198 // It defines static channels provisioned at startup and dynamic channel type
199 // rules applied when agents create channels at runtime.
200 type TopologyConfig struct {
201 // Channels are static channels provisioned at daemon startup.
202 Channels []StaticChannelConfig `yaml:"channels"`
203
204 // Types are prefix-based rules applied to dynamically created channels.
205 // The first matching prefix wins.
206 Types []ChannelTypeConfig `yaml:"types"`
207 }
208
209 // StaticChannelConfig describes a channel that is provisioned at startup.
210 type StaticChannelConfig struct {
211 // Name is the full channel name including the # prefix (e.g. "#general").
212 Name string `yaml:"name"`
213
214 // Topic is the initial channel topic.
215 Topic string `yaml:"topic"`
216
217 // Ops is a list of nicks to grant channel operator (+o) access.
218 Ops []string `yaml:"ops"`
219
220 // Voice is a list of nicks to grant voice (+v) access.
221 Voice []string `yaml:"voice"`
222
223 // Autojoin is a list of bot nicks to invite when the channel is provisioned.
224 Autojoin []string `yaml:"autojoin"`
225 }
226
227 // ChannelTypeConfig defines policy rules for a class of dynamically created channels.
228 // Matched by prefix against channel names (e.g. prefix "task." matches "#task.gh-42").
229 type ChannelTypeConfig struct {
230 // Name is a human-readable type identifier (e.g. "task", "sprint", "incident").
231 Name string `yaml:"name"`
232
233 // Prefix is matched against channel names after stripping the leading #.
234 // The first matching type wins. (e.g. "task." matches "#task.gh-42")
235 Prefix string `yaml:"prefix"`
236
237 // Autojoin is a list of bot nicks to invite when a channel of this type is created.
238 Autojoin []string `yaml:"autojoin"`
239
240 // Supervision is the coordination channel where summaries should surface.
241 // Agents receive this when they create a channel so they know where to also post.
242 // May be a static channel name (e.g. "#general") or a type prefix pattern
243 // (e.g. "sprint." — resolved to the most recently created matching channel).
244 Supervision string `yaml:"supervision"`
245
246 // Ephemeral marks channels of this type for automatic cleanup.
247 Ephemeral bool `yaml:"ephemeral"`
248
249 // TTL is the maximum lifetime of an ephemeral channel with no non-bot members.
250 // Zero means no TTL; cleanup only occurs when the channel is empty.
251 TTL Duration `yaml:"ttl"`
252 }
253
254 // Duration wraps time.Duration for YAML unmarshalling ("72h", "30m", etc.).
255 type Duration struct {
256 time.Duration
257 }
258
259 func (d *Duration) UnmarshalYAML(value *yaml.Node) error {
260 var s string
261 if err := value.Decode(&s); err != nil {
262 return err
263 }
264 dur, err := time.ParseDuration(s)
265 if err != nil {
266 return fmt.Errorf("config: invalid duration %q: %w", s, err)
267 }
268 d.Duration = dur
269 return nil
270 }
271
272 // Defaults fills in zero values with sensible defaults.
273 func (c *Config) Defaults() {
274 if c.Ergo.BinaryPath == "" {
275 c.Ergo.BinaryPath = "ergo"
276
277 DDED internal/config/config_test.go
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -0,0 +1,12 @@
1
+package config
2
+
3
+import (
4
+ "mport (
5
+ "encoding/json"
6
+ "os"
7
+ "path/filepath"
8
+ "testing"
9
+ "time"
10
+)
11
+
12
+func Test
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -0,0 +1,12 @@
1 package config
2
3 import (
4 "mport (
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "testing"
9 "time"
10 )
11
12 func Test
--- scuttlebot.yaml
+++ scuttlebot.yaml
@@ -23,5 +23,56 @@
2323
# channels: ["#general"]
2424
# buffer_size: 200
2525
# web_user_ttl_minutes: 5 # keep HTTP bridge nicks visible in /users after their last post
2626
2727
# api_addr: :8080 # scuttlebot REST API listen address
28
+
29
+# topology defines channel structure and autojoin policy.
30
+# Static channels are provisioned at startup; types apply to agent-created channels.
31
+# This is domain-agnostic — replace the example types with whatever fits your workflow.
32
+#
33
+# topology:
34
+# channels:
35
+# - name: "#general"
36
+# topic: "Fleet coordination"
37
+# ops: [bridge, oracle]
38
+# autojoin: [bridge, oracle, scribe, herald]
39
+# - name: "#alerts"
40
+# topic: "Alerts and incidents"
41
+# autojoin: [bridge, sentinel, steward, oracle]
42
+#
43
+# types:
44
+# # --- software development (default example) ---
45
+# - name: task
46
+# prefix: "task." # matches #task.gh-42, #task.JIRA-99, etc.
47
+# autojoin: [bridge, scribe]
48
+# supervision: "#general" # where summaries surface; agents post here too
49
+# ephemeral: true
50
+# ttl: 72h
51
+# - name: sprint
52
+# prefix: "sprint."
53
+# autojoin: [bridge, oracle, herald]
54
+# - name: feature
55
+# prefix: "feature."
56
+# autojoin: [bridge, scribe, herald]
57
+# supervision: "#general"
58
+# - name: infra
59
+# prefix: "infra."
60
+# autojoin: [bridge, sentinel, steward]
61
+# supervision: "#alerts"
62
+# - name: incident
63
+# prefix: "incident."
64
+# autojoin: [bridge, sentinel, steward, oracle, auditbot]
65
+# supervision: "#alerts"
66
+# ephemeral: true
67
+# ttl: 168h # 1 week
68
+#
69
+# # --- other domain examples (uncomment/replace as needed) ---
70
+# # - name: experiment # data science
71
+# # prefix: "experiment."
72
+# # autojoin: [bridge, scribe]
73
+# # - name: ticket # customer support
74
+# # prefix: "ticket."
75
+# # autojoin: [bridge, scribe]
76
+# # supervision: "#general"
77
+# # ephemeral: true
78
+# # ttl: 48h
2879
--- scuttlebot.yaml
+++ scuttlebot.yaml
@@ -23,5 +23,56 @@
23 # channels: ["#general"]
24 # buffer_size: 200
25 # web_user_ttl_minutes: 5 # keep HTTP bridge nicks visible in /users after their last post
26
27 # api_addr: :8080 # scuttlebot REST API listen address
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
--- scuttlebot.yaml
+++ scuttlebot.yaml
@@ -23,5 +23,56 @@
23 # channels: ["#general"]
24 # buffer_size: 200
25 # web_user_ttl_minutes: 5 # keep HTTP bridge nicks visible in /users after their last post
26
27 # api_addr: :8080 # scuttlebot REST API listen address
28
29 # topology defines channel structure and autojoin policy.
30 # Static channels are provisioned at startup; types apply to agent-created channels.
31 # This is domain-agnostic — replace the example types with whatever fits your workflow.
32 #
33 # topology:
34 # channels:
35 # - name: "#general"
36 # topic: "Fleet coordination"
37 # ops: [bridge, oracle]
38 # autojoin: [bridge, oracle, scribe, herald]
39 # - name: "#alerts"
40 # topic: "Alerts and incidents"
41 # autojoin: [bridge, sentinel, steward, oracle]
42 #
43 # types:
44 # # --- software development (default example) ---
45 # - name: task
46 # prefix: "task." # matches #task.gh-42, #task.JIRA-99, etc.
47 # autojoin: [bridge, scribe]
48 # supervision: "#general" # where summaries surface; agents post here too
49 # ephemeral: true
50 # ttl: 72h
51 # - name: sprint
52 # prefix: "sprint."
53 # autojoin: [bridge, oracle, herald]
54 # - name: feature
55 # prefix: "feature."
56 # autojoin: [bridge, scribe, herald]
57 # supervision: "#general"
58 # - name: infra
59 # prefix: "infra."
60 # autojoin: [bridge, sentinel, steward]
61 # supervision: "#alerts"
62 # - name: incident
63 # prefix: "incident."
64 # autojoin: [bridge, sentinel, steward, oracle, auditbot]
65 # supervision: "#alerts"
66 # ephemeral: true
67 # ttl: 168h # 1 week
68 #
69 # # --- other domain examples (uncomment/replace as needed) ---
70 # # - name: experiment # data science
71 # # prefix: "experiment."
72 # # autojoin: [bridge, scribe]
73 # # - name: ticket # customer support
74 # # prefix: "ticket."
75 # # autojoin: [bridge, scribe]
76 # # supervision: "#general"
77 # # ephemeral: true
78 # # ttl: 48h
79

Keyboard Shortcuts

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