AGENTS.md · diff
git:20260930.eb28122 to git:20261001.1f0bbae
31 added, 26 removed. Audit A to A.
# AGENTS.md — vaultmem
Cold-agent map for this repo. `CLAUDE.md` is a symlink to this file; Codex,
Cursor, Copilot, and Gemini read `AGENTS.md` directly.
## What this is
`vaultmem` is a single POSIX/bash script (`./vaultmem`, ~2800 lines) that
turns a plain-markdown [Obsidian](https://obsidian.md) vault into agent
memory: search (ripgrep-backed), a curated Agent Index, a wikilink graph, and
a Project→Session lifecycle tier with a Task backlog feeding it. No daemon, no
database, no build step — the script *is* the artifact. This repo also ships:
- `install.sh` — copies `vaultmem` to `~/.local/bin` (or symlinks
`skills/` into a harness skills dir with `--skills <dir>`).
- `skills/` — four Claude-Code-flavored agent skills, split by agent job:
`vault-recall` (check the vault before re-deriving), `session` (cross-session
working memory), `vault-capture` (write durable knowledge, incl. repo
onboarding), and `vault-curate` (what to write next / vault health). They
drive the memory workflow on top of the CLI.
- `.claude-plugin/` — projects `skills/` into an installable Claude Code
plugin/marketplace.
- - `ext/judge/` — the optional `judge` extension (`vaultmem-judge`, bash 3.2 +
+ - `ext/jev/` — the optional `jev` extension (`vaultmem-jev`, bash 3.2 +
`curl` + `jq`): typed yes/no, choice, and score answers from a remote
evaluation model. A separate executable, never sourced by core. See
§ The extension model.
- `tests/` — bats unit tests + a skills↔CLI drift lint.
Read the script top-to-bottom before making non-trivial changes — that's the
point of it being one file (see README § Design notes).
## Build / test / validate
No build step (bash script, nothing to compile). Before committing:
```bash
bats tests/vaultmem.bats # unit tests over the CLI (registry, search, graph, lifecycle)
d=$(mktemp -d) && ln -s /bin/bash "$d/bash" && PATH="$d:$PATH" bats tests/vaultmem.bats # …again under macOS system bash 3.2 (see below)
./tests/subcommand-lint.sh # every `vaultmem <cmd>` referenced in skills/**/*.md must exist in the dispatch table
./tests/bash32-lint.sh # no bash-4-only constructs (shellcheck cannot see these)
shellcheck vaultmem install.sh tests/subcommand-lint.sh tests/bash32-lint.sh
shfmt --diff vaultmem install.sh tests/subcommand-lint.sh tests/bash32-lint.sh # must produce no diff
```
This is exactly what CI (`.github/workflows/ci.yml`) runs, split into four jobs:
`lint` (shellcheck + shfmt + the bash-3.2 construct lint), `test` (bats, on
Ubuntu + macOS), `bash32` (bats under macOS `/bin/bash` 3.2), and
`subcommand-lint`. All four must pass before merge.
**Write for bash 3.2, not for the bash on your `$PATH`.** macOS ships bash
3.2.57 as `/bin/bash` (frozen 2007; bash 4+ is GPLv3) and that is what the
`SessionStart` hooks run under, but most contributors have Homebrew bash 5.x —
so `declare -A`/`local -A`, `mapfile`/`readarray`, `${x^^}`, `|&`, `&>>`, and
`coproc` pass locally and break on macOS. Both instances that reached `main`
failed *silently* (a `doctor --deep` that exited 0 finding nothing; a lint that
passed vacuously). `shellcheck` cannot detect this — it targets a dialect, not a
bash version. Full rationale, the banned list, and portable (faster) substitutes:
[docs/development.md](docs/development.md).
Tests never touch a real vault — `tests/vaultmem.bats` builds a throwaway
two-vault registry per test via `VAULTMEM_CONFIG` pointed at a `bats`
tempdir. Follow that pattern (isolated `VAULTMEM_CONFIG` + fixture vaults
under `$BATS_TEST_TMPDIR`) for new tests; never point a test at
`~/.config/vaultmem/config.toml` or a real vault path.
## The CLI subcommand surface
`vaultmem -h` (or the top-of-file comment block, lines ~12–40) is the
authoritative usage summary — read it before adding or renaming a
subcommand. Broad shape: search/index (`<query>`, `index`, `mocs`), the
wikilink graph (`resolve`/`links`/`backlinks`/`neighbors`/`dangling`), the
router (`vaults`/`path`/`which`), the lifecycle tier
(`sessions`/`projects`/`project`/`next`/`task`/`groom`/`status`), hygiene
- (`doctor`), setup (`init`), and the extension shim (`judge`). `groom --judge`
+ (`doctor`), setup (`init`), and the extension shim (`jev`). `groom --triage`
annotates its cold-parked/stale-active rows with a `groom-triage` judgment and
is advisory only: judging runs after the scan, so it never reaches a move.
- `nudge --judge` adds a second Stop-hook line when `capture-worthy` finds a
+ `nudge` adds a second Stop-hook line when `capture-worthy` finds a
durable decision/root cause in the transcript tail. `search --rerank` reorders
content hits (never curated rows) by the extension's `rerank` score; any
- failure prints plain ripgrep order. `doctor --judge` adds a `DRIFT` section
- from the `index-drift` judge (Agent-Index rows that no longer describe their
+ failure prints plain ripgrep order. `doctor --drift` adds a `DRIFT` section
+ from the `index-drift` set (Agent-Index rows that no longer describe their
note).
**Adding usage lines? Bump the `sed` range.** The usage block is printed by a
hardcoded `sed -n '4,Np' "$0"` in *two* places (the `-h` branch and the no-args
branch). Add lines to the comment block without bumping `N` and the help output
is silently truncated — no error, no test failure unless you pin it. A bats test
("usage output ends with the final usage comment line") asserts the last line
still renders; keep it pointed at whatever the real last line is.
Subcommands dispatch from the final `case "${ARGS[0]:-}" in` block at the
bottom of the script. **Any subcommand a skill's markdown references by name
must exist there** — `tests/subcommand-lint.sh` enforces this in CI, so a
renamed or removed subcommand that skills still mention fails the build, not
a user's session.
## The config/registry model
Two related but distinct contracts — don't conflate them:
- **Registry** (`~/.config/vaultmem/config.toml`, a restricted TOML subset
parsed by hand-rolled awk) — *which vaults exist* and how `vaultmem which`
routes a repo/cwd to one. Full key reference, accepted/rejected TOML shape,
and the routing algorithm: [docs/config.md](docs/config.md).
- **Schema** ([SCHEMA.md](SCHEMA.md)) — what a compliant vault looks like
*inside*: folder layout, frontmatter fields the tool reads, the status
vocabulary (`active`/`parked`/`done`), status-glyph filename convention,
and the Agent-Index row grammar.
The config parser is deliberately not full TOML — `vaultmem doctor` hard-errors
on anything outside the accepted subset (arrays, nested tables, unquoted
strings, etc.) so a malformed config fails loudly instead of silently
mis-routing. `groom` moves files between folders on disk; a silent mis-route
there is the failure mode the lint exists to prevent. If you touch
`_parse_config`/`_lint_config` in `vaultmem`, keep both docs and the bats
`doctor hard-errors on …` tests in sync with the accepted-subset rules.
## The hooks model
`vaultmem status` and `vaultmem sessions` are designed to run from an agent
harness's `SessionStart` hook (Claude Code `settings.json`, Codex
`hooks.json`) and are **fail-quiet by contract**: an unmounted/unconfigured
vault makes them exit `0` printing nothing, so a hook wired with a
`command -v vaultmem >/dev/null &&` guard can never break a session start.
Full wiring examples for both harnesses, and how to customize the printed
`directive_file` line: [docs/hooks.md](docs/hooks.md). Never change
`cmd_status`/`cmd_sessions` to error on a missing vault — that contract is
load-bearing for every downstream hook config. `nudge` (Stop hook) holds the
- same contract, and `nudge --judge` keeps it on every judge failure path: it
+ same contract, and `nudge` keeps it on every Jev failure path: it
prints nothing extra and exits `0`.
## The extension model
**Core opens no socket.** `./vaultmem` never makes a network call and never gains
a dependency; anything that needs either lives in an extension, a separate
- executable under `ext/<name>/vaultmem-<name>`. `judge` is the only one, and
- `judge` is the only dispatch word extensions take: do not add git-style "any
+ executable under `ext/<name>/vaultmem-<name>`. `jev` is the only one, and
+ `jev` is the only dispatch word extensions take: do not add git-style "any
`vaultmem-<x>` on `$PATH` is a subcommand", because search is the default
dispatch case and that makes every query ambiguous.
- `_ext_exec <name> [args]` resolves the executable in a fixed order:
`$VAULTMEM_EXT_DIR/<name>/`, then
`${XDG_DATA_HOME:-~/.local/share}/vaultmem/ext/<name>/`, then `ext/<name>/`
beside the symlink-resolved script. Never `$PATH`. Not found: one stderr line,
exit 3. It exports `VAULTMEM_BIN` (absolute, symlink-resolved) and
`VAULTMEM_CONFIG`; the extension reads vault data only by calling
`"$VAULTMEM_BIN"`, never by parsing the registry.
- - Args after `judge` pass to the extension verbatim: the arg loop stops parsing
+ - Args after `jev` pass to the extension verbatim: the arg loop stops parsing
core flags (`--vault`, `--format`, `-n`, `-h`) once the first positional is
- `judge`.
- - `_judge <judge-name> <vault-id> [args]` is the helper integrated commands
+ `jev`.
+ - `_jev <set-name> <vault-id> [args]` is the helper integrated commands
will call (no caller yet). State on stdin. It returns 3 **without forking**
- when `[ext.judge] enabled` is not `true` or the vault lacks `judge = true`, so
- a disabled judge costs a hook nothing. Callers treat anything but 0/1/2 as
- "no opinion" and print what they print today. The undocumented `_judge`
+ when `[ext.jev] enabled` is not `true` or the vault lacks `jev = true`, so
+ a disabled Jev costs a hook nothing. Callers treat anything but 0/1/2 as
+ "no opinion" and print what they print today. The undocumented `_jev`
dispatch word exists only so bats can reach the helper.
- - **`vaultmem judge config` is a frozen contract, answered by core**, never
+ - **`vaultmem jev config` is a frozen contract, answered by core**, never
forwarded. One line each, values unquoted, defaults filled in, in this order:
- `ext.judge.enabled`, `.model`, `.base_url`, `.zdr`, `.timeout_ms`,
- `.key_file` (`~` expanded), `.log`, `.rerank`, `.hook_judges`, then any other
- `[ext.judge]` key in file order, then `vault.<id>.judge=true|false` for every
+ `ext.jev.enabled`, `.model`, `.base_url`, `.zdr`, `.timeout_ms`,
+ `.key_file` (`~` expanded), `.log`, `.rerank`, `.hooks`, then any other
+ `[ext.jev]` key in file order, then `vault.<id>.jev=true|false` for every
registry vault with a path. It must work with no config file and no vault
- (`judge` is in the pre-dispatch allowlist). The extension's tests stub
+ (`jev` is in the pre-dispatch allowlist). The extension's tests stub
`VAULTMEM_BIN` with a script printing this format, so changing it breaks the
extension silently. A bats test pins it byte for byte.
- Core lints `[ext.<name>]` for subset shape only; key names belong to the
- extension (`vaultmem judge doctor`). `[vault.<id>] judge` must be a bare
- boolean.
+ extension (`vaultmem jev doctor`). `[vault.<id>] jev` must be a bare boolean.
+ - The pre-jev names (`vaultmem judge`, `groom/doctor/nudge --judge`,
+ `[ext.judge]`, `[vault.<id>] judge`, `hook_judges`, `calibration --judge`) are
+ accepted until the next breaking release and each print one stderr line naming the replacement.
+ Hook paths keep stdout untouched, and `doctor` reports a deprecated key as a
+ `WARN` without moving its exit code. The old user-override dir and log path are
+ not migrated; see the CHANGELOG migration table.
- Design and rationale: [docs/design/judge-extension.md](docs/design/judge-extension.md).
+ Design and rationale: [docs/design/jev-extension.md](docs/design/jev-extension.md).
## Skills → Claude Code plugin
`skills/` ships three agent skills; `.claude-plugin/{marketplace.json,plugin.json}`
project them into an installable Claude Code plugin (`/plugin marketplace add
jayantak/vaultmem`) where each skill appears namespaced (`vaultmem:session`,
etc.). Non-Claude-Code harnesses skip the manifests and use
`install.sh --skills <dir>` to symlink `skills/<name>/` directly. How
discovery works, what each manifest file is for, and how skill content stays
version-locked to the CLI: [docs/plugin.md](docs/plugin.md).
## Non-obvious gotchas
- **The bash floor is 3.2 (macOS), and violations fail SILENTLY.** bash 3.2
errors on `local -A`, then parses the next `arr[$path]=1` as an *indexed*
assignment with an arithmetic subscript — so `doctor --deep` sprayed syntax
errors per note and still exited 0. `mapfile` inside a process substitution
doesn't trip `set -e`, which left `subcommand-lint.sh` passing on an empty
list. Neither is visible on Homebrew bash 5, and neither is detectable by
`shellcheck`. Run the suite with a `bash` → `/bin/bash` symlink first on
`PATH` (the command above) + `./tests/bash32-lint.sh`. `BASH=/bin/bash bats …`
does nothing: bash resets `$BASH` at startup, so the suite still runs on
bash 5. See [docs/development.md](docs/development.md).
- - **`doctor --judge` is informational and must never affect the exit code.**
- `DRIFT` rows print under their own heading; `doctor --judge` exits exactly
+ - **`doctor --drift` is informational and must never affect the exit code.**
+ `DRIFT` rows print under their own heading; `doctor --drift` exits exactly
what `doctor` exits, so a probabilistic lint never breaks CI or a hook.
- **`shellcheck` disables at the top of `vaultmem` are load-bearing, not
boilerplate** — SC2016 (backticks in the usage/help text are literal, not
command substitution) and SC2012 (the MOC listing intentionally uses `ls`
over plain filenames). Don't blanket-remove them to "clean up" a lint pass.
- **`shfmt --diff` is part of CI**, separate from shellcheck — a change that
passes shellcheck can still fail CI on formatting. Run `shfmt -w vaultmem`
(or the specific file) before committing if unsure.
- **Status glyphs are presentation, never identity.** A Session's `project:`
frontmatter field and a Project's session-index `[[wikilink]]` are always
the *plain* name; the leading emoji (🟢/💤/✅/🔴/🟡) lives only on
filenames and H1s. Matching logic strips the glyph before comparing — see
SCHEMA.md § Status-glyph invariants.
- **`updated:` frontmatter, not file mtime, drives staleness math** (cold-parked
detection, sort order) — file mtime gets rewritten by cloud-drive re-sync
and would corrupt it. Mtime is only a fallback when `updated:` is
missing/unparseable.
- **This repo is generated payload for some consumers.** `skills/*/SKILL.md`
files carry a `<!-- GENERATED from the private dotfiles source repo -->`
marker — they're synced in from an external private source, not
hand-authored here. Edit them here for this repo's purposes (they must
stay accurate and pass `subcommand-lint.sh`), but know a downstream sync
process — not this repo — is their canonical origin.
## Where deeper truth lives
| Topic | Doc |
|---|---|
| Registry/config keys, TOML subset, routing algorithm | [docs/config.md](docs/config.md) |
| Vault folder layout, frontmatter contract, Agent-Index grammar | [SCHEMA.md](SCHEMA.md) |
| SessionStart hook wiring (Claude Code + Codex), directive customization | [docs/hooks.md](docs/hooks.md) |
| Claude Code plugin packaging, skill discovery, version sync | [docs/plugin.md](docs/plugin.md) |
| Install paths, quickstart, full command reference, design rationale | [README.md](README.md) |
- | Judge extension: setup, egress rules, judge files, exit codes | [docs/judge.md](docs/judge.md) |
- | Judge extension design, gateway findings, build order | [docs/design/judge-extension.md](docs/design/judge-extension.md) |
+ | Jev extension: setup, egress rules, set files, exit codes | [docs/jev.md](docs/jev.md) |
+ | Jev extension design, gateway findings, build order | [docs/design/jev-extension.md](docs/design/jev-extension.md) |
| Skill content itself (workflows, conventions each skill teaches) | `skills/<name>/SKILL.md` |
| bash 3.2 floor, banned constructs, portable substitutes, test conventions | [docs/development.md](docs/development.md) |