ScuttleBot

chore: clean worktree — gitignore artifacts, fix AGENTS.md link, add missing files - .gitignore: add REPORT.md, coverage.out, test-results/ - AGENTS.md: fix broken memory.md link → memory/MEMORY.md - Add scuttlebot daemon binary (release artifact, same convention as other root binaries) - Add .github/ISSUE_TEMPLATE/ (bug_report.yml, feature_request.yml) - Add internal/llm/filter_test.go (was untracked) Note: root-level binaries (scuttlebot, claude-relay, codex-relay, gemini-relay, scuttlectl) are intentionally committed as convenience release artifacts.

lmata 2026-04-02 23:22 trunk
Commit def1055bad3c699acc43d96b88f73f1ddc23991eb12e97987123ed0253cab9cf
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,50 @@
1
+name: Bug report
2
+description: Something isn't working
3
+labels: ["bug"]
4
+body:
5
+ - type: markdown
6
+ attributes:
7
+ value: Thanks for taking the time to report a bug.
8
+
9
+ - type: textarea
10
+ id: description
11
+ attributes:
12
+ label: What happened?
13
+ description: A clear description of the bug.
14
+ validations:
15
+ required: true
16
+
17
+ - type: textarea
18
+ id: reproduction
19
+ attributes:
20
+ label: Steps to reproduce
21
+ placeholder: |
22
+ 1. Start scuttlebot with config X
23
+ 2. Register an agent
24
+ 3. ...
25
+ validations:
26
+ required: true
27
+
28
+ - type: textarea
29
+ id: expected
30
+ attributes:
31
+ label: Expected behaviour
32
+ validations:
33
+ required: true
34
+
35
+ - type: textarea
36
+ id: environment
37
+ attributes:
38
+ label: Environment
39
+ placeholder: |
40
+ - scuttlebot version: v0.1.0
41
+ - Go: 1.22
42
+ - OS: macOS 15 / Ubuntu 24.04
43
+ validations:
44
+ required: true
45
+
46
+ - type: textarea
47
+ id: logs
48
+ attributes:
49
+ label: Relevant logs or output
50
+ render: shell
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,50 @@
1 name: Bug report
2 description: Something isn't working
3 labels: ["bug"]
4 body:
5 - type: markdown
6 attributes:
7 value: Thanks for taking the time to report a bug.
8
9 - type: textarea
10 id: description
11 attributes:
12 label: What happened?
13 description: A clear description of the bug.
14 validations:
15 required: true
16
17 - type: textarea
18 id: reproduction
19 attributes:
20 label: Steps to reproduce
21 placeholder: |
22 1. Start scuttlebot with config X
23 2. Register an agent
24 3. ...
25 validations:
26 required: true
27
28 - type: textarea
29 id: expected
30 attributes:
31 label: Expected behaviour
32 validations:
33 required: true
34
35 - type: textarea
36 id: environment
37 attributes:
38 label: Environment
39 placeholder: |
40 - scuttlebot version: v0.1.0
41 - Go: 1.22
42 - OS: macOS 15 / Ubuntu 24.04
43 validations:
44 required: true
45
46 - type: textarea
47 id: logs
48 attributes:
49 label: Relevant logs or output
50 render: shell
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,35 @@
1
+name: Feature request
2
+description: Suggest something new
3
+labels: ["enhancement"]
4
+body:
5
+ - type: markdown
6
+ attributes:
7
+ value: Thanks for suggesting a feature.
8
+
9
+ - type: textarea
10
+ id: problem
11
+ attributes:
12
+ label: What problem does this solve?
13
+ description: Describe the context and the gap you're running into.
14
+ validations:
15
+ required: true
16
+
17
+ - type: textarea
18
+ id: solution
19
+ attributes:
20
+ label: Proposed solution
21
+ description: What would you like to see?
22
+ validations:
23
+ required: true
24
+
25
+ - type: textarea
26
+ id: alternatives
27
+ attributes:
28
+ label: Alternatives considered
29
+ description: Any other approaches you've thought about?
30
+
31
+ - type: textarea
32
+ id: context
33
+ attributes:
34
+ label: Additional context
35
+ description: Links, examples, prior art — anything useful.
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,35 @@
1 name: Feature request
2 description: Suggest something new
3 labels: ["enhancement"]
4 body:
5 - type: markdown
6 attributes:
7 value: Thanks for suggesting a feature.
8
9 - type: textarea
10 id: problem
11 attributes:
12 label: What problem does this solve?
13 description: Describe the context and the gap you're running into.
14 validations:
15 required: true
16
17 - type: textarea
18 id: solution
19 attributes:
20 label: Proposed solution
21 description: What would you like to see?
22 validations:
23 required: true
24
25 - type: textarea
26 id: alternatives
27 attributes:
28 label: Alternatives considered
29 description: Any other approaches you've thought about?
30
31 - type: textarea
32 id: context
33 attributes:
34 label: Additional context
35 description: Links, examples, prior art — anything useful.
+3
--- .gitignore
+++ .gitignore
@@ -5,10 +5,13 @@
55
*.log
66
*.pid
77
scuttlebot.yaml
88
tests/e2e/
99
tmp-*.sh
10
+REPORT.md
11
+coverage.out
12
+test-results/
1013
1114
# Compiled relay binaries are committed to the repo for convenience (no build step required).
1215
# If you rebuild locally, `go build` output goes to bin/ (ignored above).
1316
# The root-level binaries (claude-relay, codex-relay, gemini-relay, scuttlectl, scuttlebot)
1417
# are the published release artifacts — do not add them here.
1518
--- .gitignore
+++ .gitignore
@@ -5,10 +5,13 @@
5 *.log
6 *.pid
7 scuttlebot.yaml
8 tests/e2e/
9 tmp-*.sh
 
 
 
10
11 # Compiled relay binaries are committed to the repo for convenience (no build step required).
12 # If you rebuild locally, `go build` output goes to bin/ (ignored above).
13 # The root-level binaries (claude-relay, codex-relay, gemini-relay, scuttlectl, scuttlebot)
14 # are the published release artifacts — do not add them here.
15
--- .gitignore
+++ .gitignore
@@ -5,10 +5,13 @@
5 *.log
6 *.pid
7 scuttlebot.yaml
8 tests/e2e/
9 tmp-*.sh
10 REPORT.md
11 coverage.out
12 test-results/
13
14 # Compiled relay binaries are committed to the repo for convenience (no build step required).
15 # If you rebuild locally, `go build` output goes to bin/ (ignored above).
16 # The root-level binaries (claude-relay, codex-relay, gemini-relay, scuttlectl, scuttlebot)
17 # are the published release artifacts — do not add them here.
18
+1 -1
--- AGENTS.md
+++ AGENTS.md
@@ -1,6 +1,6 @@
11
# Agents — scuttlebot
22
33
Primary conventions doc: [`bootstrap.md`](bootstrap.md)
4
-Context seed: [`memory.md`](memory.md)
4
+Context seed: [`memory/MEMORY.md`](memory/MEMORY.md)
55
66
Read both before writing any code.
77
88
ADDED internal/llm/filter_test.go
99
ADDED scuttlebot
--- AGENTS.md
+++ AGENTS.md
@@ -1,6 +1,6 @@
1 # Agents — scuttlebot
2
3 Primary conventions doc: [`bootstrap.md`](bootstrap.md)
4 Context seed: [`memory.md`](memory.md)
5
6 Read both before writing any code.
7
8 DDED internal/llm/filter_test.go
9 DDED scuttlebot
--- AGENTS.md
+++ AGENTS.md
@@ -1,6 +1,6 @@
1 # Agents — scuttlebot
2
3 Primary conventions doc: [`bootstrap.md`](bootstrap.md)
4 Context seed: [`memory/MEMORY.md`](memory/MEMORY.md)
5
6 Read both before writing any code.
7
8 DDED internal/llm/filter_test.go
9 DDED scuttlebot
--- a/internal/llm/filter_test.go
+++ b/internal/llm/filter_test.go
@@ -0,0 +1,134 @@
1
+package llm
2
+
3
+import (
4
+ "testing"
5
+)
6
+
7
+func models(ids ...string) []ModelInfo {
8
+ out := make([]ModelInfo, len(ids))
9
+ for i, id := range ids {
10
+ out[i] = ModelInfo{ID: id, Name: id}
11
+ }
12
+ return out
13
+}
14
+
15
+func ids(ms []ModelInfo) []string {
16
+ out := make([]string, len(ms))
17
+ for i, m := range ms {
18
+ out[i] = m.ID
19
+ }
20
+ return out
21
+}
22
+
23
+func TestNewModelFilterInvalidAllow(t *testing.T) {
24
+ _, err := NewModelFilter([]string{"["}, nil)
25
+ if err == nil {
26
+ t.Fatal("expected error for invalid allow pattern, got nil")
27
+ }
28
+}
29
+
30
+func TestNewModelFilterInvalidBlock(t *testing.T) {
31
+ _, err := NewModelFilter(nil, []string{"["})
32
+ if err == nil {
33
+ t.Fatal("expected error for invalid block pattern, got nil")
34
+ }
35
+}
36
+
37
+func TestFilterNoPatterns(t *testing.T) {
38
+ f, err := NewModelFilter(nil, nil)
39
+ if err != nil {
40
+ t.Fatal(err)
41
+ }
42
+ input := models("gpt-4", "claude-3", "gemini-pro")
43
+ got := f.Apply(input)
44
+ if len(got) != len(input) {
45
+ t.Errorf("no patterns: got %d models, want %d", len(got), len(input))
46
+ }
47
+}
48
+
49
+func TestFilterAllowOnly(t *testing.T) {
50
+ f, err := NewModelFilter([]string{"^claude"}, nil)
51
+ if err != nil {
52
+ t.Fatal(err)
53
+ }
54
+ got := f.Apply(models("claude-3-sonnet", "gpt-4", "claude-haiku", "gemini-pro"))
55
+ gotIDs := ids(got)
56
+ want := []string{"claude-3-sonnet", "claude-haiku"}
57
+ if len(gotIDs) != len(want) {
58
+ t.Fatalf("allow-only: got %v, want %v", gotIDs, want)
59
+ }
60
+ for i, id := range gotIDs {
61
+ if id != want[i] {
62
+ t.Errorf("allow-only[%d]: got %q, want %q", i, id, want[i])
63
+ }
64
+ }
65
+}
66
+
67
+func TestFilterBlockOnly(t *testing.T) {
68
+ f, err := NewModelFilter(nil, []string{"preview", "legacy"})
69
+ if err != nil {
70
+ t.Fatal(err)
71
+ }
72
+ got := f.Apply(models("gpt-4", "gpt-4-preview", "claude-3", "claude-legacy"))
73
+ gotIDs := ids(got)
74
+ want := []string{"gpt-4", "claude-3"}
75
+ if len(gotIDs) != len(want) {
76
+ t.Fatalf("block-only: got %v, want %v", gotIDs, want)
77
+ }
78
+ for i, id := range gotIDs {
79
+ if id != want[i] {
80
+ t.Errorf("block-only[%d]: got %q, want %q", i, id, want[i])
81
+ }
82
+ }
83
+}
84
+
85
+func TestFilterAllowAndBlock(t *testing.T) {
86
+ // Allow claude-*, block anything with "legacy".
87
+ f, err := NewModelFilter([]string{"^claude"}, []string{"legacy"})
88
+ if err != nil {
89
+ t.Fatal(err)
90
+ }
91
+ got := f.Apply(models("claude-3", "claude-legacy", "gpt-4", "gemini"))
92
+ gotIDs := ids(got)
93
+ // Only claude-3 survives: claude-legacy is blocked, gpt-4/gemini not in allowlist.
94
+ if len(gotIDs) != 1 || gotIDs[0] != "claude-3" {
95
+ t.Errorf("allow+block: got %v, want [claude-3]", gotIDs)
96
+ }
97
+}
98
+
99
+func TestFilterEmptyInput(t *testing.T) {
100
+ f, err := NewModelFilter([]string{"^claude"}, []string{"legacy"})
101
+ if err != nil {
102
+ t.Fatal(err)
103
+ }
104
+ got := f.Apply(nil)
105
+ if len(got) != 0 {
106
+ t.Errorf("empty input: got %d models, want 0", len(got))
107
+ }
108
+}
109
+
110
+func TestFilterBlockTakesPrecedenceOverAllow(t *testing.T) {
111
+ // Pattern matches both allow and block — block wins.
112
+ f, err := NewModelFilter([]string{"claude"}, []string{"claude-3"})
113
+ if err != nil {
114
+ t.Fatal(err)
115
+ }
116
+ got := f.Apply(models("claude-3", "claude-haiku"))
117
+ gotIDs := ids(got)
118
+ // claude-3 is blocked; claude-haiku passes allowlist.
119
+ if len(gotIDs) != 1 || gotIDs[0] != "claude-haiku" {
120
+ t.Errorf("block-over-allow: got %v, want [claude-haiku]", gotIDs)
121
+ }
122
+}
123
+
124
+func TestFilterMultipleAllowPatterns(t *testing.T) {
125
+ f, err := NewModelFilter([]string{"^claude", "^gemini"}, nil)
126
+ if err != nil {
127
+ t.Fatal(err)
128
+ }
129
+ got := f.Apply(models("claude-3", "gpt-4", "gemini-pro", "llama"))
130
+ gotIDs := ids(got)
131
+ if len(gotIDs) != 2 {
132
+ t.Fatalf("multi-allow: got %v, want [claude-3 gemini-pro]", gotIDs)
133
+ }
134
+}
--- a/internal/llm/filter_test.go
+++ b/internal/llm/filter_test.go
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/internal/llm/filter_test.go
+++ b/internal/llm/filter_test.go
@@ -0,0 +1,134 @@
1 package llm
2
3 import (
4 "testing"
5 )
6
7 func models(ids ...string) []ModelInfo {
8 out := make([]ModelInfo, len(ids))
9 for i, id := range ids {
10 out[i] = ModelInfo{ID: id, Name: id}
11 }
12 return out
13 }
14
15 func ids(ms []ModelInfo) []string {
16 out := make([]string, len(ms))
17 for i, m := range ms {
18 out[i] = m.ID
19 }
20 return out
21 }
22
23 func TestNewModelFilterInvalidAllow(t *testing.T) {
24 _, err := NewModelFilter([]string{"["}, nil)
25 if err == nil {
26 t.Fatal("expected error for invalid allow pattern, got nil")
27 }
28 }
29
30 func TestNewModelFilterInvalidBlock(t *testing.T) {
31 _, err := NewModelFilter(nil, []string{"["})
32 if err == nil {
33 t.Fatal("expected error for invalid block pattern, got nil")
34 }
35 }
36
37 func TestFilterNoPatterns(t *testing.T) {
38 f, err := NewModelFilter(nil, nil)
39 if err != nil {
40 t.Fatal(err)
41 }
42 input := models("gpt-4", "claude-3", "gemini-pro")
43 got := f.Apply(input)
44 if len(got) != len(input) {
45 t.Errorf("no patterns: got %d models, want %d", len(got), len(input))
46 }
47 }
48
49 func TestFilterAllowOnly(t *testing.T) {
50 f, err := NewModelFilter([]string{"^claude"}, nil)
51 if err != nil {
52 t.Fatal(err)
53 }
54 got := f.Apply(models("claude-3-sonnet", "gpt-4", "claude-haiku", "gemini-pro"))
55 gotIDs := ids(got)
56 want := []string{"claude-3-sonnet", "claude-haiku"}
57 if len(gotIDs) != len(want) {
58 t.Fatalf("allow-only: got %v, want %v", gotIDs, want)
59 }
60 for i, id := range gotIDs {
61 if id != want[i] {
62 t.Errorf("allow-only[%d]: got %q, want %q", i, id, want[i])
63 }
64 }
65 }
66
67 func TestFilterBlockOnly(t *testing.T) {
68 f, err := NewModelFilter(nil, []string{"preview", "legacy"})
69 if err != nil {
70 t.Fatal(err)
71 }
72 got := f.Apply(models("gpt-4", "gpt-4-preview", "claude-3", "claude-legacy"))
73 gotIDs := ids(got)
74 want := []string{"gpt-4", "claude-3"}
75 if len(gotIDs) != len(want) {
76 t.Fatalf("block-only: got %v, want %v", gotIDs, want)
77 }
78 for i, id := range gotIDs {
79 if id != want[i] {
80 t.Errorf("block-only[%d]: got %q, want %q", i, id, want[i])
81 }
82 }
83 }
84
85 func TestFilterAllowAndBlock(t *testing.T) {
86 // Allow claude-*, block anything with "legacy".
87 f, err := NewModelFilter([]string{"^claude"}, []string{"legacy"})
88 if err != nil {
89 t.Fatal(err)
90 }
91 got := f.Apply(models("claude-3", "claude-legacy", "gpt-4", "gemini"))
92 gotIDs := ids(got)
93 // Only claude-3 survives: claude-legacy is blocked, gpt-4/gemini not in allowlist.
94 if len(gotIDs) != 1 || gotIDs[0] != "claude-3" {
95 t.Errorf("allow+block: got %v, want [claude-3]", gotIDs)
96 }
97 }
98
99 func TestFilterEmptyInput(t *testing.T) {
100 f, err := NewModelFilter([]string{"^claude"}, []string{"legacy"})
101 if err != nil {
102 t.Fatal(err)
103 }
104 got := f.Apply(nil)
105 if len(got) != 0 {
106 t.Errorf("empty input: got %d models, want 0", len(got))
107 }
108 }
109
110 func TestFilterBlockTakesPrecedenceOverAllow(t *testing.T) {
111 // Pattern matches both allow and block — block wins.
112 f, err := NewModelFilter([]string{"claude"}, []string{"claude-3"})
113 if err != nil {
114 t.Fatal(err)
115 }
116 got := f.Apply(models("claude-3", "claude-haiku"))
117 gotIDs := ids(got)
118 // claude-3 is blocked; claude-haiku passes allowlist.
119 if len(gotIDs) != 1 || gotIDs[0] != "claude-haiku" {
120 t.Errorf("block-over-allow: got %v, want [claude-haiku]", gotIDs)
121 }
122 }
123
124 func TestFilterMultipleAllowPatterns(t *testing.T) {
125 f, err := NewModelFilter([]string{"^claude", "^gemini"}, nil)
126 if err != nil {
127 t.Fatal(err)
128 }
129 got := f.Apply(models("claude-3", "gpt-4", "gemini-pro", "llama"))
130 gotIDs := ids(got)
131 if len(gotIDs) != 2 {
132 t.Fatalf("multi-allow: got %v, want [claude-3 gemini-pro]", gotIDs)
133 }
134 }

Binary file

Keyboard Shortcuts

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