AGENTS.md · git:20261026.bbb9d9f · 2026-10-26 · sha256 a72d973c4afc8c6f
AGENTS.md git:20261026.bbb9d9fA
Immutable. This exact content never changes and is served at /api/v1/blob/a72d973c4afc8c6f.
# Kluris 3 (branch `v3`)
Branch `v3` is the Kluris 3 Rust rewrite. Kluris 2 (Python, PyPI `kluris`) lives
on `main`; its v2.28.11 tag is the frozen reference for everything here.
Kluris turns AI agents into team subject matter experts by giving them shared,
human-curated knowledge stored in git-backed **brains**. Kluris 3 keeps every v2
command except `pack` and reads and writes existing brains, configs and agent
skills exactly as v2 does.
## Knowledge base
Read and write to the **ngvoicu-sme** brain through kluris (never edit brain
files by hand). Use the `/kluris-ngvoicu-sme` skill — search, learn, remember,
create.
Topics relevant to this repo: kluris-cli overview, architecture, multi-brain resolver, agent skills, wake-up protocol, testing, CI/CD, deprecation.
## Layout
```
Cargo.toml # workspace: crates/kluris-core, crates/kluris-cli, xtask (edition 2024)
rust-toolchain.toml # pinned toolchain (rustfmt, clippy)
.cargo/config.toml # `cargo xtask ...` alias
crates/kluris-core/ # library: every operation, typed inputs and outputs, no printing
crates/kluris-cli/ # package `kluris`, binary `kluris`: parses arguments, calls core, prints
xtask/ # dev tasks (cargo xtask; windows and linux runners share ssh_ci/)
vendored/ # the four Specmint companion folders (unchanged from v2)
parity/ # uv project: the v2.28.11 oracle vs the v3 binary
dist-workspace.toml # dist 0.33.1: targets, installers, archives (see Release)
about.toml, about.hbs # cargo-about config and template of THIRD_PARTY_LICENSES.md
scripts/release/ # release tooling: pinned tools, licence report, local build, installer smoke
.github/workflows/ # ci.yml, parity.yml, release.yml (dist's own)
```
Rules: synchronous code, no async runtime; no `unwrap()`/`expect()` on user
data, errors are values; cross-platform (`std::path`, Unix-only APIs behind
`cfg`).
## Build & Test
```bash
cargo build --release
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --check
KLURIS_V3_BIN=target/release/kluris uv run --project parity pytest parity -q
```
The parity harness builds the oracle venv (Python 3.11, `parity/oracle.lock`,
hash-pinned) once into `parity/.oracle/` and reuses it while the lock is
unchanged. Without `KLURIS_V3_BIN` every parity test fails (it never skips).
## Windows machines (`cargo xtask windows`)
Kluris 3's tests and parity harness run on the two household Windows
machines over ssh, the way ConsensFlow's runner does (`xtask/src/windows.rs`
has the whole design):
```bash
cargo xtask windows --machine zeewin --parity # cargo test --workspace --no-fail-fast, then parity
cargo xtask windows --machine zeewin2 -- -p kluris-core # extra cargo test args after --
cargo xtask windows --parity # zeewin2 if free and allowed, else zeewin
cargo xtask windows --parity --parity-files parity/test_smoke.py # only that parity file
cargo xtask windows --machine zeewin --installer-smoke --wrapper ../sites/kluris-site/install.ps1
cargo xtask windows --clippy # cargo clippy --workspace --all-targets -- -D warnings, no tests
```
- **Machines.** `zeewin` (AGV-COMPUTE1, `ssh zeewin`, short of memory: builds
run with 2 jobs) and `zeewin2` (the owner's ASUS laptop, `ssh zeewin2`, 8
jobs). Facts: zeespirevpn-sme (`devices/agv-compute1.md`,
`devices/agvlaptop.md`) and consensflow-sme (`operations/test-machines.md`).
- **What goes.** `git archive` of HEAD, or of a `git stash create` commit when
tracked files have uncommitted changes (untracked files do not go), without
`target/`, `parity/.oracle/`, `parity/.venv/`.
- **One run per machine, shared with ConsensFlow.** A run takes ConsensFlow's
lock, `%USERPROFILE%\consensflow-build\.windows-run.lock` (opened for the run
alone, ConsensFlow's `.windows-run.pid`/`.windows-run.id` records beside it),
before anything else. A held lock ends the command at once with **75**,
naming the holder (a Kluris or a ConsensFlow run, its id and process): it
never waits, takes or breaks it.
- **zeewin2 is refused (75)** while poker-lab's `~/.compile-lock/owner` exists
on `zeelin` (a zeelin that does not answer is an Ubuntu that is not running,
and the check passes) and while C: has under 50 GiB free. That lock is only
looked at: never taken, broken or waited on. With no `--machine`, a refused
or busy zeewin2 sends the run to zeewin.
- **Nothing on the machine changes.** No Windows settings, Defender, firewall,
registry or PATH (user or machine) changes, no logins, no system-wide
installs. Everything Kluris adds lives in `%USERPROFILE%\.kluris-ci\`:
`tools\rustup` (`RUSTUP_HOME`: the tree's pinned toolchain, installed by the
machine's own `~\.cargo\bin\rustup.exe` called by full path), `tools\cargo`
(`CARGO_HOME`), `tools\uv-0.12.8\uv.exe` (fetched once, SHA-256 checked),
`tools\python` (uv's managed CPython), `cache\target` (`CARGO_TARGET_DIR`:
registry deps stay built; the workspace crates rebuild in each run's new
folder), `cache\oracle` (`KLURIS_PARITY_ORACLE_DIR`), `cache\parity-venv`,
`cache\uv`, and `runs\<run id>\` (the unpacked tree, its `tmp\`). PATH is
prepended for the run's own process only. A machine without rustup (or the
VS Build Tools linker) is reported, not fixed. The one exception is
`--installer-smoke`, whose test is the installer's user PATH edit: the
smoke saves `HKCU\Environment\Path` (bytes and registry type,
`scripts\release\user-path.ps1`, into `runs\<id>\user-path.saved`),
writes it back however it ends (the run does again after a step ended past
its bound, and so does the stop) and compares it byte for byte; the system
PATH is only read. kluris.exe and the receipt go to a sandbox profile.
- **Bounds and cleanup.** The run is in a job object: at most 64 processes,
all ended when the run ends. Each step has its own bound (toolchain 30 min,
cargo-test 90, release-build 60, parity 90; a step past it is ended, status
124), the whole run one more (this side kills ssh and sends the stop), and
every probe a few seconds. The bootstrap's `finally` always removes the
run's tarball and its lock records and closes the lock (after a failed
upload, a 30-second ssh with no lock removes that run's own tarball and says
whether it could); run folders older
than a day and stray `kluris-*.tgz` are swept by the next run, under the lock.
Ctrl-C ends the run there too (130).
- **The answer.** The output streams back; the status is the machine's last
line `== exit N` (ssh to PowerShell answers 1 for every failure), then a
summary line with each step's status and time.
- **Parity on Windows.** v2.28.11 runs there as the oracle (the hash-pinned
`parity/oracle.lock` has the `win_amd64` wheels) against the release
`kluris.exe` (`KLURIS_V3_BIN`). The harness reads CRLF in stdout/stderr as
LF (PAR-004). `--parity-files parity/test_x.py[,parity/test_y.py]`
(repeatable, needs `--parity`) runs `pytest` on those files alone instead
of the whole suite: each must be a `parity/*.py` file in this worktree,
checked before anything is sent.
## Linux machine (`cargo xtask linux`)
The same tests and parity harness run on `zeelin`, the household laptop's
Ubuntu 26.04 under WSL2 (the same laptop as zeewin2), over ssh
(`xtask/src/linux.rs` has the whole design; the upload, the streamed run and
the stop are shared with `windows` in `xtask/src/ssh_ci/`):
```bash
cargo xtask linux --parity # cargo test --workspace --no-fail-fast, then parity
cargo xtask linux -- -p kluris-core # extra cargo test args after --
cargo xtask linux --parity --parity-files parity/test_smoke.py # only that parity file
cargo xtask linux --installer-smoke --wrapper ../sites/kluris-site/install.sh
cargo xtask linux --clippy # clippy -D warnings on every target, no tests
```
- **The machine.** `ssh zeelin` (10.6.0.6:22, user `zee`, bash; 16 threads,
27 GiB). It answers only while WSL runs (a scheduled task keeps it up while
the owner is logged in). It is poker-lab's compute box first. Facts:
zeespirevpn-sme (`devices/agvlaptop.md`) and consensflow-sme
(`operations/test-machines.md`). Builds run with 8 jobs.
- **Refused with 75, nothing sent**, by a bounded probe (5 s to connect, 30 s
in all) and again by the bootstrap under its lock: zeelin not answering
("zeelin not running": it is never started from here; an ssh that connected
but could not say is an error, 1), poker-lab's `~/.compile-lock/owner`
present, a run holding zeewin2 (its record
`/mnt/c/Users/AGV/consensflow-build/.windows-run.id`, written by ConsensFlow's
and Kluris's `cargo xtask windows`, or `/mnt/c/Users/AGV` out of sight), and
Kluris's own one-run lock `~/.kluris-ci/.linux-run.lock` held (`flock -n`,
the holder's id and process in `.linux-run.id` beside it). Each refusal
names the holder. The other projects' locks are only looked at: never
taken, broken or waited on. They are checked when the run starts: a
poker-lab job or a Windows run that starts later runs beside it.
- **What goes.** The same tarball as for Windows (HEAD, or a `git stash
create` commit with the uncommitted tracked changes; not `target/`,
`parity/.oracle/`, `parity/.venv/`), by scp to
`~/.kluris-ci/incoming/<run id>.tgz`.
- **Nothing outside `~/.kluris-ci/` changes.** No `sudo`, no system packages,
no shell profiles, nothing of poker-lab's. `tools/rustup` (`RUSTUP_HOME`:
the tree's pinned toolchain, installed by the user's own
`~/.cargo/bin/rustup` called by full path; `~/.cargo/env` is never read),
`tools/cargo` (`CARGO_HOME`), `tools/uv-0.12.8/uv` (fetched once, SHA-256
checked), `tools/python` (uv's managed CPython), `cache/target`
(`CARGO_TARGET_DIR`), `cache/oracle` (`KLURIS_PARITY_ORACLE_DIR`),
`cache/parity-venv`, `cache/uv`, `cache/xdg`, and `runs/<run id>/` (the
unpacked tree, its `tmp/` as `TMPDIR`). PATH changes for the run's own
process only. A zeelin without rustup is reported, not fixed.
- **Bounds and cleanup.** The bootstrap runs in a session of its own
(`setsid`) under `set -euo pipefail`; its `trap` ends every other process of
the session and removes the tarball and the record however the run ends,
and the lock goes with its descriptor. `ulimit -u 1024` caps the user's
tasks while the run goes (Linux counts threads and every process of `zee`,
so it is a ceiling against runaway forks, not the Windows job's 64). Each
step has the Windows bounds (toolchain 30 min, cargo-test 90, release-build
60, parity 90) under `timeout` (TERM, then KILL 30 s later; status 124), and
the whole run one more. Ctrl-C (130), the bound (124) and a lost
connection send the stop, which ends the run's session only if the record
names that run. Run folders and tarballs older than a day are swept by the
next run, under the lock.
- **The answer.** As on Windows: the output streams back, the status is the
run's last line `== exit N`, then a summary line `linux: zeelin run <id>
tree <commit>: <step> <status> (<time>), ...; exit N`.
- **Parity on Linux.** v2.28.11 runs there as the oracle (the hash-pinned
universal `parity/oracle.lock`) against the release `kluris`
(`KLURIS_V3_BIN`); `--parity-files` as for Windows.
- **zeelin is WSL2, not native Linux.** v2's and v3's WSL code paths run
there (`mri` prints `file://wsl.localhost/<distro>/...` links, built with
`wslpath -w`; `parity/test_mri.py` expects that form on a WSL host), so
behaviour only native Linux shows is first covered by GitHub's Ubuntu
runners at release.
## Release (dist)
Kluris 3 ships from GitHub Releases only (no PyPI, crates.io, Homebrew or
npm; both crates are `publish = false`), built by dist 0.33.1 from
`dist-workspace.toml`: eight targets (macOS arm64/x86_64, Linux x86_64 and
aarch64 as gnu and static musl, Windows x86_64/arm64), tar.gz/zip archives
with a `.sha256` each and one `sha256.sum`, GitHub attestations, and
`kluris-installer.sh` / `kluris-installer.ps1` into `~/.kluris/bin` (no
updater: `kluris self-update`). dist names everything after the Cargo
package, hence the package `kluris`.
- **Archive layouts** (what `kluris_core::dist::binary_in_archive` and
self-update follow): a `.tar.gz` holds everything in a top-level
`kluris-<target>/` folder; a Windows `.zip` is flat, `kluris.exe` at its
root. `--version` is the package's version (clap's `version`), so a
release prints its own.
- **Archives** hold `kluris`, `LICENSE`, `README.md`,
`THIRD_PARTY_NOTICES.md` (ported code) and `THIRD_PARTY_LICENSES.md` (the
crates built into `kluris`, from cargo-about). Auto-includes are off: dist
resolves the crate's `license-file = "../../LICENSE"` from the wrong folder.
After a dependency change run `scripts/release/licenses.sh` (CI runs it
with `--check`; a crate under a license not in `about.toml` fails it).
- **What the installers do** (0.33.1, read from the generated scripts): the
shell one checks the archive's SHA-256, writes `~/.config/kluris/env.sh`
and `env.fish` beside the receipt `~/.config/kluris/kluris-receipt.json`,
and adds `. "$HOME/.config/kluris/env.sh"` to `~/.profile` and `~/.zshrc`
(or the existing bash/zsh rc files) plus fish's `conf.d/kluris.env.fish`;
the PowerShell one checks no checksum, prepends to `HKCU\Environment\Path`
as REG_EXPAND_SZ and writes the receipt to `%LOCALAPPDATA%\kluris\`
(one line, CRLF, no BOM; `binaries` `["kluris.exe"]`, `install_prefix` in
backslashes, the same text it puts first in PATH). It rebuilds the user
PATH as `-split ';' -ne ''`, so empty entries (a trailing `;`) are gone and
a REG_SZ value becomes REG_EXPAND_SZ; `kluris self-uninstall` then removes
only its own entry and cannot bring those back.
- **release.yml is generated:** change `dist-workspace.toml`, then
`dist generate`; `dist plan` fails while it is stale.
- **Its tag trigger is dist's own** (`'**[0-9]+.[0-9]+.[0-9]+*'`, decided
2026-10-26), not `v3.*`: dist cannot generate `v3.*` (`tag-namespace =
"v3"` makes a glob that misses `v3.0.0`), and a hand edit would turn off
its drift check. v2 and v3 stay apart by tree instead, as GitHub runs the
workflows of the tagged commit: v2.29.0 is tagged on a Python commit
before `main` becomes Rust and later v2 fixes on branch `v2`, which has no
release.yml, so they run v2's publish.yml; this tree has no publish.yml,
so a v3 tag can never reach PyPI; a v2 tag pushed onto a v3 commit makes
`dist plan` fail ("nothing to release"), never a release.
`scripts/release/check_workflows.sh` (ci.yml) fails if publish.yml, or any
workflow publishing to PyPI, comes back.
- **Tools** (`scripts/release/tool.sh dist|cargo-about|actionlint`): the
pinned prebuilt binary, its SHA-256 checked, in `target/tools/`; nothing is
installed globally.
- **Checks** (ci.yml runs each): `dist plan --output-format=json` through
`scripts/release/check_dist_plan.py`; `scripts/release/build-local.sh`
(`.ps1` on Windows) builds this machine's release in release.yml's order
(the archive's manifest beside it, so the shell installer embeds its
SHA-256) into `target/distrib`; `scripts/release/installer-smoke.sh
[--wrapper <site install.sh>] target/distrib` (`.ps1` on Windows) installs
it from a local server in a sandbox HOME, and checks the rc lines, a fresh
login shell, `kluris --version` and `kluris doctor --json`.
- **The installed life cycle** (`cargo xtask windows|linux --installer-smoke`
runs it after the installer smoke): `scripts/release/build-versions.sh
target/releases 3.0.0 3.0.1` (`.ps1`: `-Versions 3.0.0,3.0.1`, one word)
builds the tree as two releases that differ only in version, and
`scripts/release/lifecycle-smoke.sh target/releases 3.0.0 3.0.1` (`.ps1`
on Windows) serves them with a fake GitHub release API (`serve.py`; plain
http is allowed to loopback in `KLURIS_GITHUB_API_URL`), installs 3.0.0
with its own installer, makes a brain, a companion and skills, then
checks `self-update --check --json`, `self-update --json` (the binary is
the new archive's, only the receipt's `version` changes), `--version`,
and `self-uninstall --yes --json` (exactly the installer's files and PATH
entry go; every other file, the seeded rc files included, is
byte-identical); then a KLURIS_NO_MODIFY_PATH=1 install over the user's
own env.sh and profile line (Windows: the user PATH), and on Unix the
uninstalls the pre-flight must block (an rc file it cannot rewrite, a
read-only folder in a purged ~/.kluris, a read-only or unsearchable
fish conf.d, on macOS an immutable receipt): exit 1, every file under
the sandbox HOME byte-identical, then a rerun that finishes. ci.yml's
installer-smoke job runs it on the three OSes (`check_workflows.sh` fails
if that goes).
- **A receipt authorises nothing unless it is Kluris's** (T-111):
`Receipt::read` (so `self-update` and `self-uninstall` alike) accepts only
dist 0.33.1's receipt for Kluris: `source` kluris/kluris/ngvoicu/github,
`provider.source` cargo-dist, `binaries` exactly `["kluris"]`
(`["kluris.exe"]`), no cdylibs, cstaticlibs or aliases, `install_layout`
flat, an absolute `install_prefix`, a boolean `modify_path`; anything
else is "The receipt at <path> is not a Kluris install receipt; nothing
was changed (<why>)", exit 1. The one file removed or replaced is
`<install_prefix>/kluris(.exe)`.
- **`self-uninstall` checks everything before the first change** (T-119,
`dist/preflight.rs`): the whole plan (rc edits, the fish file, env
scripts, under `--purge` every generated skill and ~/.kluris, the
binary, the receipt, their emptied folders) is built from reads alone,
where only NotFound means "not there"; `preflight()` then checks it
without changing anything: rc files readable and writable and their
real folder (a link's target's) writable, every folder an entry leaves
writable and searchable, every folder of a removed tree readable,
writable and searchable (`faccessat` with `AT_EACCESS`, no probe
writes), no `uchg`/`schg`/append flag on macOS, no read-only attribute on
a Windows file. Anything in the way: exit 1, nothing changed, each
blocker in `blocked` (`{path, problem, fix}` in the JSON). Not checked:
Windows ACLs and Linux `chattr` flags.
- **`self-uninstall`'s order keeps what a retry needs**: with `modify_path`
false (KLURIS_NO_MODIFY_PATH=1) no rc file, env script, fish file or PATH
entry is touched. Otherwise: the rc lines first (a failure stops
everything), then the fish conf.d line, env.sh/env.fish and the Windows
PATH entry (a failure stops before the binary), then the binary, the
receipt last. env.sh/env.fish go only when they are byte for byte what
dist writes (`write_env_script_sh`/`_fish` around `$HOME/.kluris/bin`);
one the user changed is kept, with the lines sourcing it, and listed in
`kept`. A failure the pre-flight could not see stops before the binary
and a rerun finishes; once the binary is gone (by then no shell file
sources a removed script and the PATH entry is gone) the rest still
goes, exit 1, and `left` says what to remove by hand ("remove the file
<receipt>", "remove the empty folder ~/.kluris/bin").
- **Checksums.** dist's shell installer verifies the archive with
`sha256sum` only and silently skips it without one (macOS 14 and older;
Apple added `sha256sum` in macOS 15). The site's `install.sh` then puts
a temp `sha256sum` that is `exec shasum -a 256 "$@"` first on PATH for
the installer alone, and refuses with neither command;
`installer-smoke.sh --wrapper` tests it with `sha256sum` off PATH and a
corrupted archive. **dist's PowerShell installer checks no checksum at
all**: a known dist 0.33.1 limitation, not fixed here (the download is
over https from GitHub; `kluris self-update` does check `sha256.sum`).
- **Site wrappers** live in `../sites/kluris-site` (`install.sh`,
`install.ps1`): thin pass-throughs to `releases/latest/download/` or
`releases/download/v$KLURIS_VERSION/`; a push there deploys the site.
## Parity rule
Byte parity with v2.28.11 for `--json` output, exit codes, files written into
brains, `~/.kluris/config.yml`, brain `kluris.yml` and SKILL.md files.
Human-readable (non-JSON) output keeps v2's wording, not its exact layout.
Every deliberate difference goes into `parity/allowlist.md` with the decision
it comes from; an unlisted difference is a bug.
## Design decisions
They live in the spec at
`/Users/gabrielvoicu/Projects/ngvoicu/kluris/.specs/kluris-v3-rust/` (local to
the `main` checkout, not in git): `SPEC.md` plus the research files
(`research-03-v2-contract.md` is the v2 command contract with file:line
references into v2.28.11). The v2 source on `main` is the read-only reference.