Skip to content

Releases: jojoprison/mnemo

v1.2.17

Choose a tag to compare

@github-actions github-actions released this 21 Aug 01:53
2def4e7

Added

  • A private-leak guard in the linter — a link into someone's vault can no longer ship. Two [[wikilink]]s pointing at the maintainer's own Obsidian notes reached a public PR during the v1.2.14 review and were caught by hand; nothing in the repo guarded that class. scripts/lint-skills.py now sweeps every shipped text file and fails on links that name a concrete note. A blanket ban was impossible — mnemo is a plugin about Obsidian and its docs carry 60+ legitimate examples — so the rule passes only shapes that cannot name a real note (placeholders like [[{hub_name}]] and [[Session — …]], single ASCII words like [[wikilinks]], regex fragments) plus the invented names listed in the new scripts/wikilink-allowlist.txt. Adding a name there is a review decision, not a formality: the file says so, and scripts/test-lint-wikilinks.py (22 tests) fails on entries that no longer appear anywhere, so the list stays a review surface instead of debris. The same pass flags absolute home paths while leaving ~/ and ${VAR} forms alone.
  • Two properties of that guard are worth naming, because both are how this class hides. The sweep reads whole files: the second link caught by hand was wrapped across two source lines, and a line-by-line scan would have called that file clean. And a sweep matching nothing at all fails instead of reporting clean, since a wrong root, extension list, or regex is indistinguishable from a spotless repo. The guard caught its own documentation while it was being written, and it caught five machine-local paths that an earlier hand-run grep had missed — that grep had searched for one specific username and returned an honest, meaningless zero.

Fixed

  • 49 documentation claims that no longer matched the code, found by an adversarial audit of every doc against the implementation and confirmed one by one (of 50 findings, verification rejected exactly one). The dominant failure was not the code moving ahead of the docs — it was a fact corrected in one file and left standing in its neighbour: the RU and ZH README sections still said "all fields are optional" long after the EN section was fixed in v1.2.5, CONTRIBUTING.md carried a stale copy of a gate whose canonical list lives in TESTING.md, and docs/health.md showed a report format the skill stopped printing.
  • Six places across all three README languages claimed nothing runs without your pick. review --full has been the documented exception since v1.2.8 — the flag itself is the consent — and review.full.autoConnect lets its connect step apply links unprompted. Both are now stated wherever the absolute was. The RU and ZH connect rows carried the same absolute and now carry the exception too.
  • CONTRIBUTING.md described a skill that fails the mandatory gate. Its "Adding a New Skill" steps named neither the required ## Portable paths section nor the literal invocation marker, both enforced by the gate, and implied an eighth skill is a drop-in addition when the canonical set is pinned in three separate places. All of it is now named, with the registries listed.
  • skills/session/SKILL.md contradicted itself about handoff.maxKB — 40 KB in one line, 56 KB fifty lines earlier. The code says 56; 40 turns out to be the legacy archive-handoff fallback used only when no value is passed, and config-schema.md now records both.
  • Seven CHANGELOG compare-links pointed at four tags that exist neither locally nor on origin (v0.1.0, v0.4.0, v0.5.8, v0.8.0), each rendering a GitHub 404. The neighbouring links now span the gaps, with a dated note explaining why those four releases have no link.
  • TESTING.md — the manual-gate document — described nothing that shipped in v1.2.16 and still stamped itself v1.2.15, the exact staleness v1.2.15 had fixed. It now covers context-window.py --explain, health's Step 7.55, and the new guard, and its skill-routing claim matches the frontmatter (context: fork is on one skill, not three). Smaller corrections landed in config-schema.md (the reserve formula omitted the second clamp; a repo-relative command does not resolve for an installed plugin; a model alias was pinned to a version), docs/setup.md (three config blocks setup writes were absent), docs/health.md (four steps missing from the contract table), and one-line omissions in the ask, connect, review, save, and session guides.

v1.2.16

Choose a tag to compare

@github-actions github-actions released this 21 Aug 01:04
f3681a9

