12 Commits

Author SHA1 Message Date
vikingowl 9814795b3c ci: migrate release pipeline from Woodpecker to GitHub Actions
Release / release (push) Has been cancelled
Drop the broken .woodpecker/release.yml (top-level when: triggered an
'error' status on every dev push instead of skipping non-tag events)
and replace with .github/workflows/release.yml driving the same
GoReleaser flow.

Rationale:
- Release artifacts already land on GitHub (releases + ghcr.io), so
  running the pipeline on GitHub eliminates a build hop.
- GH Actions auto-provides GITHUB_TOKEN with packages:write via the
  workflow permissions block — no PAT plumbing or login secrets.
- docker/setup-qemu-action and docker/setup-buildx-action handle the
  multi-arch cross-build setup that Woodpecker would require manual
  host configuration for.

Trigger: any tag matching refs/tags/v*. Mirror sync from somegit.dev
propagates tags to GitHub, so 'git push origin v0.3.1' on the canonical
remote still drives the GitHub-side release.
2026-05-24 16:45:17 +02:00
vikingowl 047924da2b ci(woodpecker): release pipeline on vX.Y.Z tag
Runs 'go test ./...' then 'goreleaser release --clean' inside the
official goreleaser image when a tag matching refs/tags/v* is pushed.
GITHUB_TOKEN comes from the 'github_token' repo secret (needs repo +
write:packages scopes) and is reused for ghcr.io docker login so the
multi-arch image build can push.

Runner requirements documented inline: docker socket access plus QEMU
registered on the host (tonistiigi/binfmt --install all) for arm64
cross-builds. Directory form chosen so a non-release CI pipeline can
land later under .woodpecker/ci.yml without restructuring.
2026-05-24 16:38:24 +02:00
vikingowl a23eb6b92c style: gofmt drift from prior commits
Pure whitespace cleanup surfaced when 'make check' ran gofmt over the
tree. Mostly struct-field column alignment in internal/safety/banner.go
(SessionInfo) and the var(...) flag block in cmd/gnoma/main.go after
--dangerously-allow-anywhere was added without realignment. Verified
zero substantive changes via 'git diff --ignore-all-space
--ignore-blank-lines'.
2026-05-24 16:33:17 +02:00
vikingowl 0981fb82d6 chore(make): add govulncheck and semgrep to 'make check'
Both checks already passed locally on the current dev tip; wiring them
into the canonical pre-commit gate so security regressions fail fast
instead of leaking into a release.

- 'make vuln' runs govulncheck with reachability analysis against the
  Go vuln DB.
- 'make sec' runs semgrep with p/golang + p/security-audit, metrics
  off, --error so findings exit non-zero.

Tools must be installed locally (commands in Makefile comments). If
upstream Woodpecker CI runs 'make check', it will need both binaries
on the runner image.
2026-05-24 16:30:54 +02:00
vikingowl 3888966e68 fix(deps): bump golang.org/x/net to v0.55.0 to clear reachable CVEs
govulncheck flagged two reachable vulnerabilities in
golang.org/x/net@v0.52.0:

- GO-2026-5026 (idna fails to reject ASCII-only Punycode labels),
  reached via router.DiscoverOllama -> http.Client.Do -> idna.ToASCII.
- GO-2026-4918 (HTTP/2 transport infinite loop on bad
  SETTINGS_MAX_FRAME_SIZE), same call path -> http2.Transport.*.

Bumping to v0.55.0 covers both. Transitive bumps to x/crypto v0.51.0,
x/sys v0.45.0, x/text v0.37.0. Post-bump govulncheck reports 0
reachable vulnerabilities and 0 in directly imported packages.
2026-05-24 16:27:28 +02:00
vikingowl 847cd5fe0c fix(security): use crypto/rand for session-ID suffix
Semgrep flagged math/rand for the /tmp artifact-directory session-ID
generation. Modern Go (1.20+) auto-seeds the global math/rand source
so this wasn't exploitable in practice, but crypto/rand is the
idiomatic choice for any security-adjacent identifier and removes the
finding from future security audits.

Drops the mrand alias entirely; reads 8 random bytes once and masks
to 24 bits to preserve the existing %06x suffix format.
2026-05-24 16:22:50 +02:00
vikingowl 001865f069 fix(env): correct ANTHROPIC_API_KEY typo, add missing vars
The placeholder ANTHROPICS_API_KEY (with trailing S) silently failed:
the auth layer reads ANTHROPIC_API_KEY, so anyone copying .env.example
to .env and pasting their key would see gnoma never pick it up, with
no clear error.

