vikingowl 0aabd19906 feat(router): per-arm strengths + cost weight (Phase D)
Plan D from docs/superpowers/plans/2026-05-19-post-slm-unlock.md
(static portion; dynamic bandit-driven promotion deferred to D-2).

Routing previously let tier ordering (CLI > local > API) dominate
selection — Opus, in tier 3, would lose to a tier-1 CLI agent for
SecurityReview even though Opus is empirically stronger at that task.
This change introduces explicit per-arm overrides:

  [[arms]]
  id = "anthropic/claude-opus-4-7"
  strengths = ["security_review", "planning"]
  cost_weight = 0.3

Strengths gate cross-tier promotion: arms matching task.Type bypass
the tier loop and compete with each other directly. Promotion is a
preference, not a pin — if no strength-tagged arm is feasible
(backoff, pool capacity, tool support), selection falls through to
the default tier order.

CostWeight linearly dampens the cost penalty in scoreArm via
  effectiveCost = 1 + CostWeight * (cost - 1)
CostWeight=1.0 (or unset) preserves current behavior; lower values
trade cheapness for quality. The earlier draft used cost^CostWeight
which inverts direction for sub-1 local-arm costs (raising a
fraction <1 to a fractional power makes it bigger, not smaller); a
monotonicity regression test prevents that drift.

- internal/router/arm.go: Strengths []TaskType, CostWeight float64,
  HasStrength(), ResolvedCostWeight() (zero → 1.0).
- internal/router/selector.go: scoreArm strength bonus const
  (strengthScoreBonus = 0.15) + linear cost dampening; selectBest
  cross-tier promotion before tier loop.
- internal/router/router.go: ArmOverride type + ApplyArmOverrides()
  returns unknown IDs; unknown strength names skipped with per-name
  warning via slog.
- internal/router/task.go: ParseTaskTypeStrict() returns ok bool;
  ParseTaskType now delegates so the two switches stay in sync.
- internal/config/config.go: ArmConfig + [[arms]] TOML wiring.
- cmd/gnoma/main.go: applies overrides after all initial arms
  register; logs a warning when an [[arms]] id has no matching
  registered arm.

Tests cover: predicate helpers, scoring direction across two arms,
linear-formula monotonicity on both sides of cost=1, cross-tier
promotion, empty-Strengths preserves tier order, promoted arm in
backoff falls through via full Router.Select path, observed-quality
tiebreak between two strength-tagged arms, ApplyArmOverrides happy
path + unknown-ID reporting + unknown-strength skipping.
2026-05-19 21:14:45 +02:00

gnoma

A provider-agnostic agentic coding assistant built in Go. gnoma routes tasks to the best available LLM — cloud or local — through a multi-armed bandit router, while tools, hooks, skills, MCP servers, and plugins keep it extensible. Named after the northern pygmy-owl (Glaucidium gnoma); agents are called elfs (elf owl).

Quickstart

# Install
go install somegit.dev/Owlibou/gnoma/cmd/gnoma@latest

# Or build from source
git clone https://somegit.dev/Owlibou/gnoma && cd gnoma
make build    # binary at ./bin/gnoma

# Set at least one provider key
export ANTHROPIC_API_KEY=sk-ant-...   # or OPENAI_API_KEY, MISTRAL_API_KEY, GEMINI_API_KEY

# Run
gnoma                                 # interactive TUI
echo "list files" | gnoma             # pipe mode
gnoma --provider ollama               # use a local model

Build

make build          # ./bin/gnoma
make install        # $GOPATH/bin/gnoma

Providers

Anthropic

export ANTHROPIC_API_KEY=sk-ant-...
./bin/gnoma --provider anthropic
./bin/gnoma --provider anthropic --model claude-opus-4-5-20251001

Integration tests hit the real API — keep a key in env:

go test -tags integration ./internal/provider/...

OpenAI

export OPENAI_API_KEY=sk-proj-...
./bin/gnoma --provider openai
./bin/gnoma --provider openai --model gpt-4o

Mistral

export MISTRAL_API_KEY=...
./bin/gnoma --provider mistral

Google (Gemini)