Added

  • context-window.py --explain — why the autocompact nudge is quiet, and what to set. hooks.autocompactNudge fails silently by design: when the window cannot be established it stays quiet, which is indistinguishable from "nothing to warn about". The new read-only diagnostic says which case you are in, names the source the window came from (env / settings / cache / model-default), and reports the threshold that follows from it. health runs it (new Step 7.55, only when the nudge is enabled) and setup passes on its hint when the user turns the nudge on.
  • It answers the two questions people get wrong. The threshold is not the window: Claude Code compacts at window − min(max_output, 20000) − 13000, so asking for compaction at 460000 means setting 493000 — setting 460000 compacts at 427000. And a value above the model's context window does nothing: on a 200k model 460000 resolves to 200000, which looks identical to a working setting. Both are reported explicitly, the second as a clamped: true flag.

Changed

  • mnemo still ships no default for autoCompactWindow, and this is now written down as a decision rather than an omission. It is Claude Code's setting, not mnemo's; measured against the model table in the runtime, 8 of 14 models have a 200k window where any larger value has no effect at all, while on the 6 native-1M models a "sensible" value would halve someone's usable context without being asked. mnemo reports what to set and never writes it.

v1.2.15

Choose a tag to compare

@github-actions github-actions released this 20 Aug 14:26
6fbd4d3

Fixed

  • CI ran 7 of the 18 test suites. .github/workflows/skill-lint.yml named each suite as its own step, so a test that shipped with a feature was never executed until someone remembered to add a step — test-context-window.py and test-autocompact-nudge.py, both released in 1.2.14, had never run in CI at all. It now loops over scripts/test-*.py, and fails when the glob matches fewer than 10 files, because a glob that matches nothing would otherwise report a green gate on zero tests.
  • docs/review.md claimed the session scan "never falls back to another task". It does: with no runtime session id it borrows the newest Codex rollout sharing the working directory. 1.2.14 made that visible with a SOURCE: line rather than removing it, so the doc was the only place in the repo directly contradicting the code. The review skill now also checks that line and treats a borrowed scan as unavailable instead of auditing another task's work as your own.
  • The gate command list lived in three hand-kept copies (TESTING.md, CONTRIBUTING.md, docs/codex.md), each naming a different subset and two missing verify-release.py. TESTING.md § Automated gate is now the single canonical list and uses the same glob as CI; the other two point at it.
  • config.example.json was missing keys setup writeshandoff, hot, review.full, hooks.hotDigest — while README tells you to copy that file as your working config. The full schema in config-schema.md had the same gap, and its "defaults are: …" line read as exhaustive while omitting hotDigest=true.
  • README.md's project tree had drifted: no mnemo-autocompact-nudge.sh, no context-window.py, no depth-contract.md / vault-conventions.md. The README config example also carried no hooks block at all, so a reader never learned the nudges exist or that both blocking ones are opt-in.
  • head -32 still truncated the session scan. render() reaches 43 lines at worst — 44 with a SOURCE: line — and WORTH_SAVING_SIGNALS prints last, so it was lost on an active session. Raised to head -44.
  • The CHANGELOG entries for 1.2.13 were in Russian, which put them into the body of a public GitHub Release. Translated; the surrounding trigger-phrase quotes stay as they are.

Changed

  • TESTING.md gains manual checks V19-V23 for the 1.2.14 session-id fix and the autocompact nudge, and its header now tracks the current version (it read v1.2.6).
  • docs/BACKLOG.md gains a P4 card for PreCompact as a second leg to autocompactNudge, recording why it was deliberately not used as a replacement: at PreCompact time the agent is not running, so it cannot invoke the close-out the nudge exists to trigger. Filed so the option stays visible instead of being re-proposed as new. The card also notes that lint-skills.py has no [[wikilink]] rule — two links to private notes reached a public PR during the 1.2.14 review and were caught by hand.

v1.2.14

Choose a tag to compare

@github-actions github-actions released this 20 Aug 13:01
b30767b

Added

  • hooks.autocompactNudge — warn before autocompact silently drops raw context. New opt-in (default false) Stop hook hooks/mnemo-autocompact-nudge.sh blocks once per severity level as the session closes in on the point Claude Code compacts (warn ~50k / critical ~10k tokens remaining) and recommends /mn:review --full. The window follows Claude Code's own chain — env CLAUDE_CODE_AUTO_COMPACT_WINDOW, settings.autoCompactWindow, then its per-model ~/.claude.json cache read by the active model's key — validated to the [100k, 1M] range Claude Code accepts, then clamped to the model's context window, because a configured value is a ceiling request and not the threshold: a 460k setting on a 200k session never takes effect. With nothing configured the standard 200k window applies only where it is provable — when Claude Code's 1M-access cache shows the account has no large-context access — so a normal install gets a working nudge with no configuration, while a large-context account stays silent rather than be warned at 15% full (the transcript records the model id without its [1m] suffix, so the two are indistinguishable). Bands are measured from where compaction actually happens, W − min(max_output, 20k) − 13k; measured from W, critical would sit past a point the session can never reach. The anti-loop marker lowers when the level drops, so a second compaction in the same session still warns — a compaction changes neither session_id nor transcript path. Silent when autoCompactEnabled is false, when save and session have both already run (the same gate stopNudge uses, so the two hooks never nag together), and when usage exceeds the resolved window, which can only mean the window is wrong. Claude Code only — no-op on Codex. See docs/design-decisions.md, "Proactive nudges via hooks".

Fixed

  • Session scans read the session id Claude Code actually exports. session-scan.py looked for CLAUDE_SESSION_ID, which Claude Code never sets in a child process — it only expands that name as a ${...} placeholder inside skill text. Every model-invoked scan therefore fell through to the newest Codex rollout sharing the cwd and rendered it as the caller's own session, or, with no Codex installed, reported SESSION_ID: not available and left session and review working blind. It now reads CLAUDE_CODE_SESSION_ID first (CLAUDE_SESSION_ID stays a fallback), and prints a SOURCE: line whenever a scan does borrow an unattributed Codex rollout, so borrowed numbers can never pass for your own.
  • The prewarm cache is finally read back. The warm is keyed on (jsonl, session_id); because the hook seeded the legacy name and the model-invoked scan resolved a different one, the two keys diverged and the warmed entry was never hit — so TESTING.md's "first review is near-instant thanks to prewarm" was untrue on Claude Code. The hook now seeds the canonical name, and its run gate recognises a session carried only by the environment (hook stdin without session_id) instead of skipping the warm silently.
  • Cross-runtime recall detects Claude Code from the bash-tool environment. runtime-memory.py recognised only CLAUDE_SESSION_ID / CLAUDE_PLUGIN_ROOT, neither of which exists there, so an omitted runtime placeholder degraded quietly to zero hits. It now also accepts CLAUDE_CODE_SESSION_ID and CLAUDECODE. Related: CODEX_HOME no longer outranks a live Claude session — it only names a config directory, so exporting it no longer makes a Claude Code user look like a Codex one; live thread markers still win.

v1.2.13

Choose a tag to compare

@github-actions github-actions released this 26 Jul 17:34
c19f720

Changed

  • The handoff index rotates by calendar, not by line count. handoff.keepDays (default 31) is now the rule; handoff.maxLines is gone. At the measured pace of a live vault — 7.5 sessions/day, 234 in 31 days — the old 180-line ceiling silently delivered ~24 days under a config that read keepDays: 31. handoff.maxKB (56, up from 40) is a backstop underneath the window, sized from measurement: 234 lines × ~195 B ≈ 46 KiB, so a normal month fits with ~20% headroom and the note still opens in a single read. handoff.hardCapBytes folded into maxKB, so the config carries three knobs where it carried five.
  • When the window does not fit, the file says so. A month busier than maxKB allows drops its oldest pointers and states it in one > _overflow: line. A window that silently holds less than it promises is the failure this whole reform exists to remove; it must not reappear as a silent truncation.
  • Every index line is re-clipped, including ones written by hand. An inline open-item list (· open 4 (a · b · c) ·) is now recognised and trimmed — measured at 754 B and 881 B on the live vault against a 200 B ceiling. The [[Session — …]] link is still never truncated (median 106 B, max 177 B on that vault): only the project label gives way.