Also surfaces vars that already work but weren't templated:
GOOGLE_API_KEY (alternative to GEMINI_API_KEY), GNOMA_PROVIDER and
GNOMA_MODEL (config overrides), and the two subprocess sandbox bypass
footguns (GNOMA_AGY_BYPASS_PERMISSIONS, GNOMA_CODEX_BYPASS_SANDBOX),
left commented out so they don't accidentally turn on.
2026-05-24 16:16:39 +02:00
vikingowl c1c52f139d docs(readme): add 'no phone-home' bullet and data-flow scope note
Clarify that gnoma itself emits no telemetry to external services
while being explicit that cloud-provider arms send data to those
providers by design. Adds:
- 'No phone-home' bullet to the differentiator list, naming the
  on-device path (Ollama/llama.cpp + --incognito).
- 'Data flow' paragraph to the Security scope-note blockquote so
  the framing is consistent between the hero bullets and the
  Security section.
2026-05-24 16:00:40 +02:00
vikingowl 7040041f13 docs(readme): correct firewall scope; track egress controls in TODO
The 'What makes gnoma different' bullet and Security section both
implied a network-egress firewall. Today the Firewall only enforces a
content boundary (secret scan, Unicode sanitize, redact/block). Reword
both spots and add a Scope note. Surface the gap as a top-of-TODO
entry covering per-session audit log and per-host egress allowlist,
with the open design question (host-level vs per-tool) called out.
Raised via r/SideProject v0.3.0 launch thread.
2026-05-24 15:50:35 +02:00
vikingowl 1828151162 docs(claude): big-picture architecture and expanded test commands
Add a 'Big picture' section summarising the request flow (cmd →
session → engine → router → security/permission → extensibility) so
future Claude Code instances can orient without reading INDEX.md plus
five package directories first. Note that internal/safety and
internal/slm aren't in INDEX.md yet. Document the somegit.dev /
GitHub mirror split and the ruleset that blocks force-push and
deletion on main/dev. Expand build/test section with make check, make
test-integration, single-test, and benchmark commands.
2026-05-24 15:39:23 +02:00
vikingowl b5062d59e9 docs(readme): hero screenshot, differentiators, status, TOC
Add docs/img/gnoma-tui.png as a hero image so visitors see the TUI
above the fold instead of a wall of text. Pull the bandit router,
prefer-policy, SLM, and built-in firewall out of buried sections into
a 'What makes gnoma different' bullet list. Add a Status block flagging
pre-1.0 and a table of contents. Move the pygmy-owl naming note and
upstream/mirror URLs into a footer About section.
2026-05-24 15:39:14 +02:00
vikingowl b13a6a2801 docs(plans): mark v0.3.0 plans shipped
Three plans shipped end-to-end in v0.3.0; removing them from
TODO.md In-flight and adding a Status: shipped header to each
plan doc with the commit references.

Shipped:
- 2026-05-23-routing-defaults-refresh.md
- 2026-05-23-prefer-routing-policy.md
- 2026-05-23-startup-safety-banner.md