export GEMINI_API_KEY=AIza...
./bin/gnoma --provider google
./bin/gnoma --provider google --model gemini-2.0-flash

Ollama (local)

Start Ollama and pull a model, then:

./bin/gnoma --provider ollama --model gemma4:latest
./bin/gnoma --provider ollama --model qwen3:8b     # default if --model omitted

Default endpoint: http://localhost:11434/v1. Override via config or env:

# .gnoma/config.toml
[provider]
default = "ollama"
model   = "gemma4:latest"

[provider.endpoints]
ollama = "http://myhost:11434/v1"

llama.cpp (local)

Start the llama.cpp server:

llama-server --model /path/to/model.gguf --port 8080 --ctx-size 8192

Then:

./bin/gnoma --provider llamacpp
# model name is taken from the server's /v1/models response

Default endpoint: http://localhost:8080/v1. Override:

[provider.endpoints]
llamacpp = "http://localhost:9090/v1"

Extensibility (M8)

gnoma supports hooks, skills, MCP servers, and plugins.

MCP Servers

Connect any MCP-compatible tool server:

[[mcp_servers]]
name    = "git"
command = "mcp-server-git"
args    = ["--repo", "."]
timeout = "30s"

# Replace a built-in tool with an MCP tool
[mcp_servers.replace_default]
exec = "bash"   # MCP tool "exec" replaces gnoma's built-in "bash"

MCP tools appear as mcp__{server}__{tool} (e.g., mcp__git__status), or under the built-in name when using replace_default.

Skills

Drop markdown files into .gnoma/skills/ or ~/.config/gnoma/skills/:

/skillname          # invoke a skill
/skills             # list available skills

Hooks

Run shell commands on tool events:

[[hooks]]
name         = "block-rm-rf"
event        = "pre_tool_use"
type         = "command"
exec         = "bash-safety-check.sh"
tool_pattern = "bash*"

Plugins

Bundle skills, hooks, and MCP configs into installable plugins:

gnoma plugin install ./my-plugin    # install from directory
gnoma plugin list                   # list installed plugins

Plugins are pinned by SHA-256 of their plugin.json on first load (Trust-On-First-Use). A manifest that changes between runs is refused with a clear error and a re-enrollment hint. See docs/plugins-trust.md and ADR-003.


Session Persistence

Conversations are auto-saved to .gnoma/sessions/ after each completed turn. On a crash you lose at most the current in-flight turn; all previously completed turns are safe.

Resume a session

gnoma --resume              # interactive session picker (↑↓ navigate, Enter load, Esc cancel)
gnoma --resume <id>         # restore directly by ID
gnoma -r                    # shorthand

Inside the TUI:

/resume                     # open picker
/resume <id>                # restore by ID

Incognito mode

gnoma --incognito           # no session saved, no quality scores updated

Toggle at runtime with Ctrl+X.

Config

[session]
max_keep = 20   # how many sessions to retain per project (default: 20)

Sessions are stored per-project under .gnoma/sessions/<id>/. Quality scores (EMA routing data) are stored globally at ~/.config/gnoma/quality.json.


Config

Config is read in priority order:

  1. ~/.config/gnoma/config.toml — global
  2. .gnoma/config.toml — project-local (next to go.mod / .git)
  3. Environment variables

Example .gnoma/config.toml:

[provider]
default = "anthropic"
model   = "claude-sonnet-4-6"

[provider.api_keys]
anthropic = "${ANTHROPIC_API_KEY}"

[provider.endpoints]
ollama   = "http://localhost:11434/v1"
llamacpp = "http://localhost:8080/v1"

[permission]
mode = "auto"   # auto | accept_edits | bypass | deny | plan

Environment variable overrides: GNOMA_PROVIDER, GNOMA_MODEL.


Testing

make test               # unit tests
make test-integration   # integration tests (require real API keys)
make cover              # coverage report → coverage.html
make lint               # golangci-lint
make check              # fmt + vet + lint + test

Integration tests are gated behind //go:build integration and skipped by default.

S
Description
No description provided
Readme Apache-2.0 1.3 MiB
Languages
Go 99.9%