Added

  • relink-orphan-pointers.py — a pointer must have a live target. A migrated block with no [[Session — …]] produced a pointer reading (без session-заметки). That was cosmetic while the index kept everything; under a calendar window it is data loss, because eviction removes the block's last inbound link. Now such pointers are repointed at the exact archive part holding the block (10 of 186 blocks on the live vault, carrying 170 open items). The migration no longer creates them in the first place.

  • backfill-tails-from-archive.py — recently-archived tails move into their own session note. Pre-reform sessions wrote unfinished work into the shared handoff, so after migration those tails sat in cold parts the digest deliberately does not read. This is a deliberately narrow revisit of "don't rewrite the user's notes": it appends only genuinely missing items — measured, 105 of 136 recent tails already existed in their note under different wording, so 31 were appended, each marked with its block's date. Coverage is fuzzy on purpose (a literal comparison reports 0% where the truth is 77%), which also makes a second run a no-op.

  • The handoff maintenance scripts now ship with the plugin. migrate-handoff-to-index.py, split-handoff-archive.py, restore-handoff-from-bak.py and the two above moved from the repo into plugins/mnemo/scripts/, and health Step 7.6 offers them in dependency order when it finds a handoff still in block format. An installed user previously had no migration path at all: their handoff was in block format while the new session wrote pointers.

  • setup's config template carries the handoff, hot and hooks.hotDigest keys, so a fresh install learns about the window, the digest and their bounds instead of silently inheriting defaults.

  • docs/design-decisions.md — раздел про handoff, которого не было: почему индекс, что отвергнуто (архивировать сильнее, триаж ради размера, сжатие блоков, переписывание чужих заметок) и почему миграция остаётся отдельной ручной операцией. Non-goal hot.md переписан: отвергнут файл-кэш в волте, отгружен эфемерный вычисляемый дайджест.

