Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06d4069076 | |||
| f641bd4971 | |||
| 798f2ab3c3 | |||
| 9814795b3c | |||
| 047924da2b | |||
| a23eb6b92c | |||
| 0981fb82d6 | |||
| 3888966e68 | |||
| 847cd5fe0c | |||
| 001865f069 | |||
| c1c52f139d | |||
| 7040041f13 | |||
| 1828151162 | |||
| b5062d59e9 | |||
| b13a6a2801 |
+13
-2
@@ -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
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# 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 }}
|
||||
# Force GoReleaser to use the triggering tag rather than fall
|
||||
# back to `git describe` — which can resolve to an older tag
|
||||
# (e.g., a vX.Y.Z-rc tag) when multiple tags point at the same
|
||||
# commit. Surfaced as the v0.3.1 release failure on 2026-05-24.
|
||||
GORELEASER_CURRENT_TAG: ${{ github.ref_name }}
|
||||
+9
-3
@@ -37,9 +37,12 @@ changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
- "^chore:"
|
||||
# Match both bare and scoped conventional commits, e.g. both
|
||||
# "docs:" and "docs(readme):" should be excluded.
|
||||
- "^docs[:(]"
|
||||
- "^test[:(]"
|
||||
- "^chore[:(]"
|
||||
- "^style[:(]"
|
||||
|
||||
# Multi-arch Docker images published to GitHub Container Registry.
|
||||
# Build host needs Docker buildx and a `docker login ghcr.io` for the
|
||||
@@ -98,3 +101,6 @@ release:
|
||||
github:
|
||||
owner: VikingOwl91
|
||||
name: gnoma
|
||||
# Auto-detect prereleases from semver: tags with -rc, -beta, -alpha,
|
||||
# -pre, etc. suffix get marked as prerelease on GitHub.
|
||||
prerelease: auto
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 .
|
||||
|
||||
@@ -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).
|
||||

|
||||
|
||||
- **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).
|
||||
|
||||
@@ -4,35 +4,58 @@ 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).
|
||||
- **Bandit selector — design decisions deferred.** The current
|
||||
selector (`internal/router/selector.go:scoreArm`) is greedy
|
||||
quality-weighted: per-(arm × task-type) EMA scores blended 70/30
|
||||
with heuristic defaults, divided by CostWeight-adjusted cost. It
|
||||
is **not** a true multi-armed bandit — no UCB-style exploration
|
||||
bonus, no Thompson sampling. Tracked as a design question rather
|
||||
than a must-implement item because of two open dependencies:
|
||||
|
||||
1. **Whether to keep numeric EMA at all.** The 2026-05-07 roadmap
|
||||
(Phase 4) puts re-evaluating bandit learning on hold until the
|
||||
SLM-driven dispatcher is in production. Three options on the
|
||||
table: keep bandit as feedback for the SLM, retire EMA in
|
||||
favour of qualitative outcome summaries fed to the SLM, or
|
||||
split responsibilities (SLM = intent routing, bandit =
|
||||
cost/quality within a tier). See
|
||||
[`docs/superpowers/plans/2026-05-07-gnoma-roadmap.md`](docs/superpowers/plans/2026-05-07-gnoma-roadmap.md)
|
||||
§Phase 4.
|
||||
|
||||
2. **User-tunable selector knobs.** Several constants are
|
||||
hardcoded today: `qualityAlpha` (EMA smoothing, ~3-sample
|
||||
memory), the 70/30 observed/heuristic blend,
|
||||
`strengthScoreBonus` for tagged task types, and the
|
||||
`DefaultThresholds.Minimum` quality floor. Surfacing these as
|
||||
`[router.bandit]` config keys would let users tune for their
|
||||
workloads (faster alpha for shifting model performance, longer
|
||||
memory for stable fleets) without waiting for the strategic
|
||||
decision in #1.
|
||||
|
||||
Surfaced from the r/coolgithubprojects v0.3.1 launch thread
|
||||
(2026-05-24, `u/Ha_Deal_5079`).
|
||||
|
||||
- **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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user