{"slug": "show-hn-an-open-source-multi-tenant-ai-native-software-factory", "title": "Show HN: An open-source multi-tenant, AI-native software factory", "summary": "Missing Studio released Eva 0.1.0, an open-source, multi-tenant, AI-native software factory that logs every interaction to disk as evidence, with the terminal chat client showing only what is already recorded. The tool, built in Go 1.26, supports Anthropic and OpenAI providers, and its roadmap includes a control plane for coding agents with verifier-scored races, though current version lacks file, test, and shell tools.", "body_md": "**Evidence, not claims.**\n\nAn autonomous, multi-tenant, AI-native software factory\n\nMost AI tools tell you what they did. Eva writes it down first.\n\nEvery question you ask, every answer that comes back, every retry, and every token counted is appended to a file on your disk as it happens. What you see on screen is read back out of that file. If Eva shows you an answer, the file already had it. There is no second version of events.\n\nThat sounds like a small thing. It is the whole design.\n\n``` bash\n$ eva\n\n EVA\n Evidence, not claims\n\n version  0.1.0+e839c8a\n model    claude-sonnet-4-5\n branch   main\n cwd      ~/code/eva\n\n type /help for slash commands\n\n› what's the difference between a cache write and a cache read?\n\nA cache write stores your prompt prefix so later calls can skip re-reading\nit. A cache read is one of those later calls hitting the stored copy…\n\n› /cost\nsession 1.2k in / 340 out · cache 2.0k write / 1.1k read · cost unreported\n```\n\nNotice `cost unreported`\n\n. Neither Anthropic nor OpenAI returns a dollar figure with a response. So Eva says so, rather than multiplying tokens by a price it looked up somewhere. A number you can argue with a bill about has to come from the bill.\n\nNote\n\n**Eva is early.** Today it is a good terminal chat client with a very carefully built foundation. It can read your question and answer it. It cannot read your files, run your tests, or touch your shell — there are no tools yet.\n\nEva is being built toward a control plane for coding agents. Work arrives as a spec with acceptance criteria a machine can check. Several harnesses race the same spec in isolated environments. A verifier Eva owns decides what actually passed, and the whole race is scored from the same record everything else is scored from.\n\nThe reason for building the foundation this carefully first is the ladder in [docs/explanation/the-ladder.md](/missingstudio/eva/blob/main/docs/explanation/the-ladder.md). The usual story goes model → agent → harness → factory. That chain skips five rungs, and each omission is a known way this fails:\n\n| The rung that gets skipped | What breaks without it |\n|---|---|\nWorkflow |\nControl flow is handed to the model before the model can hold it |\nEnvironment + verifier |\nNothing tells the agent it is wrong, so it never converges |\nScheduler + spec format |\nMore agents is not a factory. A factory needs a queue and a spec |\nLearning loop + economics |\nNo evals, and no cost per merged change |\nIntent + authority |\nNobody owns what was decided, and nobody answers for it |\n\nNineteen stages, each with an exit test it can fail. One of them is built. The plan is a draft; the stage that shipped is not.\n\nYou need [Go 1.26](https://go.dev/dl/) or newer. Nothing else.\n\n```\ngit clone git@github.com:missingstudio/eva.git\ncd eva\ngo build -o eva ./cmd/eva\n```\n\nThat produces a single binary in the current directory. Put it on your `PATH`\n\nif you want it everywhere.\n\nEva talks to Anthropic and OpenAI. Pick one.\n\n```\nexport ANTHROPIC_API_KEY=sk-ant-...\n./eva\n```\n\nThat's the whole setup. Anthropic is the default, so nothing needs configuring.\n\nFor OpenAI, create a settings file and name the provider:\n\n```\n./eva init                       # writes ~/.eva/config.toml\nexport OPENAI_API_KEY=sk-...\n[provider]\nname = \"openai\"\n```\n\nOne line is enough. The model and the key variable follow the provider you picked, so you get `gpt-5.6-terra`\n\nreading `OPENAI_API_KEY`\n\nwithout saying either out loud.\n\nIf you pay OpenAI monthly, you can use that instead of an API key:\n\n```\n./eva login\n```\n\nIt prints a URL and a short code, you approve it in a browser, and the credential is saved to `~/.eva/auth.json`\n\n. Then set the mode in `~/.eva/config.toml`\n\n:\n\n```\n[provider]\nname = \"openai\"\nauth = \"subscription\"\n```\n\nCheck what Eva will actually use at any time:\n\n```\n./eva auth status\nprovider: openai\nauth:     subscription\nstore:    /Users/you/.eva/auth.json\nlogin:    account acct_1a2b, valid until Mon, 11 Aug 2026 09:14:00 IST\n```\n\nImportant\n\n** auth decides, and nothing overrides it.** If it says\n\n`subscription`\n\n, an exported `OPENAI_API_KEY`\n\nis ignored, and `eva auth status`\n\nwill tell you so rather than quietly using it. Most tools try the environment first, which is how people bill the wrong account for a month without noticing. ([why](/missingstudio/eva/blob/main/docs/adr/0031-a-credential-has-a-mode-and-the-mode-alone-decides.md))\n\nYour key is never written to a settings file. Eva reads it from the environment, or gets it when you log in. It never appears in the history file, a log, or anything sent to a model.\n\nRun `eva`\n\nwith no arguments and type. Answers stream in as they arrive.\n\n| Key | What it does |\n|---|---|\nenter |\nSend |\nshift+enter or alt+enter |\nNew line without sending |\nctrl+c |\nStop the answer in progress, keep what you typed |\nctrl+d |\nQuit |\ntab |\nFinish a slash command |\nshift+↑ ↓, pgup pgdn |\nScroll back |\nctrl+home / ctrl+end |\nJump to the top / back to live |\n\nInterrupting is safe. The conversation stays usable and the history file records that you stopped it.\n\nType `/`\n\nat the start of a line. These are handled locally and never reach a model, so they cost nothing.\n\n| Command | What it does |\n|---|---|\n`/help` |\nList the commands |\n`/cost` |\nWhat this conversation has cost so far |\n`/clear` |\nStart a fresh conversation |\n`/model` |\nShow which model is answering |\n`/model gpt-5.6-terra` |\nSwitch models, keeping the conversation |\n`/login` |\nExplains that logging in happens outside the chat |\n\n`/model`\n\nswaps the model mid-conversation without dropping context, so the next answer still knows what you talked about. Eva doesn't keep a list of valid model names, because a list compiled last month would reject a model released last week. If the provider doesn't recognise the name, that answer fails and tells you.\n\n`/clear`\n\nstarts a new conversation rather than deleting messages from the current one. Your old messages are still in the history file either way. ([why](/missingstudio/eva/blob/main/docs/adr/0019-clearing-the-transcript-opens-a-new-session.md))\n\n`eva -p`\n\nanswers one question, prints it to stdout, and exits.\n\n```\neva -p \"explain this error\" > answer.md || echo \"that failed\"\n```\n\nIt exits non-zero when the answer failed, and writes the reason to stderr. That makes it safe to use in a pipeline: stdout is the answer and nothing else.\n\n```\n› what is this?\n\nNo response — the credential was refused\nprovider.auth is \"api_key\", so what anthropic refused is the key in\n$ANTHROPIC_API_KEY\n```\n\nTwo lines, both true. The first names the kind of failure, in Eva's own words rather than the vendor's error document. The second appears only when Eva checked something about your machine, and only when that fact leaves exactly one next step. A missing login says `run eva login`\n\nbecause that is certainly the fix. A refused key says which key was sent and stops, because revoked, wrong organisation, and suspended account all look identical from here. Sending you to fix something that was never broken costs you every later hint that would have been right. ([why](/missingstudio/eva/blob/main/docs/adr/0041-a-remedy-is-checked-and-the-layer-that-can-check-it-is-not-the-layer-that-says-it.md))\n\n| Command | What it does |\n|---|---|\n`eva` |\nOpen the chat |\n`eva -p \"<question>\"` |\nAnswer once, print to stdout, exit |\n`eva init` |\nWrite a starter settings file |\n`eva login` |\nSign in to a subscription |\n`eva auth status` |\nShow how Eva will authenticate |\n`eva help` |\nShow this list |\n\nTwo flags, and that's deliberate: `--config <path>`\n\npicks a settings file, `-p <question>`\n\nasks one question. Everything else is a setting, because settings are reviewable and flags are not.\n\n| Environment variable | What it's for |\n|---|---|\n`ANTHROPIC_API_KEY` |\nYour Anthropic key |\n`OPENAI_API_KEY` |\nYour OpenAI key |\n`EVA_CONFIG` |\nA different settings file (default `~/.eva/config.toml` ) |\n`EVA_HOME` |\nA different home for Eva's files (default `~/.eva` ) |\n\n`eva init`\n\nwrites `~/.eva/config.toml`\n\nwith every option present but commented out, and a note beside each saying what happens without it. Nothing is chosen for you. Uncomment a line to change it.\n\nHere is what Eva does with none of it written down:\n\n```\nmodel = \"claude-sonnet-4-5\"     # follows the provider if you leave it out\n\n[provider]\nname        = \"anthropic\"       # anthropic or openai\nauth        = \"api_key\"         # api_key or subscription\napi_key_env = \"ANTHROPIC_API_KEY\"\nbase_url    = \"\"                # a proxy, gateway, or local server\nmax_tokens  = 0                 # 0 lets the provider decide\n\n[trace]\npath = \"~/.eva/trace.jsonl\"     # where the history goes\nkind = \"jsonl\"                  # which writer keeps it\n\n[identity]\ntenant     = \"local\"\nactor      = \"local\"\nactor_kind = \"human\"            # human, agent, or system\n```\n\nColours, glyphs, spacing, and key bindings live under `[theme]`\n\nand `[keymap.bind]`\n\n, and the starter file lists those too. Set none of them and Eva looks exactly as it did before any of it was configurable. Colours follow your terminal's background and what it can display, so it fits in without being told.\n\nSettings are read from four places, each overriding the one before: built-in defaults, your file, the project's file, then `--config`\n\n. Eva works fine with none of them.\n\nA typo is an error, not a shrug. Write `modl = \"...\"`\n\nand Eva refuses to start and names the key. A settings file that silently ignores what you wrote is worse than one that won't load.\n\nA repo can carry `.eva/config.toml`\n\n, found by walking up from wherever you are. Handy for sharing a team's look:\n\n```\n[theme.colors]\nperson = \"#7AA6DC\"\n\n[theme.symbols]\nprompt = \"› \"\n\n[keymap.bind]\nfollow = [\"ctrl+g\"]\n```\n\nWarning\n\n**A repo can change how Eva looks, never what it does.** You clone a repo from the internet, and Eva reads that file before your first question, in a process holding your API key. So the list of what it may set is a short allow-list: appearance and key bindings. It cannot pick the model provider, point traffic at another server, rename the variable your key is read from, or move your history file. Anything else in that file is refused by name. ([why](/missingstudio/eva/blob/main/docs/adr/0029-a-repository-may-choose-how-eva-looks-and-not-what-it-does.md))\n\nEva is strict about naming, because the same concept under three names is how a codebase rots. Five of them show up in the docs and the code:\n\n| Word | Plain English |\n|---|---|\nSession |\nOne conversation. Survives a crash. What resume and rewind will act on. |\nRun |\nOne question and its answer. A conversation has many. |\nTurn |\nOne round trip to the model. A single Run may need several once tools exist. |\nTrace |\nThe history file. The single source of truth for what happened. |\nProvider |\nA model behind one interface: Anthropic, OpenAI, whatever comes next. |\n\nThe full list is [CONTEXT.md](/missingstudio/eva/blob/main/CONTEXT.md), including the words that were tried and retired.\n\nOne Go module. Data flows one way, and the compiler enforces it.\n\n```\n   you type                                     you read\n      │                                            ▲\n      ▼                                            │\n   ┌──────┐    ┌──────┐    ┌───────────┐        ┌────────┐\n   │ tui  │───▶│ loop │───▶│ providers │        │ render │\n   └──────┘    └──┬───┘    └───────────┘        └───▲────┘\n                  │                                 │\n                  ▼          committed first        │\n              ┌───────┐  ───────────────────────────┘\n              │ trace │      then shown\n              └───────┘\n```\n\nThe important part is the bottom. Nothing reaches your screen that didn't go through the file first. The rendering layer physically cannot talk to a model, a conversation, or the history file. It takes records and returns strings, and that's all it's allowed to import.\n\n| Package | What lives there |\n|---|---|\n`events` |\n\n`core`\n\n`trace`\n\n`config`\n\n`auth`\n\n`providers`\n\n`anthropic`\n\nand `openai`\n\n.`loop`\n\n`render`\n\n[,](/missingstudio/eva/blob/main/internal/theme)`theme`\n\n`tui`\n\n`cli`\n\nThose boundaries are an allow-list per package in [ .golangci.yml](/missingstudio/eva/blob/main/.golangci.yml), running in strict mode. An import nobody explicitly permitted fails the build. Widening a list is a visible line in a diff with a reason next to it, not something that happens by accident.\n\nThree decisions do most of the work:\n\n**A provider only knows how to dial, read a chunk, and hang up.** Queueing, retrying, and counting are written once and shared. Adding a provider is a few hundred lines, not a copy of the machinery. ([0034](/missingstudio/eva/blob/main/docs/adr/0034-one-driver-pulls-a-turn-and-a-provider-is-a-wire.md))**Things register themselves.** Providers and file writers add themselves to the set that settings choose from. So the wiring layer names no implementation, and the error listing your options cannot go stale. ([0028](/missingstudio/eva/blob/main/docs/adr/0028-selection-is-a-registry.md))**The screen is a read-only view.** It renders records and nothing else. ([0015](/missingstudio/eva/blob/main/docs/adr/0015-the-live-area-shows-the-stream-and-only-the-record-is-kept.md))\n\n```\nmake check\n```\n\nThat is exactly what CI runs: formatting, build, vet, lint, tests, every package.\n\n| Target | What it does |\n|---|---|\n`make check` |\nEverything below, in order |\n`make fmt` |\nFails if anything isn't gofmt-clean |\n`make lint` |\ngolangci-lint, where the package boundaries are enforced |\n`make test` |\nEvery package |\n`make eva` |\nBuild the binary into the repo root |\n`make tidy` |\nTidy dependencies |\n\nThe linter version is pinned and run via `go run`\n\n, so you don't install anything and your results match CI.\n\nTests drive the real Anthropic and OpenAI code against a local server speaking the real protocol. There's no mock provider, because a mock is a second implementation that can disagree with the first and be believed. ([0036](/missingstudio/eva/blob/main/docs/adr/0036-a-replaying-provider-is-not-a-provider-a-person-may-select.md))\n\n|\n\n[docs/how-to/](/missingstudio/eva/blob/main/docs/how-to)[docs/Product.md](/missingstudio/eva/blob/main/docs/Product.md)[docs/roadmap.md](/missingstudio/eva/blob/main/docs/roadmap.md)[docs/decisions.md](/missingstudio/eva/blob/main/docs/decisions.md)[docs/adr/](/missingstudio/eva/blob/main/docs/adr)`ls`\n\nis the index.[CONTEXT.md](/missingstudio/eva/blob/main/CONTEXT.md)[AGENTS.md](/missingstudio/eva/blob/main/AGENTS.md)[docs/agents/design-rules.md](/missingstudio/eva/blob/main/docs/agents/design-rules.md)[docs/agents/project-structure.md](/missingstudio/eva/blob/main/docs/agents/project-structure.md)Nothing in `docs/adr/`\n\nis ever rewritten or deleted. When a later decision overturns an earlier one, the old file says so in its own status line and stays. Being wrong is part of the record too.\n\nMIT. See [LICENSE](/missingstudio/eva/blob/main/LICENSE).", "url": "https://wpnews.pro/news/show-hn-an-open-source-multi-tenant-ai-native-software-factory", "canonical_source": "https://github.com/missingstudio/eva", "published_at": "2026-08-10 10:40:40+00:00", "updated_at": "2026-08-10 11:12:50.158129+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-agents", "ai-infrastructure"], "entities": ["Missing Studio", "Eva", "Anthropic", "OpenAI", "Go"], "alternates": {"html": "https://wpnews.pro/news/show-hn-an-open-source-multi-tenant-ai-native-software-factory", "markdown": "https://wpnews.pro/news/show-hn-an-open-source-multi-tenant-ai-native-software-factory.md", "text": "https://wpnews.pro/news/show-hn-an-open-source-multi-tenant-ai-native-software-factory.txt", "jsonld": "https://wpnews.pro/news/show-hn-an-open-source-multi-tenant-ai-native-software-factory.jsonld"}}