Skip to content

feat(installer): add Codex CLI parity - #129

Draft
JagoGG wants to merge 320 commits into
mainfrom
codex/codex-cli-parity
Draft

feat(installer): add Codex CLI parity#129
JagoGG wants to merge 320 commits into
mainfrom
codex/codex-cli-parity

Conversation

@JagoGG

@JagoGG JagoGG commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • install the four vkm skills and a TOML vkm-implementer agent for Codex CLI
  • add hash-tracked, idempotent Codex hooks for vault context, memory writes, effort gating, and token compaction
  • extend update/uninstall, flags, README, changelog, regression coverage, and the bilingual Codex parity hand-off

Drift documented

Codex does not currently support updatedMCPToolOutput; the token saver returns compacted PostToolUse feedback with continue: false. The unstable transcript_path dependency for the effort gate and the /hooks trust review remain explicit follow-ups.

Validation

  • npm run sync-agents:check
  • npm run lint
  • npm run typecheck
  • npm test
  • npm test --workspace @vkmikc/create-vkm-kit -- --test-name-pattern='Codex'

Vahlame and others added 30 commits June 14, 2026 19:04
The @Vahlame org scope is not registered on npm, so publishing
@vahlame/create-obsidian-memory 404'd (Scope not found). Renamed the
initializer to the maintainer's personal scope @vkmikc/create-obsidian-memory
across package.json, README, src header, all install/troubleshooting/faq docs,
legacy guides and the npm-create short form (npm create @vkmikc/obsidian-memory).
Regenerated package-lock.json. The private @vahlame/obsidian-memory-mcp package
is untouched (never published, run from the clone).

Verified: prettier clean, markdownlint 0, sync-agents OK, linkcheck 76,
npm test 16+41 pass. Tarball: @vkmikc/create-obsidian-memory@3.0.0 (4 files).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(npm): rename initializer scope @Vahlame → @vkmikc for publish
…pe consistency

Unifies the npm scope: the public initializer is already @vkmikc, so the
private @vahlame/obsidian-memory-mcp -> @vkmikc/obsidian-memory-mcp (package.json
name + the historical CHANGELOG reference). Package stays private (never
published; run from the clone via file path), and the MCP server registration
name 'obsidian-memory-hybrid' is unchanged. Regenerated package-lock.json.

Verified: prettier clean, markdownlint 0, sync-agents OK, linkcheck 76,
npm test 16+41 pass. Only remaining @Vahlame reference is the intentional
'renamed from' note in CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor(npm): rename private mcp package @Vahlame → @vkmikc for scope consistency
…-kit

The kit is IDE-agnostic (Cursor, Claude Code, ...); the old slug implied
'Cursor only'. Renamed the GitHub repo and updated every in-repo reference:
clone URLs, source-verification blocks, README badges, package.json
repository/homepage, the Go module path (github.com/Vahlame/obsidian-memory-kit),
agent.toml, SECURITY.md, .lycheeignore, issue-template config, examples.
GitHub redirects the old URLs; the local clone folder name is unaffected.

Also documents concurrent edits (Obsidian open while the agent writes) in the
sync guide: MCP writes are atomic (temp+rename), dynamic logs are append-only
and agent-owned, git rebase is the conflict backstop. Addresses the last stale
'Cursor-only' signal and the write-conflict question from external review.

Verified: prettier clean, markdownlint 0, sync-agents OK, linkcheck 76,
npm test 16+41, go test pass (new module path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refactor: rename repo → obsidian-memory-kit + concurrent-edit docs
…ault, -y, auto-create (3.1.0)

Make the npx/npm install far less tedious:
- Vault is now OPTIONAL: pass it as a positional arg (`npx ... ./vault -y`)
  or omit it to default to ~/Documents/cursor-memory-vault.
- `-y` is a new alias for --non-interactive/--yes.
- A missing vault is CREATED (starter notes) instead of erroring out.
- Interactive 'create sample' now uses the standard default location too.
- Rewrote --help with examples; old `-- --non-interactive --vault` still works.

Docs (README, install, install-with-agent, troubleshooting, package README)
now show the short form: `npx @vkmikc/create-obsidian-memory "<vault>" -y`.
Bumps the package to 3.1.0.

Tests: replaced the obsolete 'requires --vault' case with default-vault +
positional + auto-create coverage (17 pass). prettier/markdownlint/sync-agents/
linkcheck/npm test all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(create-obsidian-memory): simpler install — positional vault, default, -y, auto-create (3.1.0)
After de-cursoring the repo name, the initializer still defaulted the vault to
~/Documents/cursor-memory-vault — inconsistent. Renamed the default everywhere it
is suggested/created: initializer defaultVaultPath + prompts + --help, the daemon
systemd-unit fallback, docs (install, install-with-agent), README, CHANGELOG, and
the test. Now: ~/Documents/obsidian-memory-vault.

Left docs/adr/0008 untouched (historical record of a removed v1 script). Folds
into the still-unpublished 3.1.0 (no version bump).

Verified: go build + uncached test, npm test 17+41, prettier 0, markdownlint 0,
linkcheck 76.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e root daemon binaries

A stray `go build ./...` emitted the daemon binary at repo root and the previous
commit picked it up. Remove it and ignore /obsidian-memoryd[.exe].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix: default vault → obsidian-memory-vault (drop 'cursor' from the default)
…anti-noise)

Reinforce the canonical block (install.md/instalacion.md Step 4 — pasted into
Cursor User Rules or ~/.claude/CLAUDE.md) and the AGENTS.md memory protocol so
ANY agent extracts maximum value:

- Proactive-recall triggers: search the vault BEFORE answering when a task
  continues prior work, names a project/person/tool, repeats a question, or
  revisits a decision — don't wait to be told to 'check your notes'.
- Load MEMORY.md on non-trivial tasks (global prefs shape everything).
- Anti-noise save rule: store only what's reusable beyond the session; dedup
  first; never per-day TODOs/command output/what the code already documents —
  a noisy vault is one nobody reads.

Bilingual (ES+EN), kept portable/IDE-agnostic. prettier/markdownlint/sync-agents/
linkcheck all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs: strengthen memory User Rules — proactive recall + anti-noise (max value for any agent)
…nfigs (3.2.0)

The initializer can now install the memory-protocol rules — not just the MCP —
into ~/.claude/CLAUDE.md (global), ./AGENTS.md, and .cursor/rules/obsidian-memory.mdc.

- New --rules <claude|agents|cursor|all|none> + --no-rules.
- Idempotent MARKED block (<!-- obsidian-memory:start/end -->): merge replaces
  only between markers, else appends — NEVER clobbers the user's own content.
  Re-runs just refresh the block.
- Interactive asks (deriving targets from --ide); headless writes nothing unless
  --rules is passed (no surprise writes to global files in CI).
- Cursor GLOBAL User Rules can't be auto-written (not a file) — summary reminds
  the user to paste it; install.md Step 4 documents the shortcut.
- Single source for the block: src/memory-rules.mjs (ES+EN); merge engine:
  src/rules-merge.mjs. New unit + integration tests (24 pass).

