Skip to content
nyrvo-devPublic

About

A Go CLI that captures and compares execution environments, answering why an application behaves differently across local, CI, staging, and production.

Resources

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

Nyrvo

A Go CLI that captures and compares execution environments, answering why an application behaves differently across local, CI, staging, and production.

Status

Early development. The commands listed under Usage are what works today: capture, diff, reading CI configuration, importing a run that happened, and deterministic diagnosis. nyrvo ci replay prints what a job does; it never runs it, and nyrvo doctor --ai prints a request for your own agent while --agent= hands it to an agent CLI you already installed. Not yet available: inspecting a database's own schema or settings, configuration files, and executing anything else on your behalf.

Why

Code that passes on a laptop and fails in CI is usually not a code problem: it is environment drift. Different Go, Node, or Python versions, a different kernel, an uncommitted working tree, or a missing environment variable silently change behavior between environments. Nyrvo snapshots each environment as a small, shareable JSON document and diffs them so you can see exactly what differs between local and CI, staging, and production.

Install

Every release attaches prebuilt binaries for macOS (amd64, arm64), Linux (amd64, arm64) and Windows (amd64, arm64). Download the archive for your platform from the releases page and unpack it:

VERSION=X.Y.Z   # the release you want, without the leading v
curl -LO https://github.com/nyrvo-dev/nyrvo/releases/download/v$VERSION/nyrvo_${VERSION}_darwin_arm64.tar.gz
tar -xzf nyrvo_${VERSION}_darwin_arm64.tar.gz
./nyrvo version

On Windows, the .zip archive unpacks to nyrvo.exe. Every archive contains the binary, the LICENSE and the README, and each release also carries a SHA256SUMS you can use to verify the download.

Or, if you have a Go toolchain, install the command directly:

go install github.com/nyrvo-dev/nyrvo/cmd/nyrvo@latest

or build the binary locally:

make build

Usage

Capture two environments and diff them:

$ nyrvo capture local
$ nyrvo capture ci
$ nyrvo diff local ci

--json prints machine-readable output instead of human text:

$ nyrvo capture local --json
$ nyrvo diff local ci --json

Compare this machine against what CI declares:

$ nyrvo ci inspect            # what each workflow job says it needs
$ nyrvo ci capture lint       # save that job's declared environment as "ci"
$ nyrvo capture local
$ nyrvo diff local ci

