feat(installer): add Codex CLI parity - #129
Draft
JagoGG wants to merge 320 commits into
Draft
Conversation
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)
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
vkm-implementeragent for Codex CLIDrift documented
Codex does not currently support
updatedMCPToolOutput; the token saver returns compactedPostToolUsefeedback withcontinue: false. The unstabletranscript_pathdependency for the effort gate and the/hookstrust review remain explicit follow-ups.Validation
npm run sync-agents:checknpm run lintnpm run typechecknpm testnpm test --workspace @vkmikc/create-vkm-kit -- --test-name-pattern='Codex'