Docs: package README + install.md/instalacion.md Step 4 + CHANGELOG. Verified:
prettier 0, markdownlint 0, sync-agents OK, linkcheck 76, npm test 24+41.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(create-obsidian-memory): auto-install memory rules into agent configs (3.2.0)
…onomy (3.3.0)

Make the memory loop learn and improve service without spending more tokens.
The rules block (memory-rules.mjs + install.md Step 4 + AGENTS.md) now adds:

- Self-check before non-trivial answers (assumptions / edge cases / what would
  make this wrong), scaled to the task, internal — no padding.
- Coach, don't impose: flag HIGH-IMPACT anti-patterns in the user's code as a
  QUESTION, log a one-line hypothesis to PRACTICES/observations.md, promote to
  confirmed-{good,bad}.md only when the user confirms.
- Evolving memory: track new tech in STACKS/ (one note each), record firm user
  preferences in MEMORY.md, hypotheses->facts, drop stale observations.
- Explicit token-economy guard on the whole loop: passage-first, terse bullets,
  dedup — smarts come from good notes + targeted recall, not re-reading.

Scaffold now creates PRACTICES/ + STACKS/; examples/ gains matching samples.
Rules kept dense (loaded every session). Bumps package to 3.3.0.

Verified: npm test 24+41, prettier 0, markdownlint 0, sync-agents OK, linkcheck 80.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat: evolving memory + self-critique + coaching, bounded by token economy (3.3.0)
The vault can be driven by different models that deduce what to do differently
(Claude Opus/Sonnet, Cursor Composer, GPT, DeepSeek, Gemini...). Make the
agent<->memory loop adapt to the active model's strengths AND learn which model
fits which task over time — without spending more tokens.

- New scaffolded _meta/agent-profiles.md: per-model default profiles (decision
  strength · lean-on-it-for · memory tuning) + an evolving Observations area
  (model · task type · what worked/failed). Defaults are framed as general and
  evolving — corrected by real observations.
- Rules block (memory-rules.mjs + install.md Step 4 + AGENTS.md) gains a SHORT
  pointer: read your own row, follow its tuning, append observations. Detail
  lives in the vault and is read passage-first (one row) — always-loaded rules
  stay lean, so the per-model layer is token-positive (a model that knows to be
  concise spends less).
- Honesty: profiles are high-level characterizations the system self-corrects
  via observations, not hardcoded claims about closed models.

Bumps package to 3.4.0. Verified: npm test 24+41, prettier 0, markdownlint 0,
sync-agents OK, linkcheck 81.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat: per-model adaptive memory + evolving model-profiles (3.4.0)
…(3.4.1)

The published npm page was a couple versions behind: it documented up to --rules
(3.2.0) but not the evolving-memory (3.3.0) or per-model-adaptive (3.4.0) layers.

- Add a 'What the installed memory does' section: passage-first recall, self-check,
  coach-don't-impose, evolving memory (STACKS/PRACTICES), model-aware profiles
  (_meta/agent-profiles.md), token economy.
- Fix the stale scaffold list (now includes PRACTICES/, STACKS/, _meta/).
- Link to install.md Step 4 (the rules block) so readers can review before installing.

Docs-only patch (no src/ change). Bumps package to 3.4.1 so the npm page updates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs(create-obsidian-memory): refresh npm README to 3.3/3.4 features (3.4.1)
…s/gitleaks-action-ff98106e4c7b2bc287b24eaf42907196329070c7

ci: Bump gitleaks/gitleaks-action from dcedce43c6f43de0b836d1fe38946645c9c638dc to ff98106e4c7b2bc287b24eaf42907196329070c7
…sh/setup-uv-38f3f104447c67c051c4a08e39b64a148898af3a

ci: Bump astral-sh/setup-uv from e4db8464a088ece1b920f60402e813ea4de65b8f to 38f3f104447c67c051c4a08e39b64a148898af3a
…/setup-python-6.2.0

ci: Bump actions/setup-python from 5.6.0 to 6.2.0
…5.0)

Use the [[wikilink]] graph the vault already encodes as a third retrieval signal. hybrid_search gains opt-in graph=True (--graph / graph:true on vault_hybrid_search) that fuses notes one hop from the strongest hits into the existing RRF, so a note linked from a strong hit surfaces even when its own text barely matches. The graph is parsed on demand from the always-fresh FTS bodies (no stale edge cache; O(N)/query). Each hit gains graph_rank.

Also: new vault_complete tool + complete/json-complete CLI (Trie prefix autocomplete over titles/filenames/#tags); search_chunks top-k via heapq.nlargest (O(n log k)). ADR-0019; how-it-works en/es, ARCHITECTURE, AGENTS, rag README; bump obsidian-memory-rag + obsidian-memory-mcp to 3.5.0. Tests: +test_graphlink, +test_complete (Python 52 pass, Node MCP 41 pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add two Mermaid diagrams to how-it-works (en/es): the three-ranker retrieval stack (lexical + semantic + graph -> RRF -> passage) and the link-expansion example, so old and new search features read at a glance. Bump @vkmikc/create-obsidian-memory 3.4.1 -> 3.5.0 and refresh its npm README (graph-aware recall + vault_complete). Update install guides (en/es) to list vault_complete + the graph option. CHANGELOG entry. Docs/version-alignment only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move CHANGELOG [Unreleased] -> [3.5.0] (2026-06-15) so release.yml can extract the section on tag; add the [3.5.0] compare link. Bump kit version markers to 3.5.0: agent.toml, daemon main.go (var + ldflags example), README release badge. Daemon now reports 'obsidian-memoryd 3.5.0'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(rag): graph-aware retrieval over the [[wikilink]] graph + Trie autocomplete (3.5.0)
…ptional)

Rewrote docs/{en,es}/install-with-agent.md to be self-contained for both
Cursor and Claude Code. The basic install is clone-free on both IDEs
(--ide claude registers basic-memory via `claude mcp add`; only --with-hybrid
needs the kit clone), so the core path collapses to one npx command, with
--rules all auto-installing the User Rules. Removes the redirect to fresh-PC
Path A; only Cursor's global User Rules stay a manual paste.

Filenames unchanged -> all cross-links intact. CHANGELOG [Unreleased] updated.
Docs-only; markdownlint + prettier + linkcheck verified locally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs: simplify "install with an agent" guide (both languages)
dependabot Bot and others added 30 commits July 22, 2026 18:08
Bumps [@opentelemetry/exporter-trace-otlp-http](https://github.com/open-telemetry/opentelemetry-js) from 0.219.0 to 0.221.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-js/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md)
- [Commits](open-telemetry/opentelemetry-js@experimental/v0.219.0...experimental/v0.221.0)

---
updated-dependencies:
- dependency-name: "@opentelemetry/exporter-trace-otlp-http"
  dependency-version: 0.220.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
#97)

Absorbs the non-breaking members of the dependabot dev-dependencies group
(#82) — @types/node 26.0.1 -> 26.1.1 and tsx 4.19.2 -> 4.23.1 — while
holding typescript at 6.0.3: TypeScript 7 breaks both the typecheck gate
(~24 stricter-inference errors) and the lint gate (typescript-eslint's
typescript-estree caps at `typescript <6.1.0`, so ESLint crashes under TS7).
TS7 adoption needs a dedicated migration once typescript-eslint supports it.

Also fixes the pinned-SHA version comments left by #77: setup-node's SHA is
v7.0.0 but the trailing `# v6` comment was stale in ci.yml, llm-benchmarks.yml
and release.yml (12 occurrences) — now `# v7.0.0`, matching the checkout pin.

lint, typecheck and sync-agents:check all green.


Claude-Session: https://claude.ai/code/session_01VsCMqLwaM1VnDiMyKTndnM

Co-authored-by: Claude <noreply@anthropic.com>
…#99)

Lands the safe member of the regrouped dependabot dev-dependencies PR (#98):
typescript-eslint 8.64.0 -> 8.65.0 (adds TS7 detection + an opt-in
onUnsupportedTypeScriptVersion error), keeping typescript at 6.0.3. TS7 is
still deferred — the toolchain only just started *detecting* it, not fully
supporting it, and it breaks both the typecheck and lint gates today.

lint, typecheck and sync-agents:check all green on TS 6.0.3.


Claude-Session: https://claude.ai/code/session_01VsCMqLwaM1VnDiMyKTndnM

Co-authored-by: Claude <noreply@anthropic.com>
TS7 breaks both CI gates today — typecheck (~24 stricter-inference errors)
and lint (typescript-eslint's typescript-estree only detects TS7, doesn't
support it, so ESLint crashes). Declaratively hold the typescript major so
dependabot stops re-opening a red PR for it; adopt TS7 later via a dedicated
migration PR (drop this ignore, bump typescript-eslint to a TS7-supporting
release, add the ~24 annotations).


Claude-Session: https://claude.ai/code/session_01VsCMqLwaM1VnDiMyKTndnM

Co-authored-by: Claude <noreply@anthropic.com>
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@9c091bb...3d3c42e)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/cache](https://github.com/actions/cache) from 4.3.0 to 6.1.0.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](actions/cache@0057852...55cc834)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.5.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](actions/setup-go@924ae3a...b7ad1da)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.2 to 7.0.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@ea165f8...043fb46)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 8.3.2 to 9.0.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](astral-sh/setup-uv@11f9893...c771a70)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 9.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
#108)

execa 10 imports `Set.prototype.union`, a Node 22+ builtin, so it throws
`TypeError: TEXT_ENCODINGS.union is not a function` on import under Node 20 —
which this repo supports (`engines: node >=20`; CI's test-node matrix runs
node 20). Verified: PR #107's node-20 leg failed exactly there. Hold the execa
major until the Node floor is intentionally raised to 22.


Claude-Session: https://claude.ai/code/session_01VsCMqLwaM1VnDiMyKTndnM

Co-authored-by: Claude <noreply@anthropic.com>
…t breaker + content extraction (ADR-0062) (#109)

Bundles the ADR-0062 seed-URL site crawler (obscura_crawl_start/_status/_stop) with a
five-part resilience pass on the scraping path (fetch retries, SERP-parser canary, two
opt-in search engines, expandQuery caching, persistent stale-while-revalidate cache), plus
two amendments: a per-host circuit breaker (crawl.mjs/research.mjs stop hammering a host
that just failed repeatedly) and Readability-based main-content extraction (trims
nav/sidebar/footer boilerplate before curation, falls back to the full sanitized body when
no article is found).

robots.mjs is temporarily a no-op stub (checkRobots always allows, unchecked) — a confirmed,
tracked decision, not a regression; see its module doc. The 11 tests that assert real
robots.txt parsing/compliance are skipped, not deleted, and must be un-skipped when it's
restored.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…uped run-report findings (#110)

* feat(obscura-web): deep-research auto_continue, auto-consolidate, grouped run-report findings

- auto_continue (opt-in, default off): when a job would stop at budget_exhausted
  with leads still queued, extend the SAME job's budgetMs once, straight to
  max_total_minutes (default 60, cap 180), instead of spawning a new job. New
  stoppedReason "max_total_time_exhausted" when that ceiling is hit with work
  left; frontier_empty and max_rounds (40) still apply as backstops.
- auto_consolidate (default on): best-effort consolidateTopic() call in
  runJob's finally, after the run report is durably written, so a failure
  (no Ollama, no sources, already-consolidated summary) never affects
  job.state. obscura_research_status.next reflects whether it actually ran.
- renderRunReport groups "Top findings" by originating query instead of one
  flat relevance-sorted list; retention cap raised 15->40 (TOP_FINDINGS_CAP)
  so a grouped report has enough per topic to be worth grouping.
- 8 new/updated tests across deep-research.test.mjs and
  deep-research-mcp.test.mjs; full obscura-web suite 450/461 (11 pre-existing
  robots.mjs skips, untouched); lint + typecheck clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* style(obscura-web): prettier formatting fix for deep-research.test.mjs

CI's lint job runs prettier --check separately from eslint; two lines in
the new auto_continue/auto_consolidate tests exceeded print width. No
semantic change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…WP0, ADR-0063) (#111)

* feat(evals): fixed-context budget inventory (WP0, ADR-0063)

The kit's fixed layer — rules block, MCP tool schemas, SessionStart injection,
skill/sub-agent descriptions, output style — is a permanent behavioural prior paid
by every session before any tool is called, and most of it had never been measured.

scripts/context-budget.mjs emits piece -> chars -> tokens -> file:line -> when
(always / on-trigger / on-call / opt-in), from this checkout or from a real install
(--home / --cwd), as markdown or --json.

Measured on v4.5.1: 45,247 chars ~= 11,312 tokens always, 76,891 worst case;
54,622 / 86,266 on an install whose project loads a second copy of the block.

Three findings the working estimate had wrong:

- obscura-web's schemas are the largest single piece (18,167 chars over 11 tools —
  bigger than the vault's 22 at 10,748), wired by the default install, no budget gate.
- The rules block is charged twice in a Claude Code session inside an installed
  project: --full writes both ~/.claude/CLAUDE.md and the project AGENTS.md
  (index.js:234-240, rules-merge.mjs:88,100) and Claude Code loads both.
- schema-budget.test.mjs's regex reads only the first literal of a "a" + "b"
  description. It is honest today only because hybrid-mcp.mjs happens not to use
  concatenation; obscura and downloads do. The new extractor follows concatenation
  and is pinned to agree exactly with the old regex where none exists.

Recorded as a tripwire, not a budget: composition pinned exactly, totals within
+/-20%. Choosing a real ceiling waits for the off-target behavioural number (WP1) —
picking one now would freeze an accidental value as policy.

No behaviour changes. lint + typecheck clean; 1,080 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLZkLQoyH2sjezhYi5TY9N

* fix(mcp): harden the schema-budget gate and extend it to the two ungated servers

WP0 found the gate depends on a property nothing checks: its regex reads only the
FIRST literal of a description, so `"a" + "b"` measures as `"a"`. hybrid-mcp.mjs
happens to use one string per description, which is the only reason the gate has
been honest — a refactor to concatenation would have let a schema of ANY size pass
without failing. Documenting that and leaving it in place was half the job.

- All three gates now use the shared concatenation-aware extractor from
  scripts/context-budget.mjs, and each asserts 1:1 anchor-to-string coverage, so
  silent under-measurement (an unreadable anchor contributing 0 chars) fails loudly.
- obscura-web gets a gate it never had. It is the largest single piece of the kit's
  fixed layer: 18,167 chars over 11 tools, more than the vault server's 22 (10,748),
  wired by the default --full install.
- vkm-downloads gets one too (5,236 chars). Opt-in, so lower stakes — gated anyway,
  because "it's opt-in" is how the largest ungated piece got that way.

The two new per-string maxima RECORD current debt rather than endorse it: the vault
server holds itself to 450 chars per description, obscura's longest is 1,475 (3.3x).
Trimming those changes what the model reads, so it belongs to a measured pass, not a
drive-by edit here — reshaping functionality because it was newly measured is the
anti-objective this work operates under. The gate's value today is that the number
cannot grow silently while that pass is pending.

lint + typecheck clean; 1,087 tests pass (9 new).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLZkLQoyH2sjezhYi5TY9N

---------

Co-authored-by: Claude <noreply@anthropic.com>
…t (WP24, ADR-0066) (#114)

"Cross-platform" in this repo has always meant OPERATING SYSTEM. e2e-smoke.mjs
installs with `--ide none` and proves the stack works on Windows/macOS/Linux. It
never asked whether wiring Codex, or Cursor, or anything that is not Claude Code,
produces a working install — while the README advertises six harnesses.

One structural fact makes that gap worse than a missing test: every deterministic
guarantee the kit has is a Claude Code hook. SessionStart injection, native-memory
write guard, close-ritual nudge, effort gate, token-saver compaction, vkm-terse —
all install into ~/.claude/ (claude-native-memory.mjs:305-309, token-saver.mjs:83-89).
Outside Claude Code the kit is an MCP server plus a prose block, and every rule in
that block depends on the model choosing to follow it.

scripts/harness-matrix.mjs runs a REAL install per harness into a throwaway HOME and
asserts the config artifact that harness actually reads. Wired into the existing
e2e-smoke job; exits non-zero on any broken wiring cell.

The design rule that matters most is what it does when it cannot know: a cell whose
CLI is absent reports `not-installed` — not a pass, not a failure, and never inferred
from the wiring cell beside it. A matrix that turns an absent CLI into a green tick
is worse than no matrix: it is a claim nobody checked wearing the costume of evidence.

So cells come in two classes with different evidence value: wiring (filesystem, runs
anywhere, every harness) and live (needs the CLI, honestly unmeasured when missing).
A `deterministic enforcement` column states the architectural finding in the artifact
rather than in prose someone has to notice.

Measured here: install + rules surface pass for all four harness families, Cursor's
mcp.json lands, Codex reported unmeasured (no CLI on this machine).

Full lint block green.


Claude-Session: https://claude.ai/code/session_01CLZkLQoyH2sjezhYi5TY9N

Co-authored-by: Claude <noreply@anthropic.com>
…0067) (#115)

The installed block is 9,375 chars injected into every session of every wired agent,
in every project, forever. ADR-0036 gated its SIZE. Nothing ever gated its
COMPOSITION — and composition is what matters, because the block is not a cost, it
is a permanent behavioural prior applying to work the kit was never designed for as
forcefully as to the work it was.

Five of its eight sections are general working style, not memory protocol:
Auto-cuestionate, Acompana no impongas, Memoria evolutiva, Conoce tu modelo,
Mantenlo barato. Possibly good advice — and unconditional instructions about how to
answer anything, shipped to someone who installed a memory tool.

Worse, it contradicted the skills with no stated precedence. /vkm-discipline
triggers on "any non-trivial task — coding, debugging, data, infra, writing, review"
and says "Deliver more than asked" and "No two approaches"; the block said "pide
confirmacion", "Muestra los candidatos y espera", "Nunca impongas". Three resident
priors, arbitration cost paid silently on every task, no way to turn any of it off —
and therefore no way to find out what it does.

Three levels, --rules-profile minimal|standard|full:

  core      always, <=1,200 chars. Membership test: does its absence make the kit
            UNSAFE or DISHONEST? (1,181 es / 1,165 en)
  memory    the protocol proper — recall, close ritual, which tool, what to save.
  doctrine  general working style. Real value, but a prior on ALL work.

core also carries a new arbitration rule that existed nowhere before:
  1. your preferences and the current chat beat any rule here, in a skill or in the
     vault — ask for two approaches and you get two approaches;
  2. brevity belongs to the prose, never to the work;
  3. low stakes -> decide and proceed; medium/high -> ask before assuming.

Rule 3 resolves the conflict instead of papering over it: "deliver more than asked"
and "ask first" were never opposed, they are the same rule at different stakes and
nothing had said so.

Default stays full (the advertised default is the full stack). `minimal` is the
documented KILL SWITCH: verified end-to-end at 1,507 chars against 9,445 — an 84%
smaller permanent prior.

Per-level budgets replace the single one and load-bearing rules are pinned TO A
LEVEL, closing the failure mode this refactor could most easily have introduced: a
safety rule silently demoting from always-on to opt-in. That is exactly why
`Nunca simplifiques` / `never simplify away` moved out of the terseness section into
core — otherwise turning off the style guidance would have turned off the rule
protecting validation, data loss and security too.

A fourth embedding surface (.cursor/rules/obsidian-memory.mdc, the installer
dogfood) is regenerated by sync-agents; its drift gate caught what a hand-written
resync missed.

Full lint block green; 1,087 tests, 0 failures.


Claude-Session: https://claude.ai/code/session_01CLZkLQoyH2sjezhYi5TY9N

Co-authored-by: Claude <noreply@anthropic.com>
…ADR-0064) (#112)

* feat(evals): shared statistics + an executable reporting rule (WP11, ADR-0064)

METHODOLOGY.md section 5 has prescribed replicas, confidence intervals and effect
sizes since it was written. Nothing in the repo computed any of them, so RESULTS.md
files reported bold deltas — the visual grammar of a finding — off n=1 to n=3.

Counted across the benches: 18 bold deltas, all at n <= 3, none at n >= 5.

evals/lib/stats.mjs provides a seeded (reproducible) bootstrap CI, Cliff's delta with
magnitude bands, Hedges' g, and classify()/formatDelta(), which apply the rule:

  n < 5                                   -> directional   (never bold)
  n >= 5, CI excludes 0, clears preset e  -> significant   (bold)
  n >= 5, CI includes 0 or within e       -> inconclusive  (never bold)

Bold is emitted by code, so it can no longer be written by hand off two runs. The
epsilon margin exists so a "must be no worse than" gate is decided by the same code
as "is better than" — the shape the off-target control needs, available before that
bench is written.

Two deliberate choices: the bootstrap is seeded, because an interval that moves
between runs cannot gate anything or be checked by a reviewer; and the headline
effect size is Cliff's delta, not Cohen's d, because bench scores are bounded and
often bimodal and a parametric d over n=5 of that shape reads more authoritative
than it is.

evals/ is not an npm workspace, so `npm test --workspaces` never covered it. The
lint job (deterministic, ubuntu — this is pure computation with no OS surface) now
runs the stats tests.

Correction pass: every pre-existing RESULTS.md gains a banner stating the rule and
has its under-powered deltas re-labelled directional. NO measurement changed; only
the weight placed on it. implementer-bench needed no change (it already declined to
claim a verdict at n=3) and token-quality-ab's n=9 round keeps its weight — that
bench pre-registered its decision rule, which is the pattern being generalised.

lint + typecheck clean; 13 new tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLZkLQoyH2sjezhYi5TY9N

* fix(changelog): merge the ADR-0064 entry into the existing Changed section

Two defects in the previous commit's changelog edit, both caught by CI:

- A second `### Changed` under `[Unreleased]` tripped MD024/no-duplicate-heading
  (siblings_only), and worse, it orphaned the pre-existing `### Added` entries
  underneath it — the seed-URL crawler entry ended up filed as a change.
- The Added entry claimed CI runs `node --test evals/lib/`, which is not what
  shipped: that form makes Node treat the path as a module to load, and a shell
  glob would not expand on the Windows matrix leg. The lint job runs the test file
  explicitly instead.

Verified by running the lint job's full step list locally this time, not a
hand-picked file subset — which is how the duplicate heading slipped through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLZkLQoyH2sjezhYi5TY9N

---------

Co-authored-by: Claude <noreply@anthropic.com>
* feat(evals): cost accounting in every bench run (WP7, ADR-0065)

Every live-LLM bench measured delta-quality and none measured what it cost, so
these were the same reported result:

  - the kit wins by 20 points
  - the kit wins by 20 points while spending 3x the tokens

For a kit whose central claim is token efficiency, that is the wrong blind spot.

The instrumentation was almost there and unused: runSubject has always returned
`usage`, and all five call sites are written `const { answer } = await runSubject()`
— the data arrived and was dropped one line later.

runSubject now returns a normalized `cost` (inputTokens / outputTokens / cacheRead* /
totalTokens / turns / costUsd / wallClockMs) and every runner carries it on its
emitted row, so a past run can be re-read for cost without re-running anything.

Three decisions, each with an obvious wrong alternative:

- Unreported is null, never 0. aggregateCost refuses to average a cell where any
  run is missing a field. Averaging 100 and "unknown" to 50 fabricates a
  measurement, and it errs toward *cheaper* — the flattering direction.
- wallClockMs is measured by the runner, not read from the CLI, so latency works
  for any agent (codex exec, a future bridge) even without token counts.
- totalTokens excludes cache tokens: they are billed differently and folding them
  in would double-count every cost claim.

costEfficiency() gives the delta-quality/delta-tokens column (quality per 1,000
extra tokens); a gain at zero token cost reports null rather than Infinity, because
"free" is a real answer that deserves its own representation.

toTelemetryTotals() re-keys a cell into the exact shape vkm-doctor aggregates local
OTLP into (doctor.mjs:38), with a test pinning the vocabulary — that is the join
that makes lab cost and real-session cost the same units.

No bench was re-run, so no RESULTS.md gains a cost column yet; the next round of
each populates it. A column of retroactively-estimated costs would be worse than
an empty one.

Also documented, not changed: agentCmd is split on whitespace, so no single
argument may contain a space. Found by writing the test.

lint + typecheck clean; 6 new tests pass; full suite 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLZkLQoyH2sjezhYi5TY9N

* fix(adr): use a text fence so prettier does not reformat the shape sketch

The block sketches SubjectCost's field names, not runnable code, so a `js` fence
invites prettier to format a fragment it cannot parse cleanly. `text` satisfies
markdownlint's MD040 without handing the content to a formatter.

Root cause of it reaching CI: I re-ran markdownlint after adding the fence but not
prettier. Same shape as the duplicate-heading miss on #112 — a hand-picked subset
of the lint job is not the lint job. Now verified against the full step list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLZkLQoyH2sjezhYi5TY9N

---------

Co-authored-by: Claude <noreply@anthropic.com>
…conditional (WP9, ADR-0068) (#116)

The skill advertised itself as firing on "any non-trivial task — coding, debugging,
data, infra, writing, review". That is why it reaches explanation, non-technical
writing, summarisation and decisions that are the user's — task types where its own
contract is actively wrong: "deliver more than asked" is scope inflation when the user
fenced the scope, and "no two approaches" contradicts a user who asked for two.

ADR-0067's arbitration rule settles precedence, but the skill was still FIRING on
those tasks and paying the arbitration cost every time.

The wording was not written in one pass. Three candidates were drafted under different
objectives (precision / recall / legibility to a small model) and each was attacked by
two independent reviewers who classified all 64 bench cases under it. ALL SIX VERDICTS
came back "risky" — no candidate survived intact — and the union of their findings drove
the final text:

- hard-08 ("make the error messages friendlier") was the unanimous #1 predicted miss;
  fixed by naming the artifact ("code and its copy") and bounding the trivial-edit
  negative to a concrete list instead of an open category.
- Every candidate had a head clause its own positives contradicted, predicted to kill
  disc-09 and hard-09. Deleted outright: a flat noun list cannot contradict itself.
- Bare "summaries"/"explanations" re-swallowed those positives; scoped to "chat/log
  recaps" and "explainers", which hit the intended cases by literal token.
- "the user's own call" dropped as unroutable (English-only idiom, ambiguous, and a
  behavioural constraint the arbitration rule already enforces).

Also: "Deliver more than asked" is now conditional on the user not having scoped the
request and names the rule that outranks it; the domain table is documented as
deliberately wider than the trigger (every domain stays reachable via explicit
/vkm-discipline); 8 new bench negatives (64 -> 72 cases, none 15 -> 23).

Two of my own defects, caught by the gates rather than by me:

- The first draft was 316 chars and broke skills-install.test.mjs's 300-char cap,
  which I had not checked before briefing the design. Trimmed to 298 against an
  explicit list of the 20 tokens reviewers tied to a specific bench case — all 20
  survive; only "prose" (redundant with the explicit "diagrams" negative) and "real"
  (anchors no case) were dropped.
- One proposed bench case was discarded: it asked for postmortem-template prose
  expecting "none" while disc-09 expects vkm-discipline for postmortem prose. The
  intended distinction is real but not expressible in a description, so the case would
  have measured noise. Validate the instrument before trusting it (METHODOLOGY 2.1).

PREDICTED, NOT MEASURED. The reviewers' classifications are model judgements, not a
run. skills-triggering must be re-run at more than one tier before any claim — three
reviewers independently noted the failure mode is model-dependent clause anchoring.

Full lint block green; 1,095 tests, 0 failures.


Claude-Session: https://claude.ai/code/session_01CLZkLQoyH2sjezhYi5TY9N

Co-authored-by: Claude <noreply@anthropic.com>
…ance (ADR-0069/0070) (#117)

* feat(audit): detect index drift; answer the architectural questions in the repo (ADR-0069)

A review named "multiple representations of knowledge — Markdown, graph, embeddings,
indices, SQLite" as the top technical debt. Read against the code, that debt does not
exist as stated: Markdown IS canonical and it is enforced structurally, not by
convention. indexer.py keys each note on (mtime_ns, size_bytes), drops rows whose file
left disk, and rebuilds that note's typed relations and observations from the text in
the same pass; a SCHEMA_VERSION bump forces full reprocess — "no note can stay stale"
(indexer.py:141). Nothing writes to the graph, FTS or vectors independently of a note.

The real debt is narrower and sharper: NOTHING VERIFIES the derived state still matches
the Markdown. The incremental key cannot see an edit preserving both mtime and size — a
git checkout restoring an mtime, a two-char swap inside one filesystem tick, a restored
backup — and a note deleted outside an indexing pass leaves its row behind.

In a memory system that is worse than a missing index. A missing index fails loudly; a
stale one keeps answering, confidently, from text no longer on disk.

vault_audit now reports index_drift: missing (on disk, no row), orphaned (row, no file),
stale (both, metadata disagrees). It names its own fix (vault_fts_index) and NEVER
applies it — a report that repairs hides the problem it exists to surface, and a test
pins that two consecutive audits see the same drift. None when there is no index
(absence is not drift) or the sidecar is locked/corrupt (not a vault-health finding).

Stated honestly rather than buried: `stale` is exactly the subset the incremental key
CAN see. Content hashing would catch the rest and was rejected — O(bytes) on a tool
users run routinely, to catch a case needing an mtime-preserving edit. If it shows up
in practice, an opt-in deep:true is the follow-up.

Cost: zero new tools, zero new parameters — a result field is free in schema terms,
which is what made this shape possible with 52 chars of headroom on the ADR-0063 gate.
The one-line description edit still overran it by 2 chars; I trimmed the description
rather than raise the gate. Final 10,790 / 10,800.

ARCHITECTURE.md gains "Architectural questions, answered": what the kit is (a memory
substrate over MCP), which representation governs, how much intelligence belongs in the
engine vs the model (45,247 chars of fixed prior, being moved to mechanism), whether it
works without the skills (yes — --rules-profile minimal), how far it scales (UNPROVEN
past a few thousand notes; the retrieval bench is 19 notes), whether it self-repairs
(detects and refuses to act, deliberately), and the global quality metric (there ISN'T
one — a real gap). Several answers are admissions. An architecture document that only
records strengths is marketing.

Full lint block green; node suite 0 failures; python 203 passed (4 new).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLZkLQoyH2sjezhYi5TY9N

* fix(vault): normalize newText EOL in vault_edit_file; three doctrine contradictions (ADR-0070)

A capability audit ran 34 agents across every kit surface — 22 vault tools, 11 web
tools, 6 download tools, 4 skills, the memory protocol, the ADR backlog — with every
proposal attacked by an independent reviewer told to reject when uncertain.

ZERO new skills and ZERO new tools survived. Nearly every proposal was already covered
by an existing parameter, failed the membership test (a component belongs only if it
measurably improves another component), or rested on a premise the source contradicts.

That is the useful result. The rejections surfaced better work than the proposals did:

1. THE WRITE PATHS DISAGREED ABOUT LINE ENDINGS. vaultAppendFile normalizes its chunk
   to the file's own EOL; vaultEditFile spliced newText in raw. Vault notes are
   commonly CRLF — the doctrine says so and tells the model to anchor each edit on ONE
   single line precisely because of it — while a model composes newText with LF. So
   identical content produced a clean note through one tool and a MIXED-ENDING note
   through the other, which then breaks the single-line anchoring that rule exists to
   protect and noises every later git diff.

   Fixed with one shared toFileEol() helper used by BOTH paths, because the bug WAS
   the drift: fixing one copy and leaving the other is how it happened. Single-line
   newText is byte-identical to before. Four tests, VERIFIED to fail without the fix
   (2 of 64 fail when the normalization is removed) rather than assumed to.

2. THE CLOSE RITUAL NAMED THE EXPENSIVE PATH ONLY. The rules block said "Close =
   vault_edit_file/vault_write_file -> SESSION_LOG.md (1 line at the end)" and never
   mentioned vault_append_file, whose own description calls it "the CRLF-aware append
   — the SESSION_LOG one-liner path, no anchor round-trip". The most frequent write in
   the protocol was routed through the path that needs a unique anchor.

3. THE DISCIPLINE SKILL CONTRADICTED ITSELF 54 LINES APART. Step 5 mandates the
   bundled evidence-gate runner; the guardrails section called evidence gates "modules
   you wire when you want them, off by default". It now names the three guardrails
   that actually ship: the ADR-0067 stakes ladder, the bundled gate runner, and the
   untrusted-data envelope + domains/security.md.

No capability claim is attached to (2) and (3). No bench scores "did it pick the
cheaper write path" or "did it ask at high stakes", so a number here would be invented.

Rejected and recorded: adding an underHeading param to vault_append_file (the audit's
most developed proposal) — the default search payload already returns each hit's
heading, so no read is needed for an anchor; and the kit's own corpora use three
different decision-heading names, so it would miss and fire the round-trip it meant to
save. Also rejected: normalizing oldText for matching, which would retire the
one-line-anchor rule but loosens match semantics on a path that rewrites the user's
notes — separate decision, real risk.

Cost: no new tool, no new parameter, no schema characters. +33 chars (es) on the
memory rules level, inside its 4,800 budget.

Full lint block green; node suite 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLZkLQoyH2sjezhYi5TY9N

* fix(audit): match the indexer's dot-directory rule; single source for the search limit

Two more defects from the lifecycle audit. One of them I introduced an hour ago.

THE AUDIT MEASURED A DIFFERENT VAULT THAN SEARCH DOES.
indexer._should_skip_dir skips ANY dot-directory (`name.startswith(".")`);
audit._iter_md_files excluded only three by name. vault_delete_file soft-deletes into
.trash/ INSIDE the vault, so trashed notes counted toward the audit's token budget,
appeared in `oversized`, and had their [[wikilinks]] scanned — reporting on notes
retrieval can never return.

It also produced FALSE index_drift in the feature added one commit earlier (ADR-0069):
a soft-deleted note read as `missing` forever, because the index is correct to omit it.
Reproduced live before fixing: a vault with one trashed note reported drift_total: 1.

How this was found, stated honestly: the working hypothesis was the opposite and much
stronger — that soft-deleted notes stay SEARCHABLE, i.e. that "delete" did not remove a
note from recall. An empirical probe (index a vault with a note in .trash/, search for
its distinctive content) REFUTED it — _should_skip_dir already excludes it. The real
bug was on the audit side. Only the probe told the two apart; reading the code had
pointed at the wrong one.

Fixed at the root — the audit now applies the indexer's rule — rather than by
special-casing .trash inside index_drift.

TWO SOURCES OF TRUTH FOR THE DEFAULT SEARCH LIMIT.
DEFAULT_SEARCH_LIMIT is derived once from VKM_DEFAULT_LIMIT (ADR-0034's A/B lever) and
used in both search tools' schema defaults, then contradicted by a hardcoded
String(limit ?? 10) in both handlers. Harmless today because the schema default always
populates `limit`; a lie that would silently ignore the lever the moment that default
moved. Both now read the constant.

Full lint block green; node suite 0 failures; python 205 passed (2 new regression
tests: the audit ignores dot-directories, and a trashed note is not drift).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CLZkLQoyH2sjezhYi5TY9N

---------

Co-authored-by: Claude <noreply@anthropic.com>
Seven merged PRs and eight ADRs (0063-0070). The version markers move together
(package.json x8, README badges, agent.toml, main.go) and CHANGELOG [Unreleased]
becomes [4.6.0], which is what the release workflow's guard compares the tag against.

The section opens with what actually changes for an existing install — the rules
block rewritten in place across four surfaces, /vkm-discipline no longer auto-firing
on explanation/writing/recaps/diagrams/research/user-reserved decisions, the new
--rules-profile kill switch, and the vault_edit_file EOL fix — and states plainly
that the behavioural changes are PREDICTED, not yet measured. The instruments landed
in this release; the runs come next.


Claude-Session: https://claude.ai/code/session_01CLZkLQoyH2sjezhYi5TY9N

Co-authored-by: Claude <noreply@anthropic.com>
…ies (WP1, ADR-0071) (#119)

ADR-0063 measured the fixed context layer and refused to turn it into a budget until
the off-target control said what that layer costs in BEHAVIOUR. Nothing here could
build those subjects, and the obvious way to build them was already wrong: e2e-smoke
and harness-matrix both install with --minimal --no-skills --no-agents, correctly,
because neither measures behaviour. Reused as the "kit installed" arm, either produces
a subject missing the entire independent variable — and the bench then runs, grades,
and reports a null.

So an arm DECLARES the layers it carries and a build that does not match its
declaration throws. Verification reads the filesystem a session would actually load,
never the installer's exit code. Five arms: off (control), core, standard, rules-full,
full. Arms opt pieces IN from a strip-everything base; rule levels are detected via
levelBody() rather than copied markers; an arm whose required CLI is absent reports
skipped, never a pass with an empty layer list.

The probe found its own bug before shipping: hooks are spelled {command:"node",
args:[script]}, so reading `command` returned "node" and reported hooks=0 for an
install carrying seven. It slipped the first survey because the full arm had not
declared `hooks` at all. Both fixed, both pinned by tests verified to fail against the
old code by mutation.

Evidence on the CI runner: all four buildable arms verified with byte-identical layer
compositions to the local run (1487c / 6115c / 9258c), full reported skipped, survey
exit 0. 16/16 checks green.

No behavioural claim is attached. This ships the instrument; it has not been pointed at
anything yet.

Also adds ADR-0062..0071 to the ADR index, which had drifted nine entries behind.
…ADR-0072) (#120)

Two search-envelope designs were measured before either was built. Only one survived.

REFUTED — the truncation signal. The prediction written first was that it must fire
SELECTIVELY. It fires on 100% of queries in both corpora at every limit, and that is
arithmetic, not a corpus artifact: the semantic pass is dense, so the candidate pool is
always the whole vault and "pool > limit" is always true in any vault larger than limit.
The first probe reported a median pool of 1, which would have made the flag look
selective; it had called index_vault without index_vectors and measured a BM25-only pool
with the semantic half missing. A lexical-only variant fires selectively (34.2% / 12.5%)
but both corpora are 19 and 7 notes, smaller than limit itself, so shipping on that
evidence would be picking a number.

SHIPPED — `why` replaces `score` in the default hybrid payload. Measured: 380 hits, 0
cases where the score rose as rank fell. RRF scores are rank-derived, so the field is
strictly redundant with the array index. Which rankers matched is not recoverable from
order and is actionable: a sem-only hit on a query naming an exact identifier means the
literal token was never found, so vault_fts_search is the better next call. The label is
bimodal and balanced (92/98 at limit 5), not a near-constant field.

score moves behind --explain rather than being deleted; under rerank the two genuinely
differ. _why() lives in query.py beside HybridHit so bench_tokens.py's wire arm can emit
the identical shape without importing the CLI; that arm is updated in lockstep.

Cost stated plainly because it is small: -53 tokens (-0.45%) on the bench wire total.
The field is 14.6% cheaper per hit but snippets dominate — an information change, not a
token win.

Evidence: 16/16 checks green, including the three coupled gates (test-python with 2 new
tests, retrieval-bench re-running --assert-wire-savings 0.30 at --assert-answered 1.0,
and the wire contract on all three OS legs). The subtle case — rank 0 is a real rank,
only None means absent — verified by mutation.
…(ADR-0073) (#121)

The case for lowering vault_hybrid_search's default limit below 10 rested on
evals/tokens/ reporting 100% answered at k=3, saving 3x more than k=10. That table
cannot say no: the corpus is 7 notes with ground truths of at most 2, so every k >= 2
passes its completeness gate by construction. 100% at k=3 is not a finding about k, it
is a restatement of the corpus's shape.

evals/limit/ makes the question decidable — 74 real ADRs, 15 queries, ground truths of
3-6 notes:

  k=3   13% answered
  k=5   13% answered, wire savings 3% (n=2)
  k=10  60% answered, wire savings 70%
  k=20  87% answered, wire savings 36% (mean -2%, worst -566%)
  k=74  100% answered

k=5 answers 13% where k=10 answers 60%, and k=10 also saves more: a query that fails
the completeness gate contributes no savings, because the agent lacks the answer and
reads the notes anyway. Cheap-and-wrong is not a saving.

DEFAULT UNCHANGED. No code, no tool, no parameter, no schema characters — the
deliverable is the evidence and the decision not to act. The doctrine's limit: 3-5
advice for targeted single-fact recall is untouched and still correct.

The first version asked in Spanish against English ADRs with an embedder that has no
cross-lingual capability, and measured that instead: k=10 answered 13%, k=20 only 27% —
a curve reading as "raise the default to 40". Translating the same 15 queries, labels
untouched, moved k=10 to 60%. The k=74 ceiling run exposed it. Recorded in the ADR.

Also corrects evals/README.md beside the table that produced the false lesson, and
gates the labels (not the numbers) so an ADR rename cannot silently turn a label into
an unreachable path — verified by mutation. CI: 16/16 green, 206 passed + 2 skipped
(sqlite-vec extra absent) = the same 208 as local.
…e (ADR-0074) (#122)

"Cross-project isolation" was on the memory-lifecycle list as a missing capability, with
an obvious shape: a project: filter reusing the section machinery. Measured first, on 12
project-ground-truth queries over the 19-note retrieval corpus:

  k=3    4/12 (33%) leak another project,  4 foreign hits
  k=5   11/12 (92%) leak another project, 15 foreign hits
  k=10  11/12 (92%) leak another project, 30 foreign hits

The 92% is what would justify building the filter, and it is the same density artifact
that refuted the truncation signal in ADR-0072: a k=10 result on a 19-note vault is more
than half the vault, so "another project appeared" is close to arithmetically guaranteed.
The number an agent pays for is the top-3: 4 hits, all the SAME note, at rank 3, on
queries about three different projects.

NO FILTER SHIPS. NO CODE CHANGES. Two findings decide it:

1. The mitigation already shipped one ADR ago. Three of those four hits carry
   why: "sem" — matched on embedding similarity with no lexical overlap. That is what
   ADR-0072's provenance label exposes, and this is independent data: the label was
   designed for a different question and validated here without being tuned for it.

2. The decisive test cannot run in blocking CI. The measurement uses the deterministic
   hashing embedder; a --full install ships fastembed. This environment's egress policy
   denies huggingface.co (403 to CONNECT) and the CI job cannot pull a ~100 MB model, so
   the probe takes --embedder and runs BOTH arms in nightly-benchmarks.yml, which
   already caches it. One embedder is an anecdote; two are the experiment.

Reported, never gated: a leak-rate ceiling would freeze a number from one embedder on a
19-note corpus into policy — the ADR-0073 mistake, one ADR after writing it down. The
inflated k=5/k=10 rows are kept on purpose, so the next reader does not re-derive 92%
and reach the conclusion this ADR declines.

The honest gap: the answer is not available today. The nightly run produces it.

CI: 16/16 green. 208 Python tests pass.
…ADR-0075) (#123)

ADR-0027's test checks the GRAPH LAYER's ordering. Nothing checked what memory actually
asks: when a decision has been superseded, does a topic query return the current
decision or the obsolete one? Measured on five independent supersession pairs:

                        current first   obsolete first
  graph off (baseline)       2/5             3/5
  graph=True                 0/5             5/5
  graphTyped=True            0/5             5/5

Turning on graph fusion made the wrong answer deterministic. The edge is authored in the
NEW note, so the new note becomes a graph seed and the OLD note collects the one-hop
neighbour boost the new note gets nothing of from its own out-edge — the vault having
recorded that a decision was replaced is precisely what promotes the replaced decision.
The baseline row matters too: 3/5 wrong with the graph off means ranking had no notion
of currency at all.

superseded_map() + demote_superseded() order each superseding note above the note it
supersedes, before the cut to limit. After: obsolete-first 0/5 in all three
configurations. Always-on, because a knob would ship the wrong answer by default —
membership-preserving (navigation case survives), no-op without the data (VERIFIED:
wire total byte-identical at 11,810, recall@5 still 1.000), and terminating on cycles.

Rejected lowering the supersedes weight: weights are rank-based RRF inputs, so it
weakens ADR-0027's navigation case without guaranteeing the ordering memory needs — and
the defect reproduces with the graph OFF, where those weights are never consulted.

CI 16/16 green, 211 Python tests. The new test verified to fail when the reorder is off.
…dy refuted (ADR-0076) (#124)

Prediction written first: if ranking ignores status, a hypothesis will outrank a
confirmed fact roughly half the time. Five topics, each with a hypothesis note and a
confirmed note, ten fillers, one query each:

  natural wording (hedged)          confirmed 4/5   hypothesis 0/5
  adversarial (matches the query)   confirmed 0/5   hypothesis 5/5

The prediction was wrong and the truth is sharper: status is not consulted at all.
Hedged prose loses on its own — the first run was measuring the fixtures' writing style,
not the ranker. Rephrasing five notes reversed 4/5 into 5/5.

NOTHING SHIPS. Both obvious fixes are blocked by results the repo already has: a
tie-break has nothing to tie-break on (a hypothesis and the fact that settles it carry
no relation, and pairing by topic is the semantic-duplicate problem already killed by
measurement), and surfacing status on the hit needs an index migration because
frontmatter is not indexed at all. Global demotion is worse than the defect.

The unmitigated gap is stated rather than papered over.
Six memory-lifecycle questions asked with a falsifiable prediction written first; five
ended in "do not build that".

  ADR-0071  verified bench arms — an arm that does not carry what it declares throws
  ADR-0072  `why` replaces `score`; the truncation signal refuted (fires on 100%)
  ADR-0073  the default limit stays at 10 — the old bench could not fail
  ADR-0074  cross-project `project:` filter closed without code
  ADR-0075  a superseded decision could outrank its replacement — FIXED
  ADR-0076  ranking ignores `status`, and both obvious fixes are blocked

One behaviour changes for users: with `- supersedes [[old]]` in the vault, search now
returns the current decision above the obsolete one (measured 5/5 obsolete-first before,
0/5 after; strict no-op without the relation).

No new tool, no new parameter, no schema characters, no migration.

Gates: version.mjs check aligned, ALL LINT STEPS PASS, 211 Python tests, npm test exit 0,
CI 16/16 green.
`version:set` rewrote 13 markers and left the lockfile out, and `check` never read it, so 4.6.0 and 4.7.0 both shipped with every workspace recorded at 4.5.1 in the lock. `npm ci` never compares a workspace's package.json with the lock's copy, so nothing failed loudly.

Each workspace's lock entry is now a marker, derived from the existing package.json markers. `check` surveys 20 markers, up from 13.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…visories closed (#127)

Housekeeping only: nothing in the installed contract changes (no new tool, no new
parameter, no behaviour change), so this is a patch.

README, both languages. The landing page opened with theory — what MCP is, how
information flows, a ten-row component table, the measured token economy, ADR
references — with every paragraph duplicated inline in ES and EN even though a full
English README already exists one link away. A reader deciding in thirty seconds
whether this is for them had to parse an engineering document first, and the visible
surface (doctor, spec, obscura, downloads, research, hybrid, rag, daemon, graph,
telemetry) made a one-command install look like an ecosystem migration.

The first screen is now the promise in one sentence, then install -> restart -> ask
it one question, then an explicit note that this is everything needed to start.
--full, the agent-driven install, the update flow, the component table and the
benchmark wall moved behind <details>; the inline bilingual duplication at the top
is gone and the language switcher stays. No content deleted, every link still
resolves (linkcheck: 1035 files) — what changed is disclosure order.

Dependencies. brace-expansion 5.0.7 -> 5.0.8 (unbounded expansion -> OOM crash;
reached only via eslint -> minimatch) and fast-uri 3.1.2 -> 3.1.4 (host confusion
via a literal backslash authority delimiter; via @modelcontextprotocol/sdk -> ajv).
Both are build-time paths, neither reachable from a running install.

Left open and documented: @hono/node-server serve-static path traversal on Windows
(GHSA-frvp-7c67-39w9, moderate). The patch lands in 2.0.5 while sdk@1.29.0 — the
newest published — pins ^1.19.9, so npm's only offers are downgrading the SDK to
1.24.3 or overriding a major outside its declared range, which would risk breaking
the Streamable-HTTP transport in a path no test here covers. The kit never imports
it: every HTTP server in this repo is http.createServer bound to 127.0.0.1
(vkm-spec/src/server.mjs, vkm-doctor/src/otel-sink.mjs) and the MCP servers speak
stdio. Revisit when the SDK bumps.

Lint config. The .claude directory is now ignored by eslint and markdownlint. It
holds the agent worktrees — full nested checkouts of this repo — so locally both
linters reported every file once per live worktree and failed on half-written code
from an unrelated branch. CI never saw it (gitignored), so local and CI disagreed
about what a clean tree is.

The lockfile-drift fix this release also carries came from #126 (ADR-0077), which
landed on main while this branch was open; its per-workspace marker table is what
bumped the lock to 4.7.1 here.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants