NAVI // AGENTS.MD

how we build navi.

The project's shared brain — conventions, release process, and hard-won rules — written for agents and humans alike. Read it and hit the ground running.

Mirrored from AGENTS.md on the eiri rolling branch. The repo copy is the source of truth.

Read this first. This file is the project's shared brain: everything an agent or a human needs to hit the ground running on navi development. It's maintained by the people (and agents) who do the work — when you learn something the hard way, add it here so the next one doesn't have to.

What navi is

navi is xvoidsx's Linux distro: a complete, opinionated, agent-native desktop OS on a dependable Debian (trixie) base. Wayland-first (sway, our "wired" session), X11 fallback (i3). Deep blacks, neon pink, phosphor green — the nightshadeNeon design language across the whole desktop.

Guiding ideas:

Who you're working with

Repo map

navi/
├── install.sh            # installer AND deploy script (--deploy-only)
├── wired/                # the desktop layer — see "The /wired boundary"
│   ├── VERSION             # <-- source of truth for the version string
│   ├── sway/ i3/           # window manager configs (the "wired" sessions)
│   ├── waybar/             # bar config, style.css (GTK CSS!), modules,
│   │                       # mod-open.sh (the floating launcher)
│   ├── identity/           # os-release, lsb-release, issue, issue.net
│   │                       # (re-stamped from VERSION at every deploy)
│   ├── naviApps/           # the app store: apps.json + build-catalog.sh
│   │                       # → catalog.js (inlined; never fetch() it)
│   ├── scripts/            # navi-notifs, helpers
│   ├── manual/             # built-in manual (Super+Shift+H); mirrored on site
│   ├── applications/       # .desktop launchers incl. rofi-visible entries
│   └── ...                 # per-app configs: rofi, alacritty, dunst, tmux…
├── scripts/                # system-level scripts (NOT the desktop layer)
│   ├── navi-update.sh        # self-updater: pulls release tags, redeploys
│   ├── navi-wired-restore.sh # drift diff / restore / backup vs stock
│   ├── navi-wired-adopt.sh   # lay stock wired configs over customized ones
│   └── installers/           # codex, antigravity, etc.
├── iso/
│   ├── stage.sh            # stages /opt/navi-iso for the ISO build
│   ├── installer/          # the CLI installer (preflight lives here)
│   ├── config/             # iso.yml inputs: RELEASE, ISO_BASENAME…
│   └── README.md
├── .github/workflows/
│   ├── iso.yml             # builds the ISO; RELEASE CHECKLIST lives here
│   └── publish-stable.yml  # publishes tag → GitHub release + assets
├── src-navi/ src-wiredWM/  # source trees (see RESTRUCTURE.md)
├── screenshots/
└── _sources/

The /wired boundary

/wired is the desktop layer only. Distro-level concerns (base system, users, doas/sudo, services, firewall) live elsewhere in the repo. Never move system-level pieces into /wired, and never reach out of it for them.

Source of truth: wired/VERSION

The version string (e.g. 1.4.6 "mika") lives in wired/VERSION. Everything human-facing is derived from it at deploy time:

Rule: iso.yml carries a RELEASE CHECKLIST (including a POST-RELEASE section). Follow it every release. It exists because every item on it was once a shipped bug.

install.sh — installer and deployer

install.sh does double duty: fresh installs and redeploys onto a live system via --deploy-only (this is what navi-update runs after pulling).

The self-maintenance layer

Three commands, three jobs. Don't confuse them:

navi mods always float (standing rule)

Panel modules (notification bell, update indicator, …) open floating Alacritty TUIs. Launchers enforce floating themselves — never rely on the shipped sway config's for_window rules (customized configs are preserved, so the rule may not exist) and never sleep-then-float (a race slow machines lose).

The convention is wired/waybar/mod-open.sh <title> <cmd…>:

  1. Probe, don't guess, the compositor. Real swaymsg -t get_version round-trip (then i3-msg -t get_version); never trust $SWAYSOCK alone.
  2. Install a runtime for_window rule BEFORE the window maps — no race.
  3. Poll-and-float by PID as the backstop. Titles are mutable (TUIs rename themselves); PIDs aren't. Anchor the match so PID 1234 can't match 12345.
  4. Log one line per launch to ~/.local/share/navi/mod-open.log — future misbehavior must leave footprints.
  5. If there's no compositor at all, just run the command (plain-open path).

The agent mod (custom/agent)

The little robot face on the panel — navi's agent-status integration. wired/waybar/agent-mod.sh polls every 5s and emits waybar JSON with one of five classes; wired/waybar/agent-menu.sh is the right-click rofi dispatcher.

State priority (first match wins): agent-blocked (a herdr agent waits on the user) → agent-listening (Hey Lain mic hot via $XDG_RUNTIME_DIR/hey-lain/rec.pid, or thinking via processing.lock) → agent-working → agent-ready (agents idle, or ollama reachable) → agent-asleep. Herdr states come from herdr agent list (idle/working/blocked/done/unknown); the server-down shape is JSON with an error key and exit code 0, so parse the body, don't trust the exit.

Click contract: left → the agent center (navi-agents-config, floating), middle → herdr attach (floating), right → the rofi menu, which leads with blocked agents ("⚠ <agent> needs you — jump to herdr") when any are blocked.

The center's tabs: status (live herdr agents with waybar-style faces, enter jumps to one via herdr agent attach addressed at the pane id — herdr doesn't resolve bare agent labels, so the label is display-only; r reads recent output the same way), models (ollama service on/off via doas, pull/delete models), harnesses (installed harnesses, default agent — also what navi-Q opens), autonomy (opencode permission profiles - ask me / balanced / yolo - written to ~/.config/opencode/opencode.json, preserving other keys; other harnesses keep their own settings for now), evals (model compare: an 8-prompt built-in set run against selected pulled ollama models, each response scored 1-5 by a local judge model, scoreboard with averages and a winner), providers (API keys). The default agent is stored in ~/.config/navi/default-agent (whitelisted to known harness binaries); navi-Q/navi-Qx read it, falling back to opencode.

Scope is honest by design: herdr only sees its own panes, so bare-terminal agents are invisible — tooltips say "in herdr" and never claim global awareness. CSS lives in wired/waybar/style.css (#custom-agent + state classes; GTK CSS rules apply). Customized waybar configs are preserved by navi-update, so users on customized configs need navi-wired-adopt (or a manual module add) to see new modules.

Release process, end to end

  1. Bump wired/VERSION; run the iso.yml RELEASE CHECKLIST (every human-facing string: VERSION, installer banner + success box, iso config RELEASE/ISO_BASENAME/volume label, iso/README.md, identity files).
  2. Commit to main (the working branch; the mika line merged into main).
  3. Tag v<version>-mika (e.g. v1.4.6-mika). All 1.x releases are intentionally unsigned — signing is deferred to the eiri era. (The release-signing GPG key exists; it's just not wired into 1.x.)
  4. iso.yml builds the ISO artifact (navi-<version>-mika.iso).
  5. publish-stable.yml (parameterized by tag) publishes the GitHub release with the ISO + SHA256SUMS.txt. /releases/latest then resolves to it.
  6. POST-RELEASE: bump the website (xvoidsx/enter-the-wired: index.html, roadmap.html, news.html — keep download buttons on /releases/latest, never a pinned tag; follow the news-archive convention below) and the navi README version strings.
  7. Write release notes; keep a copy under ~/workspace/your_files/.

Release naming: 1.x = "mika", 2.x = "eiri". Tags look like v1.4.6-mika. The updater discovers "newer" via sort -V over tags matching ^v1\.[0-9]+, so patch versions sort correctly.

Never claim publication, updater success, hardware validation, or public deployment without verifying it. The ping/announcement is a contract — keep it, and only for what's actually done.

Hard-won rules (each one cost us a release)

naviApps catalog rule

The store page must NEVER fetch() its catalog — file:// CORS plus Chromium ignoring CLI flags when already running breaks it. The catalog ships inlined as wired/naviApps/catalog.js, generated from apps.json by wired/naviApps/build-catalog.sh. Re-run the script and push BOTH files whenever apps.json changes.

Seamless GIF loops

Modulo-wrapping a position does NOT make a loop seamless. Every translating element must move an integer number of spatial periods per loop (v * loop_seconds ≡ 0 mod period). Verify frame N against frame 0 numerically, not by vibes. (Keep flagship gifpapers under GitHub's 25 MB file limit so they can ship in the repo.)

Website conventions

House conventions

The one-sentence version

Probe, don't guess; test the real shape on real hardware; preserve the user's customizations; log everything; and never claim what you haven't verified.