Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ All planned work is tracked as GitHub issues and prioritized on the [project boa

**The project board is the single source of truth for priorities.** Column order within a status column reflects relative priority. Work the top item unless there is a blocking reason not to.

**Closing keywords in PR bodies are required.** The board has automation enabled for *Item closed*, *Pull request merged*, and *Auto-close issue*. These workflows only fire when GitHub can link a PR to its issue, which requires a closing keyword in the PR body:

```
Closes #137
Fixes #137, #146
Resolves #137
```

Branch names like `issue-137-*` are a human convention — GitHub does **not** infer the linkage from them. If the PR body omits a closing keyword, the issue stays Open and the card stays in Ready after merge, requiring manual cleanup. Always include `Closes #<n>` (one per covered issue) when opening a PR.

## Compilation Principles

`claw up` is a compiler. These principles govern the pipeline and must not be violated by new features:
Expand Down
23 changes: 23 additions & 0 deletions internal/driver/hermes/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ func GenerateConfig(rc *driver.ResolvedClaw, modelCfg *modelConfig) ([]byte, err
"timeout": 180,
},
}
if platformToolsets := platformToolsetsForHandles(rc.Handles); len(platformToolsets) > 0 {
config["platform_toolsets"] = platformToolsets
}

for _, cmd := range rc.Configures {
path, value, err := shared.ParseConfigSetCommand(cmd, "hermes")
Expand All @@ -59,6 +62,24 @@ func GenerateConfig(rc *driver.ResolvedClaw, modelCfg *modelConfig) ([]byte, err
return data, nil
}

func platformToolsetsForHandles(handles map[string]*driver.HandleInfo) map[string]any {
presets := map[string]string{
"discord": "hermes-discord",
"slack": "hermes-slack",
"telegram": "hermes-telegram",
}
toolsets := make(map[string]any)
for rawPlatform := range handles {
platform := strings.ToLower(strings.TrimSpace(rawPlatform))
preset, ok := presets[platform]
if !ok {
continue
}
toolsets[platform] = []string{preset}
}
return toolsets
}

func GenerateEnvFile(rc *driver.ResolvedClaw, modelCfg *modelConfig) ([]byte, error) {

env := make(map[string]string)
Expand Down Expand Up @@ -274,6 +295,8 @@ func allowedEnvPassthroughKeys() []string {
"DISCORD_HOME_CHANNEL",
"DISCORD_HOME_CHANNEL_NAME",
"DISCORD_REQUIRE_MENTION",
"FIRECRAWL_API_KEY",
"FIRECRAWL_API_URL",
"GATEWAY_ALLOWED_USERS",
"GATEWAY_ALLOW_ALL_USERS",
"HERMES_TOOL_PROGRESS_MODE",
Expand Down
88 changes: 88 additions & 0 deletions internal/driver/hermes/config_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package hermes

import (
"strings"
"testing"

"github.com/mostlydev/clawdapus/internal/driver"
Expand Down Expand Up @@ -170,3 +171,90 @@ func TestGenerateConfigIncludesCllamaRouting(t *testing.T) {
t.Fatalf("expected custom provider, got %#v", got)
}
}

func TestGenerateConfigIncludesPlatformToolsetsForActiveHandles(t *testing.T) {
rc := &driver.ResolvedClaw{
Models: map[string]string{"primary": "openrouter/anthropic/claude-sonnet-4"},
Handles: map[string]*driver.HandleInfo{
"discord": {},
"slack": {},
},
Environment: map[string]string{
"DISCORD_BOT_TOKEN": "discord-token",
"SLACK_BOT_TOKEN": "slack-bot",
"SLACK_APP_TOKEN": "slack-app",
"OPENROUTER_API_KEY": "or-key",
},
}

mc, err := resolveModelConfig(rc)
if err != nil {
t.Fatalf("resolveModelConfig returned error: %v", err)
}
data, err := GenerateConfig(rc, mc)
if err != nil {
t.Fatalf("GenerateConfig returned error: %v", err)
}

var cfg map[string]any
if err := yaml.Unmarshal(data, &cfg); err != nil {
t.Fatalf("parse generated yaml: %v", err)
}

toolsets, _ := cfg["platform_toolsets"].(map[string]any)
if toolsets == nil {
t.Fatal("expected platform_toolsets in generated config")
}
if got := toolsets["discord"]; !equalToolsetList(got, "hermes-discord") {
t.Fatalf("unexpected discord platform_toolsets entry: %#v", got)
}
if got := toolsets["slack"]; !equalToolsetList(got, "hermes-slack") {
t.Fatalf("unexpected slack platform_toolsets entry: %#v", got)
}
if _, exists := toolsets["telegram"]; exists {
t.Fatalf("did not expect telegram platform_toolsets entry, got %#v", toolsets["telegram"])
}
}

func TestGenerateEnvFileIncludesFirecrawlVars(t *testing.T) {
rc := &driver.ResolvedClaw{
Models: map[string]string{"primary": "openrouter/anthropic/claude-sonnet-4"},
Handles: map[string]*driver.HandleInfo{
"discord": {},
},
Environment: map[string]string{
"DISCORD_BOT_TOKEN": "discord-token",
"FIRECRAWL_API_KEY": "fc-key",
"FIRECRAWL_API_URL": "https://firecrawl.internal",
"OPENROUTER_API_KEY": "or-key",
},
}

mc, err := resolveModelConfig(rc)
if err != nil {
t.Fatalf("resolveModelConfig returned error: %v", err)
}
data, err := GenerateEnvFile(rc, mc)
if err != nil {
t.Fatalf("GenerateEnvFile returned error: %v", err)
}

env := string(data)
for _, want := range []string{
"FIRECRAWL_API_KEY=fc-key\n",
"FIRECRAWL_API_URL=https://firecrawl.internal\n",
} {
if !strings.Contains(env, want) {
t.Fatalf("expected env output to contain %q, got:\n%s", want, env)
}
}
}

func equalToolsetList(got any, want string) bool {
raw, ok := got.([]any)
if !ok || len(raw) != 1 {
return false
}
value, ok := raw[0].(string)
return ok && value == want
}