ci inspect and ci capture read .github/workflows/*.yml and never run a workflow, resolve an action, or call the GitHub API. Anything Nyrvo recognizes but does not model is listed under "not modelled" instead of being silently dropped, and a value it cannot know — node-version: 20.x, runs-on: ${{ matrix.os }} — is reported as unknown rather than guessed.

Or import a run that actually happened:

$ nyrvo ci import 31921289286
$ nyrvo ci import https://github.com/owner/repo/actions/runs/31921289286
$ nyrvo ci import <run> "test (ubuntu-latest)"     # pick a specific job

A run reports what a job got rather than what it asked for: the commit that was checked out and the runner it landed on, so a ${{ matrix.os }} that the workflow file could only leave unknown becomes a real platform. It defaults to the job that failed — the one you are asking about — and refuses to guess when more than one did.

Importing uses your own gh CLI. Nyrvo never reads, stores, or asks for a GitHub token. Run metadata does not include installed runtime versions (those live in the job logs), and the snapshot says so rather than implying it knows.

Ask what the differences mean:

$ nyrvo doctor                                  # diagnoses local against ci
$ nyrvo doctor 31921289286                      # import a run and diagnose it, in one step
$ nyrvo doctor https://github.com/owner/repo/actions/runs/31921289286
$ nyrvo doctor local prod
$ nyrvo doctor --json

Given a run reference, doctor imports the run and captures this machine in memory and diagnoses both — nothing is saved, so a one-shot question never overwrites snapshots you captured deliberately. The report opens with what the evidence itself reported: the run's conclusion, the step that failed, the error line from the job log, and anything Nyrvo could not model.

doctor applies deterministic rules — no model, no network, no clock — and ranks findings by how plausibly they explain a failure rather than by how large the difference is. A version a workflow declares as a prefix (go-version: "1.26") is satisfied by a machine running 1.26.6, so a correct setup stays quiet. When nothing matches, Nyrvo says no rule matched; it does not claim the environments are equivalent.

doctor exits 0 even when it reports findings: a diagnosis is an answer, not a failure, and a CI job opts in when it wants the exit code to depend on them. --fail-on=high (or medium, low) exits 1 when a finding at that severity or worse exists:

$ nyrvo doctor local ci --fail-on=high     # exit 0 unless a high finding exists

It must be written with an equals sign. Every other nyrvo flag is boolean and may appear anywhere on the line (nyrvo diff local ci --json), so the separate form --fail-on high cannot be told apart from a snapshot named high and is rejected with a usage error. A job that wants an unsound environment to fail the build runs it against the defaults:

$ nyrvo capture local
$ nyrvo doctor --fail-on=high

Two rules judge a CI job's backing services rather than comparing environments. service.missing reports a service a job declares — a postgres, a redis — that no container running here provides, and service.image_mismatch reports the same image name running at a different tag. Nyrvo only sees containers, so the finding says as much: a natively installed service is invisible to it, and a machine whose Docker daemon is not answering produces no finding at all rather than a false one.

Services are never compared between environments. A laptop runs whatever its owner has up and a job runs the two sidecars it asked for; listing the difference describes a desk, not a defect.

One rule never compares two environments at all. runtime.requirement_unsatisfied (always high) judges an environment against what the checked-out project declares it needs, and reports when it does not meet it — the only rule that can call an environment wrong rather than merely different. It understands comparators (>=, >, <=, <, =), caret and tilde ranges, wildcards ("20.x"), bare prefixes, and comma- or space-separated conjunctions with || alternatives. It also knows which declarations are floors rather than pins: the go directive in go.mod states the lowest version the module accepts, so a project declaring go 1.25 and built on 1.26.6 is correct, not broken. A constraint it cannot fully parse — lts/iron, workspace:*, a hyphen range — produces no finding rather than a guess.

Asking your own AI agent

nyrvo doctor --ai prints the deterministic report, then an analysis request to hand to whatever agent you already use:

$ nyrvo doctor --ai
NYRVO DOCTOR
...
2 findings: 2 high

AI ANALYSIS REQUEST (NOT EXECUTED)

This is a request for analysis, not output from a model.

DISCLOSURE
  Agent      No agent was selected or run.
  Data       Two environment snapshots, their differences, and Nyrvo's own findings.
  Execution  Nothing was executed, and nothing left this machine.
  Command    No command was invoked.
  Privacy    Environment variable values are never recorded, and only the names this diagnosis refers to are included.
  Paths      Home directory prefixes in paths are replaced with ~.

----- BEGIN AI REQUEST -----
...
----- END AI REQUEST -----

Nyrvo gathers the evidence; your agent reasons about it. No model is called, no network request is made, and the package that builds the request cannot make one — so Nyrvo owns no API credentials and is tied to no vendor. nyrvo doctor without the flag never produces a request at all.

What the request carries is chosen rather than inherited. A snapshot has no field able to hold an environment variable's value, so a request cannot carry one; on top of that the home directory prefix of a runtime path becomes ~, and the environment variable names are reduced to the ones a difference or a finding actually refers to — on a laptop that is typically one name out of seventy.

If you would rather Nyrvo hand the request over for you, name an agent CLI you already have installed:

$ nyrvo doctor --agent=claude
$ nyrvo doctor --agent=codex
$ nyrvo doctor --agent=opencode

Nyrvo shells out to your own tool, authenticated on your own account — it owns no API keys and picks no vendor. Before anything runs it states which agent it will invoke, what the request contains, and the exact command, and it does not claim to know whether your tool answers locally or over the network, because that is your configuration and not Nyrvo's business. The answer is printed under AI ANALYSIS — <agent>, verbatim, marked as model inference rather than observation.

The agent runs in an empty temporary directory. These CLIs are coding agents with file tools, and Nyrvo's whole design is that it supplies the evidence instead of letting a model go rummaging through your machine; running it where there is nothing to read is how that is enforced without depending on any one vendor's flags. The trade is real: an agent cannot look at the code that failed, so it answers from the evidence Nyrvo vouches for or says it cannot.

To stop typing it every time, configure a default:

$ nyrvo config set ai.agent opencode
$ nyrvo doctor --ai                  # now runs opencode
$ nyrvo doctor --agent=claude        # an explicit flag still wins
$ nyrvo doctor --agent=none          # print the request instead, just this once
$ nyrvo config list                  # what is set, and where it lives

The file is user-level only, at os.UserConfigDir()/nyrvo/config.json. There is deliberately no project-level config, and that is a security decision rather than a simplification: this file selects a program Nyrvo will execute, so a copy of it living in a repository would let whoever opens a pull request choose that program. For the same reason, configuring an arbitrary command as an agent is not supported at all. Adding another known agent is a line in a table — see CONTRIBUTING.md.

--json prints one document either way, replacing the diagnosis document rather than following it: two JSON documents on one stream is not something a consumer can parse, and the request already carries the findings. It reports "executed": false for a request nobody ran, and "executed": true alongside agent, command and analysis when one did.

Reproducing a CI job

nyrvo ci replay <job> prints what a job does, in order, and marks every step it cannot reproduce here with the reason:

$ nyrvo ci replay test
REPLAY test

PREREQUISITES
  this job runs once per matrix combination (go-version=[1.25 1.26], os=[ubuntu-latest macos-latest]); the steps below describe one of them

STEPS

1. Checkout
   uses              actions/checkout@v4
   not reproducible  uses actions/checkout@v4; Nyrvo does not run actions, but your working tree is already the checkout, so there is nothing to do

2. Set up Go
   uses              actions/setup-go@v5
   not reproducible  actions/setup-go@v5 installs the go named by ${{ matrix.go-version }}, which the runner decides; install it yourself

3. Build
   run
     go build ./...

It executes nothing, by design. A tool that runs a workflow's steps outside the runner diverges from CI in ways it cannot see — no runner image, no actions, no secrets, no service containers — and produces a run that proves nothing while looking authoritative. A plan you read and paste stays honest about the gap. Environment values come from the workflow file and are printed as written, but a value referencing a secret is replaced with <secret>.

Other commands:

$ nyrvo list
$ nyrvo version

What Nyrvo observes

  • OS, CPU architecture, and kernel
  • Git commit SHA, branch, and whether the working tree is dirty
  • Go, Node.js, npm, pnpm, Yarn, Python, Ruby, PHP, Composer, Rust, Java and .NET versions (including install paths)
  • Docker client, server, and compose versions, and whether the daemon answers
  • The container images running here, and the services a CI job declares
  • The version constraints the checked-out project declares
  • Environment variable names
  • From CI: the runner platform, setup-action runtime versions, declared environment variable names, and service containers a job asks for

A probe can also fail to answer, and that is not the same as finding nothing. Nyrvo records three outcomes, not two. A tool that is not on PATH is absent. A probe that ran out of time is recorded as unmeasured: the question was asked and never answered, so the diff declines to compare it rather than reporting the tool as having vanished — run the capture again to settle it. A tool that is installed and refuses to report a version is recorded as unusable, and is reported as "installed, not usable" rather than missing; the usual cause is a pinned toolchain the machine does not have, and telling you to install software you already have would be worse than saying nothing.

An absent Docker section means Docker is not installed; a present section with daemon_running false means the CLI is there but the daemon is not answering. Those are different facts, and the second is a common reason a compose-backed suite passes in CI and fails on a laptop. Docker versions are compared by nyrvo diff like everything else.

The declared constraints come from engines.node, engines.npm and packageManager in package.json, the go directive in go.mod, .nvmrc, .python-version, requires-python in pyproject.toml, .tool-versions, .ruby-version, the ruby directive in a Gemfile, require.php in composer.json, rust-toolchain.toml, rust-version in Cargo.toml, global.json, and .java-version. They are stored as written, except where a format carries a checksum that is not part of the version — corepack's packageManager is recorded as pnpm@9.1.0, not with its +sha512… suffix.

Nyrvo also records whether a constraint is a floor or a pin, because the two warrant different findings: a bare go 1.25 in go.mod is a minimum, while ^8.3 in composer.json and pnpm@9.1.0 are not.

They are never compared between environments: both sides normally read the same repository, so diffing them would manufacture drift out of provenance. They exist so a rule can call a version wrong, which is what runtime.requirement_unsatisfied does.

Snapshots

Snapshots are stored as plain JSON files in .nyrvo/snapshots/<name>.json in the working directory, so they follow the repository they describe. Each file carries a schema_version so an older Nyrvo can refuse to misread a newer snapshot.

The first capture also writes .nyrvo/.gitignore, so capturing inside a repository does not leave untracked files that would make Nyrvo report its own working tree as dirty. Empty that file if you want to commit your snapshots; Nyrvo will not recreate it.

Security

The full policy, including how to report a vulnerability privately, is in SECURITY.md.

Nyrvo never stores environment variable values — only their names. Presence is enough to detect drift ("REDIS_URL is missing in CI"), and never recording values means credentials never end up in a file you share in a bug report or commit by accident. Snapshot files are written with 0600 permissions so they remain owner-readable.

Platforms

Verified on Linux, macOS and Windows. CI runs the whole suite on all three, across Go 1.25 and 1.26 — no test is skipped to get a platform green, with one stated exception: the check that snapshot and config files carry 0600 permissions runs where POSIX permission bits exist. Windows has none, and asserting them there would claim a guarantee the platform does not offer.

On Windows the kernel string is left empty, because uname is how Nyrvo reads it. OS and architecture come from the Go runtime and are always present.

Development

Run the full local checks with make check (test, race detector, vet, gofmt and golangci-lint). Go 1.25 is the minimum supported version; development runs on 1.26. CI verifies both.

How this is built

Nyrvo is written by @lucascnunes with Claude (Anthropic) as the implementing tool, under review. Direction, design decisions and acceptance are human; a large share of the code and prose is model-written.

That arrangement is disclosed because the project's whole argument is refusing to claim what has not been verified, and how the code came to exist is part of what a reader is entitled to check. It changes nothing about the standard the code is held to: every commit builds and tests in isolation, CI runs the suite on three operating systems, and every serious defect this project has found was found by running the tool against real data rather than by reading a diff.

License

MIT

About

A Go CLI that captures and compares execution environments, answering why an application behaves differently across local, CI, staging, and production.

Resources

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages