NAVI // AGENTS.MD
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.
navi is xvoidsx's Linux distro: a complete, opinionated, agent-native desktop OS on a dependable Debian (trixie) base. Wayland-only (sway, our "wired" session) since 2026-10-08 — the X11 session (i3) was retired; even the 2008 ThinkPad x200 renders sway fine, so the compat argument was dead. XWayland covers X11 apps. The i3/picom/polybar configs stay in the repo as the port-back kit. Deep blacks, neon pink, phosphor green — the nightshadeNeon design language across the whole desktop.
Guiding ideas:
navi-wired-adopt is for (explicit, backed up, loud warning).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.
├── mods/ # Go mods (agent center, waybar modules, browser
│ # picker, …) — prebuilt binaries committed
├── 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/
/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.
wired/VERSIONThe version string (e.g. 2.0 "eiri" on the dev branch, 1.7 "mika" on stable) lives in wired/VERSION. Everything
human-facing is derived from it at deploy time:
install.sh's install_identity re-stamps wired/identity/ (os-release,
lsb-release, issue, issue.net) from VERSION on every deploy — including
--deploy-only, so navi-update refreshes them. These files go stale
silently otherwise (they read 1.3 all through the 1.4 cycle once).iso/config RELEASE/ISO_BASENAME/
volume label, and iso/README.md must all be checked per release.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.
Brave is navi's default browser — full Brave, not Origin. Stock Chromium stays installed out of the box as the vanilla alternative. Firefox ESR ships too. The OS suggests, the user decides.
install.sh's setup_brave adds Brave's official apt repo, installs
brave-browser, and seeds navi's appearance defaults: ultradark
theme (brave.darker_mode), dark color scheme
(browser.theme.color_scheme2: 2), wide address bar
(brave.location_bar_is_wide) in Default/Preferences, and
compact horizontal tabs (brave.tabs.compact_horizontal_tabs) in
Local State. Compact tabs is a Local State pref — seeding it in
Preferences silently does nothing. Both files land in /etc/skel
(future users) and the invoking user's $HOME; existing files are
never overwritten, and seeding is skipped while Brave is running (it
rewrites Preferences on exit). Every installed Brave variant gets
the same vision (2026-10-08): release, nightly
(Brave-Browser-Nightly), origin (Brave-Origin — verified from the
binary; origin nightly shares it). install.sh seeds installed variants;
the variant installers (nightly-browsers.sh, brave-origin*.sh)
seed via scripts/installers/brave-seed-lib.sh right after install.ExtensionInstallForcelist at /etc/chromium/policies/managed/navi.json
leaked into Helium (Chromium forks share that policy dir), so it was
retired — install.sh removes it on existing machines. The nightshadeNeon
theme + blackice now install via Chromium's External Extensions
mechanism, scoped to ~/.config/chromium (wired/chromium/
external-extensions/*.json, deployed to $HOME and /etc/skel), so
forks never see them. Unlike force-install, the user can remove them.wired/qutebrowser/
config.py, nightshadeNeon theme, navi bangs). It presents as Chrome
stable so sites that reject QtWebEngine's UA — including Google sign-in —
accept it. The UA pin tracks the current Chrome stable major (157 as
of November 2026), NOT navi's chromium package version, so kirie blends
in with the Chrome population. Its adblock lists (EasyList, EasyPrivacy,
Brave unbreak, uBO filters) only engage when the adblock Python module
is importable — python3-adblock isn't in trixie or trixie-backports
(sid only), so install.sh pip-installs the engine (abi3 wheel,
best-effort) in deploy_configs(); without it kirie silently falls back
to hosts blocking.mods/navi-browser, prebuilt binary
committed) is how users switch the default browser and the webapp
runtime: it writes ~/.config/navi/default-browser, offers the xdg
system default, and deploys navi's managed policy to the chosen
browser — except Brave/brave-origin/chromium/helium, which are
SkipPolicy and never get the policy offer (chromium's extensions are
handled per-user by install.sh; helium shares chromium's policy dir,
so policy there would pollute both). navi-browser-run (what webapp launchers Exec)
resolves navi-browser --print-binary: configured default, then brave,
then chromium, then first installed.navi-imprint is xvoidsx's native nightshadeNeon ISO-flashing tool (Go/bubbletea) —
flash OS images to SD cards/USB drives from the terminal. It replaced balenaEtcher,
which was dropped 2026-10-01 when its balena-etcher-electron package hard-depended
on gconf2 (removed from Debian trixie entirely — apt refuses the install). The old
setup_etcher code paths in install.sh are retired, not just commented out.
setup_sable in install.sh, from Sable's Forgejo APT repo, so it updates
with everything else). Element stays in naviApps — it's loved, not
removed.setup_cyberia), GPL-2.0. A fork of Sable stays an easy escape
hatch, not the plan.install.sh does double duty: fresh installs and redeploys onto a live
system via --deploy-only (this is what navi-update runs after pulling).
/var/lib/navi/deploy-manifest.tsv
(sha256/version/relpath/dest/mode) so drift can be detected later.deploy_config calls must live in deploy_configs() (2026-10-08):
manifest_write clears MANIFEST_TMP when it finishes, and setup_*
functions run after it — a deploy_config in a setup_* function
hits >> "" and dies with : No such file or directory (caught live
on a navi-update when setup_chromium grew deploy_config calls).
setup_* may do direct $DOAS install writes, but never
deploy_config.install -m 0755 on /usr/bin copies and chmod 0755 on every
*.sh under /usr/share/navi — scripts run no matter how the repo was
fetched (clone, zip, tarball).SIGUSR2 config-reload. If you deploy a config for a daemon,
check whether the daemon picks it up on its own; if not, signal it./etc/sudoers.d/10-navi-user (<user> ALL=(ALL:ALL)
ALL, password-required), validated with visudo -c, self-removing on
check failure. Effective on the next sudo call — no re-login (group
membership needs a re-login; a sudoers rule doesn't).scripts/installers/: nightly browsers, ani-cli,
charm, cliamp) are not run by the installer — one failure there used
to abort the whole install under set -e. They're deployed to
/usr/share/navi/installers/ and offered through the navi-extras
post-install menu instead (/usr/bin/navi-extras, rofi-visible).setup_heylain deploys mods/hey-lain/ to
/usr/share/navi/hey-lain and builds its STT/TTS venv in place
(faster-whisper + piper + whisper model). The venv survives redeploys —
the .venv-ready marker stores a hash of requirements.txt, so
unchanged requirements mean "refresh scripts, keep venv"; a PyPI outage
warns and continues instead of wedging the install. Logs go to
$XDG_STATE_HOME/hey-lain/log (HEY_LAIN_LOG_DIR) because the deploy
tree is root-owned. Alt+V is bound in the shipped sway config;
warmup.sh runs at login.Three commands, three jobs. Don't confuse them:
navi-update — pulls the release channel and re-runs deploy.
Two channels, Debian-style: stable follows release tags (never
arbitrary main commits — a fix on main is invisible to the updater until
it's in a tag); eiri is rolling and tracks the eiri branch head
(deployed commit remembered in /var/lib/navi/navi-update.state).
--check for dry status; --channel for channel selection (switching
asks first, then pins /etc/navi/channel). Two-mode trust: signing key
provisioned → verify fail-closed; no key → loud warning + unsigned
pull. Preserves customized configs by design.navi-wired-restore — shows how the live setup drifted from stock and
restores what you choose, with backups of everything it touches.navi-wired-adopt — the explicit "give me the new stock desktop"
path: lays current stock wired configs over customized ones, backup first,
loud warning, --dry-run to preview, offers WM/bar reload when done.
Has a rofi launcher.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…>:
swaymsg -t get_version
round-trip (then i3-msg -t get_version); never trust $SWAYSOCK alone.for_window rule BEFORE the window maps — no race.1234 can't
match 12345.~/.local/share/navi/mod-open.log — future
misbehavior must leave footprints.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
usually JSON with an error key and exit code 0, so parse the body — but
some versions fail the call outright when the server is down, so the center
checks the install (LookPath) separately from the server and reports
"isn't running" instead of "isn't installed".
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 — Enter on an installed
harness writes the file; 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 hermes (Lain) via navi-lain,
then opencode — and the center's harnesses tab shows the same default
(pink ♥), so the marker and Alt+A never disagree.
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.
navi mods (mods/<name>/) are committed as compiled binaries
(mods/<name>/<binary>, mode 100755) — install.sh copies them to
/usr/bin, it never runs go build. Changing a mod's source does nothing
on installed machines until every affected binary is rebuilt
(go build -o <name> . in the mod dir; mods replace => ../theme, so a
theme change means rebuilding every mod) and recommitted. Verify the new
behaviour made it into the binary before pushing (strings on it, or the
mod's own smoke check) — HEAD-current is not binary-current.
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).eiri (the working branch); stable releases are cut from main.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.)iso.yml builds the ISO artifact (navi-<version>-mika.iso).publish-stable.yml (parameterized by tag) publishes the GitHub release
with the ISO + SHA256SUMS.txt. /releases/latest then resolves to it.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.~/workspace/your_files/.eiri rolling releases (the dev channel): eiri ISOs are date-stamped
snapshots. Bump ISO_BASENAME (navi-2.0-eiri-YYYY-MM-DD) + EIRI_BUILD
in .github/workflows/iso-eiri.yml and push — the push trigger fires
the build (workflow_dispatch does NOT work while the file lives on eiri
only). publish-eiri.yml publishes the build under the rolling eiri tag
(moved via API). A mika image from eiri needs an eiri→main merge that
explicitly sets wired/VERSION for main — never inherit eiri's
2.0 "eiri" stamp.
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.
dunstctl history is busctl JSON: every D-Bus variant arrives wrapped as
{"type": "<sig>", "data": <value>} — mocks with plain strings passed
while real data crashed.python3 -c has two traps: no backslashes inside f-string expressions
(f"{x(\"y\")}" is a SyntaxError on our Python) and no bare return at
top level (module scope — use sys.exit()). Extract the -c body and run
it standalone against sample input.style.css is GTK CSS, not browser CSS. The parser rejects
comma-separated keyframe selectors — 0%, 100% { } fails with "Expected
closing bracket after keyframes block". One selector per keyframe rule.iso/stage.sh must copy EVERYTHING install.sh reads from $REPO_DIR.
If install.sh deploys from $REPO_DIR/<dir>, stage.sh must stage <dir>
into /opt/navi-iso and the installer's preflight must require it.
(Missing scripts/ once meant fresh ISOs silently skipped navi-update.)gh-push-mika overwrites remote files with LOCAL content (and stores
*.sh as mode 100755). A stale local tree once shipped an ISO package
list with the network stack silently dropped. Fetch + diff first, every
time. (Transient HTTP 422 "Tree SHA does not exist" at commit creation:
retry once before investigating.)mod-open.log); diagnose the live
output, not the mocks.floating_con. Any walker that filters
type == "con" silently drops every floating window — navi-switcher.py
did exactly this (the floating_nodes walk was there, but the type gate
threw each entry away before collection or recursion). Accept both
"con" and "floating_con" when walking windows.gaps has two forms; runtime changes must use the 4-arg one. sway/i3's
2-arg form (gaps inner 0) is the config-directive path — it only sets the
default for future workspaces and never re-arranges the live ones. Anything
that changes gaps at runtime (keybinds, scripts) must use the 4-arg form
(gaps inner all set 0), which applies per-workspace and re-arranges
immediately. The old Super+G/Super+Shift+G bindings silently did nothing
until windows moved to a fresh workspace — which initializes from the new
defaults.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.
${badgeFor()}
threw on a.source and stuck the whole store on "loading curated apps"
— one uncaught TypeError kills the first render.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.)
xvoidsx/enter-the-wired. Pages: index, roadmap, news, manual
(mirrored from wired/manual/ — update both copies together),
sponsors, labs, community./releases/latest, never a pinned tag.<details> collapsibles. When adding
a post, demote the 4th-newest.AGENTS.md is mirrored to the website as agents.html. After
changing this file, re-run build-agents-page.sh in the site repo and push
— the repo copy stays the source of truth.navivim.html (hand-written product page), navivim-handbook.html
(mirror of its HANDBOOK.md), navivim-agents.html (mirror of its AGENTS.md)
— all built by build-navivim-pages.sh in the site repo. The NaviVim repo
copies stay the source of truth.setup_navicode in install.sh (official apt repo, package
codium). The in-editor agent is Lain via hermes acp.markdown package. Make sure it's
installed somewhere that survives your environment rebuilds — a plain system
pip install may not be.navi's dictation stack is Voxtype with the Moonshine Base engine —
chosen for the lowest memory footprint and memory-bandwidth cost of
Voxtype's engines; Parakeet streaming was considered and deferred.
Scroll_Lock push-to-talk lives in wired/sway/config (compositor owns
the key; the daemon runs with [hotkey] enabled = false and
state_file = "auto"; Moonshine needs the ONNX variant,
voxtype setup onnx --enable). New installations should ship Voxtype
installed and enabled with Moonshine Base preconfigured so dictation
works out of the box. Open thread: Hey Lain's STT is faster-whisper
today — Moonshine is a candidate replacement, but bench it on real
hardware before switching her over.
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.