Fixed

  • Откат миграции был no-op. restore-handoff-from-bak.py выбирал бэкап по mtime, а повторный прогон миграции пишет бэкап УЖЕ мигрированного файла — «отмена» восстанавливала индекс поверх индекса. Теперь кандидат обязан выглядеть до-миграционным (содержит ## YYYY-MM-DD блоки либо крупнее текущего файла), иначе скрипт требует явный --stamp.
  • Повторный split-handoff-archive.py --apply затирал уже разложенные части.bak делался только хабу, блоки прошлого прогона исчезали. Теперь при существующих частях скрипт отказывается работать без --force, а с ним бэкапит каждую часть. Плюс размер части проверяется ДО добавления блока (одна часть выходила на 204 819 B при потолке 204 800).
  • handoff-index-upsert молча терял указатель, если handoff ещё в блочном формате и больше потолка: цикл усечения выбрасывал и только что вставленную строку, возвращая ok. Теперь строка, ради которой сделан вызов, неприкосновенна, а неподъёмный легаси-файл получает явную ошибку «migrate first».
  • migrate --keep-blocks N при повторном прогоне дублировал указатели — гейт «блоков не осталось» не срабатывал, пока часть блоков намеренно оставлена.
  • Существующие строки индекса не переклипывались — правило длины применялось только к новой строке, поэтому запись от старой версии или руками жила вечно (на живом vault — 754 B при потолке 200).
  • Шаблон отчёта health украл хвост content-lint: блок 📮 Handoff был вставлен между заголовком и деталями, и правило о вердиктах читалось как часть handoff. Плюс Step 7.6 честно помечен legacy-only — на мигрированном handoff он видит 0 блоков.
  • Сиблинги контракта досвипаны: session-template.md, session/SKILL.md («unfinished threads become handoff items») и хардкод max_kb: 40 / keep_days: 14 вместо конфиг-плейсхолдеров.

v1.2.12

Choose a tag to compare

@github-actions github-actions released this 26 Jul 16:30
c9a762e

Changed

  • An unfinished tail now belongs to its own session note; the handoff gets a pointer. session Step 5 and depth-contract.md previously routed open threads into the shared handoff. Measured on a live vault, that left only 9% of fresh open items present in their own session note (34% for older ones) — the handoff had become the sole home of forward state, which is precisely how it reached 805 KiB and then could be neither read nor shrunk. A pointer line is bounded by the number of sessions; a copied tail is bounded by nothing. hot-scan (v1.2.11) collects the tails from where they now live, so the digest keeps working unchanged.

Added

  • handoff-index-upsert action in vault-write.py — writes one idempotent pointer line per session (- 2026-07-25 · mnemo · open 3 · [[Session — …]]), keyed on the session link so a mid-task checkpoint refreshes its own line instead of appending a twin. This also repairs a contract that was already broken: Step 5 demanded "the exact old section copied from read", but a large handoff read comes back truncated to a preview in which no complete section appears. Bounds — handoff.maxLines (180), handoff.maxLineBytes (200), handoff.hardCapBytes (38912) — are enforced in bytes, and trimming sacrifices the project label, never the [[Session — …]] link (a cut wikilink is a dead link). The block path (archive-handoff, mode=blocks) is untouched, along with its post-v1.1.11 regression suite.
  • scripts/migrate-handoff-to-index.py — one-shot migration, --dry-run by default. Moves blocks verbatim and whole into the archive (never checkbox-extracted: 90 flat bullets and ~200 prose lines on the live file carry live state with no - [ ]), never edits a session note, never deletes, writes .bak first, and verifies afterwards that every migrated block is byte-present in the archive. Measured on the live handoff: 826 916 B → 35 910 B (−95.7%), 186 pointer lines.
  • scripts/restore-handoff-from-bak.py — the rehearsed undo. The vault has no version control, so the migration's rollback is written and exercised before the migration may run; a restore itself saves a .pre-restore copy. Verified end-to-end on a copy of the live file: migrate → restore returns it byte-for-byte (md5 match).
  • scripts/split-handoff-archive.py — splits the cold archive into per-month notes plus a small hub. A cold file is not harmless: at 717 KiB it is past the 256 KB read limit, so "it's still in the archive" was a promise nothing could keep, and it matched nearly every content search while being unopenable. A month is not automatically a readable unit either — June alone was 425 KB — so oversized months split into numbered parts under --max-part-bytes (default 200 KB).
  • 43 new tests across test-handoff-index.py, test-migrate-handoff.py, and test-split-archive.py, pinning the safety contract itself: dry-run writes nothing, blocks move verbatim, nothing is lost, backups precede writes, restore is byte-exact. Mutation-tested: counting characters instead of bytes, or dropping the hard cap, each fails the suite.

v1.2.10

Choose a tag to compare

@github-actions github-actions released this 23 Jul 12:34
625fda7

Added

  • review --full can auto-apply connect's links (opt-in, review.full.autoConnect, default false). When the flag is on, the --full chain's connect step writes its suggested links without a per-suggestion prompt (connect Step 5.5) and reports every write — so closing a session no longer stops on "apply these? (y/N)". The user typing --full and setting the flag is the consent, mirroring review.lint.autoStampReviewed: it's the second, default-off exception to the non-destructive principle (docs/design-decisions.md). A standalone /mn:connect / $mnemo:connect never auto-applies regardless of the flag, and the default install is byte-for-byte unchanged (connect stays suggest-only). Dual-runtime by construction — the behavior lives in the shared SKILL.md prose that both Claude Code and Codex read; the Claude-only model: frontmatter is untouched. test-skill-write-contracts.py pins the new invariants (flag default false, standalone never auto-applies, verify pass still never links).

v1.2.9

Choose a tag to compare

@github-actions github-actions released this 20 Jul 19:28
c6b657a

Changed

  • Stop nudge now recommends the one-command close-out /mn:review --full instead of listing /mn:save and /mn:session separately — aligning the automatic end-of-session reminder with the v1.2.8 one-command close-out (Codex: $mnemo:review --full). The gating is unchanged: opt-in hooks.stopNudge, worth-saving signals ≥3, save/session not yet run, blocks at most once per session.

Fixed

  • Stop nudge over-firing in Codex — the anti-loop governor keyed only on the Stop payload's session_id, which a Codex Stop payload can omit or vary, so the once-per-session marker failed to dedup and the nudge could fire on every Stop. It now falls back to CODEX_THREAD_ID / CODEX_SESSION_ID for a stable per-thread key. New scripts/test-stop-nudge.py (7 cases) pins the --full recommendation, both runtimes' syntax, once-per-session dedup, the Codex thread-key fallback, and the silence conditions (below-threshold signals, save+session already ran, recursion guard).

v1.2.8

Choose a tag to compare

@github-actions github-actions released this 20 Jul 18:56
dc4da6f

Added

  • /mn:review --full — one-command session close-out. An explicit --full flag turns review into the whole end-of-session ritual with no per-skill prompt: the flag itself is consent (not the implicit autorun removed in v0.16.0 — the user types it). It anchors on the session's origin (reconstructs the first request, measures drift: discussed vs wanted vs did), audits the arc, chains save → session → [focus text] → connect, then runs a read-only verify pass. health is excluded (heavy — manual). Folds the four manual "be thorough / did we capture everything / what's left / find hidden links" prompts into built-in phases. Plain /mn:review is byte-for-byte unchanged (audit + one interactive offer, never auto-runs).
  • references/depth-contract.md — thoroughness by routing, not volume. A standing contract --full injects into its save/session steps (and session reads as its default bar): business-logic / pains / how-the-user-thinks route into save's typed principle/pain/stance atoms, links to connect, unfinished work to the handoff — never a "capture everything" blob. Encodes depth = structure (the v1.2.7 rule) across the whole close-out.
  • Grounded, idempotent verify guardrails. The verify pass cites only external facts (git diff, orphans, session-scan, a Step-0 snapshot), never self-grades; its non-orphan check is binary and never rewards link count (orphans delegate to connect; verify never links); it REPORTS a missing prod/e2e verification as an unchecked gap (memory-not-CI — it never runs QA); and a re-run on an unchanged session prints "already in order" and stops.

Changed

  • session is thorough-by-routing and self-checks its own note. Step 1 carries a standing depth-routing default (pointing at the depth-contract) so even a standalone /mn:session routes atom-worthy material to save instead of swelling the narrative; Step 7 adds an own-note self-check (dup / MOC / orphan / delegation). The cross-skill palace audit stays in review --full. review's description gains the --full triggers; evals/trigger-eval.json adds P9/P10 (positives) + N8 (near-miss); test-skill-write-contracts.py pins the new --full invariants (flag = consent not autorun, chain order, grounded-binary-never-links verify, depth-contract routing).

v1.2.7

Choose a tag to compare

@github-actions github-actions released this 20 Jul 17:14
fb6bc99

Added

  • First-class "how you think" capture (principle / pain / stance)save gains a semantic sub-type for the user's business logic, pains, and decision stance, piggybacking on the insight role (no taxonomy_roles schema change, so it works on every existing vault without re-setup). These are human-authored/confirmed atomic claims — never an agent-generated dossier — recorded with a searchable kind: field so [kind:pain] is enumerable in /mn:ask.
  • Typed body slots — each note's body now follows a template for its semantic type: decision → one Y-statement (context / choice / rejected / goal / trade-off / because); gotcha / business rule → GIVEN / WHEN / THEN + Because + Fails-when; principle / pain / stance → JTBD (Job / Pain / Done-well / Anti-goal); fact / insight → claim-title + BLUF first line + evidence. "Poured-in" polish achieved by structure, not verbosity.
  • Optional aliases: retrieval keys — EN/RU synonyms or a short name so a cross-language or short query still reaches an atom (a polyglot-vault search key, not authored content).

Changed

  • save splits deep material into atoms, never one blob — a new Step 0b atomicity gate requires a claim-shaped title plus a because rationale per typed note (hard-gated for decision / actionable rule / principle·pain·stance; a soft nudge for a plain fact). Material carrying ≥2 separable claims becomes ≥2 notes plus an optional synthesis, never one exhaustive note. This keeps point-precise retrieval intact and stays inside the human-authored, non-destructive principle — no auto-ingest. The save description and agents/openai.yaml presentation are updated with the new triggers; evals/trigger-eval.json adds W4 positives and a near-miss.