From 25996d13343867102fcc159afa2f3d2c79f44133 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 14 Apr 2026 23:34:21 -0400 Subject: [PATCH 1/2] docs(agents): require PR closing keywords for board automation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document that the project board now has Item closed, Pull request merged, and Auto-close issue workflows enabled, and that PR bodies must include Closes/Fixes/Resolves keywords for the linkage to fire — branch names alone are not enough. --- AGENTS.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 0b10c95..679705f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 #` (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: From 3436a1d65b14c9295145a5a91fff8bcb7a762e36 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Wed, 15 Apr 2026 00:19:27 -0400 Subject: [PATCH 2/2] fix: expose Hermes native tool surface --- internal/driver/hermes/config.go | 23 +++++++ internal/driver/hermes/config_test.go | 88 +++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/internal/driver/hermes/config.go b/internal/driver/hermes/config.go index d882beb..2203a34 100644 --- a/internal/driver/hermes/config.go +++ b/internal/driver/hermes/config.go @@ -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") @@ -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) @@ -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", diff --git a/internal/driver/hermes/config_test.go b/internal/driver/hermes/config_test.go index 7c12875..61386b0 100644 --- a/internal/driver/hermes/config_test.go +++ b/internal/driver/hermes/config_test.go @@ -1,6 +1,7 @@ package hermes import ( + "strings" "testing" "github.com/mostlydev/clawdapus/internal/driver" @@ -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 +}