Still in flight (telemetry-gated, fires only if measurements
support it):
- 2026-05-23-tool-router-specialization.md
2026-05-23 22:45:05 +02:00
18 changed files with 311 additions and 96 deletions
+13 -2
View File
@@ -1,4 +1,15 @@
MISTRAL_API_KEY="asd**"
ANTHROPICS_API_KEY="sk-ant-**"
# --- LLM provider keys (set at least one) ---
ANTHROPIC_API_KEY="sk-ant-**"
OPENAI_API_KEY="sk-proj-**"
GEMINI_API_KEY="AIza**"
# Alternative to GEMINI_API_KEY (either is accepted)
# GOOGLE_API_KEY="AIza**"
MISTRAL_API_KEY="**"
# --- Optional overrides (config can also set these) ---
# GNOMA_PROVIDER="anthropic"
# GNOMA_MODEL="claude-sonnet-4-6"
# --- Subprocess sandbox bypass (footguns — set deliberately) ---
# GNOMA_AGY_BYPASS_PERMISSIONS=1
# GNOMA_CODEX_BYPASS_SANDBOX=1
+63
View File
@@ -0,0 +1,63 @@
# Release workflow — runs when a vX.Y.Z tag is pushed (including mirror
# pushes from somegit.dev). Drives GoReleaser to publish:
# - static binaries (linux/darwin/windows × amd64/arm64) + checksums
# + autogenerated changelog to the GitHub releases page
# - multi-arch container images to ghcr.io/vikingowl91/gnoma
#
# GITHUB_TOKEN is provided automatically by GitHub Actions and already
# carries packages:write thanks to the permissions block, so no PAT is
# needed for either the release upload or the ghcr.io push.
#
# Security note: this workflow does not interpolate any untrusted
# context (commit messages, PR titles, issue bodies) into shell commands.
# All ${{ ... }} references live in with: / env: blocks, which are
# safely passed as strings rather than evaluated as shell.
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
packages: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.26"
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Test
run: go test ./...
- name: GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+50 -10
View File
@@ -5,20 +5,60 @@ Provider-agnostic agentic coding assistant in Go 1.26.
Named after the northern pygmy-owl (Glaucidium gnoma).
Agents are called "elfs" (elf owl).
## Module
`somegit.dev/Owlibou/gnoma`
## Module & repo layout
- Module: `somegit.dev/Owlibou/gnoma`
- Upstream (primary, accepts PRs): <https://somegit.dev/Owlibou/gnoma>
- GitHub mirror (read-only): <https://github.com/VikingOwl91/gnoma>
PRs go to the upstream Gitea instance, not GitHub. The GitHub side is a
push mirror — direct pushes to `main`/`dev` there will be rejected by the
ruleset.
## Big picture (read this before diving in)
Single static Go binary. Request flow:
1. `cmd/gnoma` parses flags, picks TUI vs pipe mode, builds the session.
2. `internal/session` owns one chat lifecycle; `internal/engine` runs the
agentic loop (stream → tool calls → re-query → until done).
3. `internal/router` picks the arm per prompt: multi-armed bandit over
provider adapters in `internal/provider/{anthropic,openai,google,mistral,openaicompat}`,
tiered SLM (`internal/slm`) → CLI-agent subprocess → local → cloud,
with `Strengths` + `MaxComplexity` + `CostWeight` shaping selection.
4. `internal/security` is the safety boundary: SafeProvider wrapping,
firewall (network egress), secret scanner, redaction, incognito mode.
`internal/safety` is separate — it's the pre-launch CWD classifier.
5. `internal/tool` is the local-action boundary; `internal/permission`
gates every tool call.
6. Extensibility surfaces: `internal/hook`, `internal/skill`,
`internal/mcp` (JSON-RPC over stdio), `internal/plugin` (TOFU-pinned).
Discriminated unions (struct + type discriminant) are the project's
chosen way to model variants — see `internal/message` and
`internal/stream`. Don't reach for interfaces when a discriminant fits.
Full essentials (vision, domain model, ADRs, process flows):
`docs/essentials/INDEX.md`. **Read INDEX.md before changing
architectural boundaries or adding new packages.** Note: INDEX
predates `internal/safety` and `internal/slm` — cross-check the actual
tree.
## Build & Test
```sh
make build # build binary to ./bin/gnoma
make test # run all tests
make lint # run golangci-lint
make cover # test with coverage report
```
make build # ./bin/gnoma
make test # unit tests
make test-integration # //go:build integration — needs real API keys
make lint # golangci-lint run ./...
make check # fmt + vet + lint + test — canonical pre-commit gate
make cover # coverage.html
## Project Essentials
Project architecture, domain model, and design decisions: `docs/essentials/INDEX.md`
Read INDEX.md before making architectural changes or adding new system boundaries.
# Run a single test / package
go test -run TestRouterSelect ./internal/router/
go test -v ./internal/router/
# Benchmarks
go test -bench=. ./internal/router/
```
## Conventions
+12 -2
View File
@@ -1,4 +1,4 @@
.PHONY: build run check install test lint cover clean fmt vet
.PHONY: build run check install test lint cover clean fmt vet vuln sec
BINARY := gnoma
BINDIR := ./bin
@@ -10,7 +10,7 @@ build:
run: build
$(BINDIR)/$(BINARY)
check: fmt vet lint test
check: fmt vet lint test vuln sec
@echo "All checks passed!"
install:
@@ -43,3 +43,13 @@ clean:
tidy:
go mod tidy
# Reachability-checked dependency vuln scan against the Go vuln DB.
# Install: go install golang.org/x/vuln/cmd/govulncheck@latest
vuln:
govulncheck ./...
# Static security analysis via Semgrep (Go ruleset + security-audit).
# Install: pip install semgrep (or: brew install semgrep)
sec:
semgrep --config=p/golang --config=p/security-audit --metrics=off --error .
+86 -7
View File
@@ -10,11 +10,65 @@ to the best available model — cloud or local — through a multi-armed bandit
router, executes tools on your behalf, and stays extensible through hooks,
skills, MCP servers, and plugins.
Named after the northern pygmy-owl (*Glaucidium gnoma*); agents are called
**elfs** (elf owl).
![gnoma TUI showing a routed turn](docs/img/gnoma-tui.png)
- **Upstream:** <https://somegit.dev/Owlibou/gnoma>
- **GitHub mirror:** <https://github.com/VikingOwl91/gnoma>
*Every turn shows which arm the router picked and why — here a local
`qwen3:14b` was selected for a `generation` task.*
## What makes gnoma different
- **Multi-armed bandit router.** Per-prompt arm selection based on
capability gates, declared `Strengths`, latency, and cost. Visible in
the TUI on every turn — no black box.
- **`[router].prefer = local | cloud | auto`.** Pin routing toward local
models, cloud, or let the bandit decide. Offline-first workflows still
reach for Claude when the local model would obviously flail.
- **Tier-0 SLM routing.** A tiny local model classifies each prompt and
handles trivial tasks itself, keeping the heavy provider for real work.
- **Content boundary + secret scanner.** Every outgoing LLM message
and incoming tool result is scanned for secrets (regex + Shannon
entropy on long tokens), redacted or blocked at the content level.
Paths are canonicalised (TOCTOU-safe), Unicode is sanitized
(homoglyphs, BiDi tricks), and a `SafeProvider` boundary keeps
incognito-mode data out of long-lived stores. *(Per-host network
egress allowlist is on the roadmap, not in place today.)*
- **No phone-home.** gnoma itself sends nothing off-machine — zero
analytics endpoint, zero metrics service, no remote logging.
Prompts of course go to whatever provider you route them to:
cloud arms ship data to that provider by design; pair
Ollama/llama.cpp with `--incognito` if you want everything
on-device.
- **Provider-agnostic from day one.** Anthropic, OpenAI, Google, Mistral,
Ollama, llama.cpp, plus subprocess CLIs (`claude`, `codex`, `agy`,
`vibe`). Mix cloud and local in the same session.
- **Vision end-to-end.** `[Image: /path]` markers in prompts, `Ctrl+V`
paste in the TUI, capability-gated per arm.
- **Single static binary.** `CGO_ENABLED=0`, multi-arch container on
ghcr.io. No daemon, no runtime deps.
## Status
Pre-1.0 (current: **v0.3.0**). Single maintainer, breaking changes
possible. The provider, router, and engine surfaces are settling;
config schema and TUI bindings may still shift between minor versions.
Apache 2.0.
## Table of contents
- [Install](#install)
- [Quickstart](#quickstart)
- [Vision / image input](#vision--image-input)
- [Providers](#providers)
- [Config](#config)
- [Routing defaults](#routing-defaults)
- [SLM routing](#slm-small-language-model-routing)
- [Session persistence](#session-persistence)
- [Extensibility](#extensibility)
- [Subcommands](#subcommands)
- [Security](#security)
- [Development](#development)
- [About](#about)
- [License](#license)
---
@@ -418,9 +472,25 @@ built-in batching skill.
gnoma runs tools and shell commands on your behalf. The
[`internal/security`](internal/security) package canonicalises every path
(TOCTOU-safe), gates network access through a configurable firewall, and
scans tool output for secrets before it ever reaches the model. The
`SafeProvider` boundary keeps incognito-mode data out of long-lived stores.
(TOCTOU-safe), scans every outgoing LLM message and incoming tool result
for secrets (regex + Shannon entropy) before it reaches the model, and
sanitizes Unicode (homoglyphs, BiDi tricks). The `SafeProvider` boundary
keeps incognito-mode data out of long-lived stores.
> **Scope note.** The current "firewall" is a content boundary — it
> redacts/blocks secrets in inputs and outputs. It is **not** a
> network-egress firewall: outgoing HTTP from tools and providers goes
> through stock `http.Client`, with no per-host allowlist or
> dial-layer enforcement. Per-host egress rules and a per-session
> audit log of blocked/redacted events are tracked in
> [TODO.md](TODO.md).
>
> **Data flow.** gnoma itself emits no telemetry to external services
> — no analytics, no metrics endpoint, no remote logging. When you
> route to a cloud provider (Anthropic, OpenAI, Google, Mistral),
> prompts and tool data are sent to that provider as required to
> fulfill the request — by design. For fully on-device operation,
> use Ollama or llama.cpp and `--incognito`.
### Entropy false-positive reduction
@@ -498,6 +568,15 @@ Architecture, conventions, and TDD workflow: [CONTRIBUTING.md](CONTRIBUTING.md).
---
## About
Named after the northern pygmy-owl (*Glaucidium gnoma*); agents are called
**elfs** (elf owl).
- **Upstream:** <https://somegit.dev/Owlibou/gnoma>
- **GitHub mirror:** <https://github.com/VikingOwl91/gnoma> (read-only;
PRs go to upstream Gitea)
## License
Apache License 2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
+21 -29
View File
@@ -4,35 +4,27 @@ Active work, newest first.
## In flight
- **Startup safety + context banner** — refuse / warn / OK tier check
on the cwd at launch (refuse in `/etc`, `/sys`, system roots; warn
with keypress in `$HOME`, `/tmp`, common dumping grounds; OK in
anything inside a git repo or with a project marker). Context
banner always shown with cwd, git state, model, modes, and a
top-level sensitive-file inventory. Bypass via
`--dangerously-allow-anywhere`. Complements the in-flight
sensitive-content unified-policy work (this is the pre-flight
layer; that is the runtime layer). See
[`docs/superpowers/plans/2026-05-23-startup-safety-banner.md`](docs/superpowers/plans/2026-05-23-startup-safety-banner.md).
- **Routing-preference policy** — `[router].prefer = "local" | "cloud" | "auto"`
config knob biasing selection via a soft score multiplier
(0.3 / 0.5 / 1.0). Preserves Strengths cross-tier promotion and
the bandit's learning; complements rather than replaces incognito.
Forced arms (`--provider X`) and incognito still take priority.
Closes the original 2026-05-23 session item B (deferred when the
defaults-refresh work landed first). See
[`docs/superpowers/plans/2026-05-23-prefer-routing-policy.md`](docs/superpowers/plans/2026-05-23-prefer-routing-policy.md).
- **Routing defaults refresh** — bake family-keyed `Strengths` +
`MaxComplexity` into discovery so a freshly-pulled local fleet
routes sensibly without any TOML config. Adds a non-chat exclude
list (filters `embeddinggemma`, `kokoros`, `whisper-base`,
`vibevoice`, `*-asr/-tts/-audio/-reranker`), extends
`knownVisionModelPrefixes` (gemma4, glm-ocr), and refreshes the
cloud-side registry (Gemini 3.x, `gpt-5.3-codex`). Closed-model
`Strengths` + `CostWeight` defaults land in the provider modules.
Driven by benchmark snapshot 2026-05-23
(artificialanalysis.ai v4.0, llm-stats.com). See
[`docs/superpowers/plans/2026-05-23-routing-defaults-refresh.md`](docs/superpowers/plans/2026-05-23-routing-defaults-refresh.md).
- **Security boundary — egress controls + session audit log.** The
current `Firewall` is a content boundary only (scans messages and
tool results for secrets via regex + Shannon entropy, redacts or
blocks, logs via `log/slog`). It does not enforce network egress —
outgoing HTTP from tools and providers uses stock `http.Client`
with no per-host allowlist or dial-layer interception. Two follow-
ups surfaced from the r/SideProject v0.3.0 launch thread
(2026-05-24, `u/Secret_Theme3192`):
1. **Per-session audit log of blocked/redacted events**
grep-able file at `.gnoma/sessions/<id>/audit.jsonl` so the
user can answer "what did the firewall do this session?" in
one command. Today the `slog` output goes to whatever sink is
configured, with no per-session grouping.
2. **Per-host egress allowlist (HTTP transport layer)** — open
design question: host-level (`allow api.openai.com, deny *`)
vs per-tool (`bash can only hit these hosts`). Reply asked
the commenter for their mental model; revisit when feedback
lands. The README and v0.3.0 Reddit post phrasing oversold
"network egress gated"; corrected in the same commit as this
TODO entry.
- **Tool-router specialization (functiongemma)** — gated on telemetry,
not committed. Phase A.2 adds did-switch-rate measurement to the
two-stage `select_category` path; Phase A.3 (LoRA fine-tune of
+18 -13
View File
@@ -2,13 +2,14 @@ package main
import (
"context"
"crypto/rand"
"encoding/binary"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log/slog"
mrand "math/rand"
"os"
"os/signal"
"path/filepath"
@@ -61,17 +62,17 @@ var (
func main() {
var resumeFlag string
var (
providerName = flag.String("provider", "", "LLM provider (mistral, anthropic, openai, google, ollama, llamacpp)")
model = flag.String("model", "", "model name (empty = provider default)")
system = flag.String("system", "", "system prompt override (empty = built-in default)")
apiKey = flag.String("api-key", "", "API key (or set MISTRAL_API_KEY env)")
maxTurns = flag.Int("max-turns", 50, "max tool-calling rounds per turn")
permMode = flag.String("permission", "auto", "permission mode (default, accept_edits, bypass, deny, plan, auto)")
incognito = flag.Bool("incognito", false, "incognito mode — no persistence, no learning")
profileFlag = flag.String("profile", "", "config profile to load (empty = default_profile from base config)")
providerName = flag.String("provider", "", "LLM provider (mistral, anthropic, openai, google, ollama, llamacpp)")
model = flag.String("model", "", "model name (empty = provider default)")
system = flag.String("system", "", "system prompt override (empty = built-in default)")
apiKey = flag.String("api-key", "", "API key (or set MISTRAL_API_KEY env)")
maxTurns = flag.Int("max-turns", 50, "max tool-calling rounds per turn")
permMode = flag.String("permission", "auto", "permission mode (default, accept_edits, bypass, deny, plan, auto)")
incognito = flag.Bool("incognito", false, "incognito mode — no persistence, no learning")
profileFlag = flag.String("profile", "", "config profile to load (empty = default_profile from base config)")
allowAnywhere = flag.Bool("dangerously-allow-anywhere", false, "bypass the cwd safety classifier — only use if you know what you're doing")
verbose = flag.Bool("verbose", false, "enable debug logging")
version = flag.Bool("version", false, "print version and exit")
verbose = flag.Bool("verbose", false, "enable debug logging")
version = flag.Bool("version", false, "print version and exit")
)
flag.StringVar(&resumeFlag, "resume", "", "resume session by ID (omit ID to list sessions)")
flag.StringVar(&resumeFlag, "r", "", "resume session (shorthand)")
@@ -656,10 +657,14 @@ func main() {
}
permChecker := permission.NewChecker(permission.Mode(*permMode), permRules, pipePromptFn)
// Generate session-scoped ID for /tmp artifact directory
// Generate session-scoped ID for /tmp artifact directory.
// Use crypto/rand so the suffix isn't predictable even if a future
// caller seeds math/rand deterministically (e.g., in tests).
var randBuf [8]byte
_, _ = rand.Read(randBuf[:])
sessionID := fmt.Sprintf("%s-%06x",
time.Now().Format("20060102-150405"),
mrand.Int63()&0xffffff,
binary.BigEndian.Uint64(randBuf[:])&0xffffff,
)
// Pass the firewall's incognito mode so Save no-ops while incognito
// is active. Mode is consulted on every Save (dynamic), so TUI
Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

@@ -1,5 +1,10 @@
# Routing-Preference Policy — 2026-05-23
> **Status: shipped in v0.3.0.** Commit `f9094f6`. Implementation
> diverged from the original plan (tier-shift instead of pure score
> multiplier) — see "Implementation note" in the Approach section.
> All P-1 through P-7 tasks complete.
Adds a config knob that biases routing toward local arms, toward
cloud arms, or leaves the current tier+score behavior unchanged.
Originally surfaced as item B in the 2026-05-23 routing redesign
@@ -1,5 +1,10 @@
# Routing Defaults Refresh — 2026-05-23
> **Status: shipped in v0.3.0.** Commits `a79e991` (scaffold) →
> `9bb775a` (full local family table) → `2f8d4c4` (cloud defaults
> + gpt-5.3-codex) → `c99b2c6` (README). All R-1 through R-8
> tasks complete.
Refreshes gnoma's per-arm routing defaults so that out-of-the-box
selection produces sensible choices without requiring users to write
a `[[arms]]` block in TOML. Surfaced during the 2026-05-23 session
@@ -1,5 +1,11 @@
# Startup Safety + Context Banner — 2026-05-23
> **Status: shipped in v0.3.0.** Commits `3eeb5b4` (classifier +
> banner + main.go wiring) → `8ba77c1` (env-template precision
> fix, label alignment, banner-under-bypass). All S-1 through
> S-7 tasks complete; S-8 docs done in `d206b3c`. Windows path
> handling still deferred per plan.
Adds a pre-launch safety check that warns or refuses when gnoma is
started in a directory where it could do real damage (`$HOME`,
`/`, `/etc`, etc.), plus a context banner shown on every launch
+4 -4
View File
@@ -15,7 +15,7 @@ require (
github.com/charmbracelet/x/ansi v0.11.6
github.com/openai/openai-go v1.12.0
github.com/pkoukk/tiktoken-go v0.1.8
golang.org/x/text v0.35.0
golang.org/x/text v0.37.0
google.golang.org/genai v1.52.1
gopkg.in/yaml.v3 v3.0.1
mvdan.cc/sh/v3 v3.13.0
@@ -63,10 +63,10 @@ require (
go.opentelemetry.io/otel v1.42.0 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/otel/trace v1.42.0 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/sys v0.45.0 // indirect
google.golang.org/api v0.267.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/grpc v1.79.3 // indirect
+8 -8
View File
@@ -142,18 +142,18 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=
+1 -1
View File
@@ -38,7 +38,7 @@ func TestTryLoadOAuthCredentials_Formats(t *testing.T) {
name: "camelCase and milliseconds expiry",
data: oauthCreds{
AccessToken2: "token-camel",
ExpiresAt: time.Now().Add(1 * time.Hour).UnixNano() / 1e6,
ExpiresAt: time.Now().Add(1*time.Hour).UnixNano() / 1e6,
TokenType2: "Bearer",
},
expectError: false,
+4 -5
View File
@@ -338,10 +338,10 @@ func TestRoutingDefaults_PayoffScenario(t *testing.T) {
}
cases := []struct {
name string
task Task
wantArmID ArmID
reason string
name string
task Task
wantArmID ArmID
reason string
}{
{
name: "Generation picks qwen3-coder",
@@ -472,4 +472,3 @@ func TestRoutingDefaults_LocalFleetVisibility(t *testing.T) {
}
}
}
+4 -4
View File
@@ -54,10 +54,10 @@ func TestPolicyMultiplier(t *testing.T) {
cloudArm := &Arm{IsLocal: false}
cases := []struct {
name string
arm *Arm
policy PreferPolicy
want float64
name string
arm *Arm
policy PreferPolicy
want float64
}{
{"auto/local", localArm, PreferAuto, 1.0},
{"auto/cloud", cloudArm, PreferAuto, 1.0},
+10 -10
View File
@@ -10,16 +10,16 @@ import (
// Caller passes whatever is known at launch time; empty fields are
// omitted from the rendered banner.
type SessionInfo struct {
Version string // e.g. "0.2.1"
GitBranch string // empty if not in a git repo
GitDirty bool // true if working tree has uncommitted changes
ProjectType string // free-form, e.g. "Go module (somegit.dev/...)"
Provider string // e.g. "ollama"
Model string // e.g. "qwen3-coder:30b"
Permission string // e.g. "auto", "accept_edits"
Incognito bool
Prefer string // "auto" / "local" / "cloud"
Tenant string // optional, e.g. Kubernetes context name
Version string // e.g. "0.2.1"
GitBranch string // empty if not in a git repo
GitDirty bool // true if working tree has uncommitted changes
ProjectType string // free-form, e.g. "Go module (somegit.dev/...)"
Provider string // e.g. "ollama"
Model string // e.g. "qwen3-coder:30b"
Permission string // e.g. "auto", "accept_edits"
Incognito bool
Prefer string // "auto" / "local" / "cloud"
Tenant string // optional, e.g. Kubernetes context name
}
// RenderContextBanner returns the always-shown banner with cwd, git,
+1 -1
View File
@@ -21,7 +21,7 @@ func TestScanCWDForSensitive_Matches(t *testing.T) {
}
// Non-sensitive control files.
control := []string{
".envrc", // direnv config, not a credential
".envrc", // direnv config, not a credential
"main.go",
"README.md",
"secret_handler.go", // source code, not data