{"id":50466039,"url":"https://github.com/luispabon/steiner","last_synced_at":"2026-06-01T07:32:34.804Z","repository":{"id":352739296,"uuid":"1216407450","full_name":"luispabon/steiner","owner":"luispabon","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-29T09:00:48.000Z","size":15827,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-29T11:04:02.390Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/luispabon.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":"docs/ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-04-20T21:52:45.000Z","updated_at":"2026-05-29T09:00:36.000Z","dependencies_parsed_at":null,"dependency_job_id":"ee38f7eb-5141-4ecf-bb2b-53f64a050771","html_url":"https://github.com/luispabon/steiner","commit_stats":null,"previous_names":["luispabon/steiner"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/luispabon/steiner","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luispabon%2Fsteiner","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luispabon%2Fsteiner/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luispabon%2Fsteiner/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luispabon%2Fsteiner/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/luispabon","download_url":"https://codeload.github.com/luispabon/steiner/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luispabon%2Fsteiner/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33765378,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-01T02:00:06.963Z","response_time":115,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-01T07:32:33.923Z","updated_at":"2026-06-01T07:32:34.788Z","avatar_url":"https://github.com/luispabon.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# steiner\n\n`steiner` is a minimal, local-first coding agent in Go. It is built to work against real local repositories with bounded context, explicit approvals, and a split provider/model configuration that supports local OpenAI-compatible servers plus native provider integrations.\n\n## Today in brief\n\n* Single-agent loop with interactive terminal mode and `--exec`\n* Config, tools, skills, and version commands are available now\n* Default provider targets a local OpenAI-compatible endpoint\n* Mutating tools are approval-gated by default; reads are auto-approved\n\n## Quickstart\n\nRequirements: Go `1.25+`.\n\n1. Start from source in this repo.\n2. Make sure an OpenAI-compatible server is running at `http://localhost:11434/v1`, or override the provider/model settings in config.\n3. Run a first request from source:\n\n```bash\ngo run ./cmd/steiner --exec \"summarize this repository in one sentence\"\n```\n\n4. Optional: inspect the resolved provider/model configuration before you do real work:\n\n```bash\ngo run ./cmd/steiner config\n```\n\nIf you want a local binary:\n\n```bash\nmake build-binaries\n./bin/steiner\n```\n\n## Common commands\n\n* `go run ./cmd/steiner` or `./bin/steiner` - interactive mode\n* `go run ./cmd/steiner --exec \"...\"` - run one request and exit\n* `go run ./cmd/steiner version` - print the build version\n* `go run ./cmd/steiner config` - print the resolved configuration, including provider and model definitions\n* `go run ./cmd/steiner tools` - list configured tools\n* `go run ./cmd/steiner skills` - list discovered skills\n\n## Configuration\n\nConfiguration precedence is:\n\n1. compiled defaults\n2. `~/.config/steiner/config.yaml`\n3. `.steiner/config.yaml`\n4. environment variables with the `STEINER_` prefix\n5. CLI flags\n\nKey environment variables:\n\n* `STEINER_MODEL`\n* `STEINER_SCHEDULER_PARALLELISM`\n* `STEINER_MAX_TURNS`\n* `STEINER_MAX_TOKENS`\n* `STEINER_CAVEMAN_MODE`\n* `STEINER_LOG_LEVEL`\n* `STEINER_LOG_FILE`\n* `STEINER_TOOL_OUTPUT_MAX_BYTES`\n\n### Config format\n\nConfiguration is split between `providers` and `models`:\n\n* `providers.\u003cname\u003e` defines how Steiner connects to an API endpoint or native provider.\n* `models.\u003cname\u003e` defines a selectable model alias, references one provider with `provider`, and sets model-specific request behavior.\n* `default_model` selects the model alias used by default.\n\nThe compiled defaults in `internal/config/defaults.go` are equivalent to one local provider plus one default model alias.\n\nExample 1: minimal local config for LM Studio or Ollama:\n\n```yaml\ndefault_model: local\nproviders: {local: {type: openai_compat, base_url: http://localhost:11434/v1}}\nmodels: {local: {provider: local, id: qwen3-35b-a3b}}\n```\n\nUse `http://127.0.0.1:1234/v1` as the `base_url` when pointing the same shape at LM Studio.\n\nExample 2: OpenRouter with `api_key_env`:\n\n```yaml\ndefault_model: sonnet\n\nproviders:\n  openrouter:\n    type: openrouter\n    api_key_env: OPENROUTER_API_KEY\n\nmodels:\n  sonnet:\n    provider: openrouter\n    id: anthropic/claude-3.7-sonnet\n```\n\nExample 3: multiple providers with multiple models:\n\n```yaml\ndefault_model: local-fast\n\nproviders:\n  local:\n    type: ollama\n    base_url: http://localhost:11434\n  router:\n    type: openrouter\n    api_key_env: OPENROUTER_API_KEY\n\nmodels:\n  local-fast:\n    provider: local\n    id: qwen3:14b\n  local-deep:\n    provider: local\n    id: deepseek-r1:32b\n  sonnet:\n    provider: router\n    id: anthropic/claude-3.7-sonnet\n  gpt-4.1-mini:\n    provider: router\n    id: openai/gpt-4.1-mini\n```\n\nExample 4: tuned model with `params`, `extra_params`, and a prompt suffix:\n\n```yaml\ndefault_model: precise\n\nproviders:\n  local:\n    type: lmstudio\n    base_url: http://127.0.0.1:1234/v1\n\nmodels:\n  precise:\n    provider: local\n    id: qwen/qwen3-coder-30b\n    params:\n      temperature: 0.1\n      top_p: 0.9\n    extra_params:\n      frequency_penalty: 0\n      metadata:\n        profile: precise\n    prompt_suffix: \u003c|think_off|\u003e\n```\n\nExample 5: advanced overrides with explicit limits:\n\n```yaml\ndefault_model: long-context\n\nproviders:\n  local:\n    type: openai_compat\n    base_url: http://localhost:11434/v1\n    timeout: 45s\n\nmodels:\n  long-context:\n    provider: local\n    id: qwen3-32b\n    advanced:\n      limits:\n        context_window: 131072\n        max_output_tokens: 8192\n\nlimits:\n  max_turns: 80\n  max_tokens: 900000\n  tool_timeout_default: 45s\n  tool_output_max_bytes: 131072\n```\n\nExample 6: multi-model setup with `default_model`:\n\n```yaml\ndefault_model: everyday\n\nproviders:\n  local:\n    type: openai_compat\n    base_url: http://localhost:11434/v1\n\nmodels:\n  everyday:\n    provider: local\n    id: qwen3-14b\n  review:\n    provider: local\n    id: qwen3-32b\n    extra_params:\n      thinking:\n        type: enabled\n        budget_tokens: 16000\n```\n\nProvider fields:\n\n* `type`: `openai_compat`, `ollama`, `lmstudio`, `openrouter`, `openai`, `anthropic`, `gemini`, or `litellm`\n* `base_url`: endpoint override when the provider type uses one\n* `api_key` or `api_key_env`: credential source\n* `headers`: optional extra HTTP headers\n* `timeout`: provider request timeout\n\nModel fields:\n\n* `provider`: provider name from `providers`\n* `id`: backend model identifier sent to the provider\n* `params`: normalized generation params\n* `extra_params`: provider-specific request fields merged on top of `params`\n* `prompt_suffix`: optional text appended to the last user message for each model request\n* `retry`: retry policy for model requests\n* `prompts`: per-model prompt overrides\n* `advanced.limits`: prompt budgeting and output token limits\n\nApproval defaults are conservative: `read`, `glob`, `grep`, and `ls` are auto-approved; mutating actions like `write`, `edit`, and `bash` prompt first. For most installs, the minimum useful config is `default_model`, one provider entry, one model entry that points at that provider, and any overrides you actually need in `limits`, `approval`, `tools`, `project_context`, `paths`, or `logging`.\n\n## Web search\n\nThe `web_search` tool lets the model search the web and return URL, title, and description results. It is not available by default — it must be enabled by setting `search.backend` in the config.\n\n### Supported backends\n\n| Backend | Config key | Auth |\n|---------|------------|------|\n| **Google** | `google` | `GOOGLE_SEARCH_CX` + `GOOGLE_SEARCH_API_KEY` env vars |\n| **Kagi** | `kagi` | `KAGI_API_KEY` env var |\n| **Brave** | `brave` | `BRAVE_API_KEY` env var |\n| **SearXNG** | `searxng` | `search.searxng_url` config key (self-hosted, no API key) |\n\nSetting `search.backend` to `\"\"` (or omitting the `search` block entirely) disables the `web_search` tool. The model will not have access to web search.\n\n### Config example\n\n```yaml\nsearch:\n  backend: google          # one of: google, kagi, brave, searxng\n  # searxng_url: http://localhost:8888   # required when backend is \"searxng\"\n```\n\n#### Per-backend environment variables\n\n**Google:**\n```bash\nexport GOOGLE_SEARCH_CX=your_custom_search_engine_id\nexport GOOGLE_SEARCH_API_KEY=your_google_api_key\n```\n\n**Kagi:**\n```bash\nexport KAGI_API_KEY=your_kagi_api_key\n```\n\n**Brave:**\n```bash\nexport BRAVE_API_KEY=your_brave_api_key\n```\n\n**SearXNG:** no API key required. Point `search.searxng_url` at your instance.\n\n### How it works\n\nThe search config is loaded from the `search` block in `config.yaml` (or the equivalent `STEINER_SEARCH_*` env vars). At startup `NewSearchBackend` dispatches on `search.backend` and creates the corresponding `web.Searcher` implementation.\n\nWhen a searcher is available:\n- The `web_search` tool is registered in the active tool registry (visible to the main model).\n- The `research` sub-agent also gains `web_search` in its allowlist.\n\nWhen no searcher is available (`search.backend` is unset):\n- The `web_search` tool is not registered at all — the model never sees it.\n- The `research` sub-agent exclude `web_search` from its available tools.\n\n### All providers config example\n\nThis example shows all four backends configured across different config layers (only one can be active at a time — set `search.backend` to pick):\n\n```yaml\n# Only one backend is active at a time, selected by search.backend.\n# Environment variables must be set for the selected backend (see above).\nsearch:\n  backend: brave            # change to google, kagi, or searxng as needed\n  searxng_url: http://localhost:8888   # only used when backend is \"searxng\"\n```\n\n### Notes\n\n- The `web_search` tool is separate from the model provider — it uses its own HTTP client and does not route through the model provider configuration.\n- SearXNG is self-hosted and has no API key; any public or private instance URL works.\n- Brave caps results at 20. All other backends cap at 30 (the global limit is adjustable per invocation via the `limit` parameter, up to 30).\n\n## Sub-agent delegation\n\n`steiner` exposes seven sub-agent-as-tool operations that delegate bounded tasks to isolated child agents. Sub-agent delegation is **enabled by default** — the model sees the following tools:\n\n- **`explore`** — navigate the codebase to find files, symbols, call sites, and patterns\n- **`research`** — gather and synthesise information from the codebase or web (only available with a `web_search` backend configured)\n- **`code`** — implement a scoped change, run tests, report results\n- **`plan`** — analyse a sub-problem and produce a structured recommendation\n- **`verify`** — run checks (tests, linters, builds) and report pass/fail\n- **`delegate`** — generic sub-agent with custom system prompt, context, and per-invocation overrides\n- **`follow_up`** — resume an existing sub-agent session by ID with a new user message; preserves conversation history while resetting the child's budget to fresh defaults\n\n### Configuration\n\nSub-agents are configured under the `sub_agent` key in `config.yaml`:\n\n```yaml\nsub_agent:\n  enabled: true                  # set to false to remove sub-agent tools\n  max_turns: 30                  # default turn limit\n  max_tokens: 100000             # default output token budget\n  allowed_tools:                 # tools for generic `delegate` only\n    - read\n    - glob\n    - grep\n    - ls\n    - write\n    - edit\n    - bash\n    - scratchpad\n  agents:                        # per-type model overrides (optional)\n    code:\n      model: gpt-4o\n    research:\n      model: claude-sonnet-4\n```\n\nSee [docs/SUBAGENTS.md](docs/SUBAGENTS.md) for full documentation — including agent-specific tool allowlists, safety restrictions, and per-invocation overrides for the `delegate` tool.\n\n## Caveman mode\n\nCaveman mode makes the model speak tersely, stripping filler, articles, pleasantries, and hedging. Reduces tokens and response length while keeping technical content intact.\n\n**Disabled by default.** Enable explicitly via config, env var, CLI flag, or `/caveman` slash command in interactive mode.\n\n```yaml\n# config.yaml\ncaveman_mode: true\n```\n\n```bash\n# environment\nSTEINER_CAVEMAN_MODE=true\n\n# CLI flag\n--caveman\n```\n\nIn interactive TUI, toggle on/off with `/caveman`. The toggle persists for the session.\n\nWhen enabled, caveman-style instructions are injected into:\n\n- **System preamble** — the main agent prompt instructs the model to respond tersely\n- **Compaction prompts** — compaction summaries are written in caveman style to maximise information per token\n- **Sub-agent prompts** — delegated agents inherit the terseness instruction\n- **Scaffold inference** — internal scaffold generation also follows caveman style\n\nCaveman mode is purely a prompt-layer transformation. It does not change tool behavior, approval gates, or any other config.\n\n## Development\n\n### Build and test\n\n```bash\ngo build ./...\ngo test ./...\nmake build-binaries\ngo vet ./...\n```\n\n### Development checks\n\nInstall local check tools:\n\n```bash\nmake install-check-tools\n```\n\nRun all checks:\n\n```bash\nmake check\n```\n\nFormatting:\n\n```bash\nmake fmt\n```\n\nThe full linter configuration is in `.golangci.yml` at the repo root.\n\n## Development notes\n\nRepo layout is compact: `cmd/` for entrypoints, `internal/` for agent/provider/tool/config code, `docs/` for product docs, and `testdata/` for fixtures. Conventions and deeper repo rules live in [AGENTS.md](AGENTS.md).\n\n## Further reading\n\n* [AGENTS.md](AGENTS.md)\n* [docs/PRD.md](docs/PRD.md)\n* [docs/ROADMAP.md](docs/ROADMAP.md)\n* [docs/INITIAL_IMPLEMENTATION_PLAN.md](docs/INITIAL_IMPLEMENTATION_PLAN.md)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fluispabon%2Fsteiner","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fluispabon%2Fsteiner","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fluispabon%2Fsteiner/lists"}