Memory Vault — Implementation Plan

Companion to design/memory-vault-design.html (the approved spec). This plan pins the code-level decisions: module layout, integration points (verified against the codebase 2026-07-18), and per-milestone file/test lists. Recon facts referenced as file:line.

0. New surface at a glance

jacked/memory/                    NEW package (mirrors jacked/packs.py idioms)
  __init__.py                     public API re-exports
  vault.py                        paths, init, vault.json mapping, group resolution,
                                  note schema + frontmatter, add, index, search, status, git ops
  capture.py                      SessionEnd/PreCompact triage, episodic writes, retry queue
  merge_capture.py                post-merge distillation (deterministic metadata + one claude -p)
  recall.py                       SessionStart brief assembly, token budget, ff-only sync
  rollup.py                       deterministic today→recent→archive
  migrate.py                      .remember import + count verification (never deletes)
  hooks_config.py                 settings.json hook entries: pure add/remove on a settings dict,
                                  shared by cli.py installer AND api/routes/features.py (no dup logic)
  githook.py                      post-merge script install/remove per repo (guardrails-style guards)
jacked/data/hooks/memory_capture.py    SessionEnd + PreCompact dispatch (async)
jacked/data/hooks/memory_recall.py     SessionStart synchronous stdout injection
jacked/data/git-hooks/post-merge-memory.sh
jacked/data/skills/memory-librarian/SKILL.md
jacked/data/commands/remember.md       jacked-owned /remember (plugin's is external)
jacked/cli.py                          `memory` Click group + install/uninstall wiring
jacked/api/routes/features.py          VALID_HOOKS += memory_vault; toggle branches
jacked/data/web/js/components/settings.js  card copy only if needed (hooks section auto-renders)

1. Conventions (binding, from recon)

2. Vault format (fixed by the design; code-level detail)

~/jacked-vault/
  README.md          agent-facing schema doc, written by init
  vault.json         {version:1, roots:["~/Github"], groups:{<g>:{repos:[<identity>...]}},
                      repo_map:{<identity>: <group>}}   identity = normalized remote URL else dir name
  groups/<g>/index.md      "- [title](decision/slug.md) - hook phrase (updated YYYY-MM-DD)" lines, soft cap 150
                          (plain hyphen separator: search echoes index lines to the CLI, so no em-dash)
  groups/<g>/hot.md        current slice, overwritten by librarian/skills
  groups/<g>/{decision,convention,vision,reference}/<slug>.md
  groups/<g>/episodic/<repo>/{today-YYYY-MM-DD.md, recent.md, archive.md, core-memories.md}
  strategy/  archive/

Note frontmatter (exact keys): type (decision|convention|vision|reference|progress), repos (list), group, created, updated (ISO date), tags (list; candidate marks unreviewed auto-captures). Episodic entry format preserved from the remember plugin so migration is lossless: ## HH:MM | branch entries in today files; # Recent + ## YYYY-MM-DD; # Archive + ## Week of YYYY-MM-DD (plugin regexes: consolidate.py:32, save-session.sh:192).

3. Milestones — files and tests

MCodeTests (all under tests/unit/, uv run python -m pytest)
M1 vault corejacked/memory/{__init__,vault}.py; cli.py memory group (init|status|add|search now; migrate|rollup stubs raise SystemExit(2) with "not yet available" is BANNED — instead register them in later milestones only). init: scan roots for .git dirs, cluster by remote org + name prefix, TTY review (--yes accepts), write vault.json + README + groups skeleton, git init + first commit. add: --type --group --repos --title --tags --body -|text, auto group/repos from cwd identity, validate frontmatter, write note, update index.md, bump drift counter, vault commit. search: case-insensitive tiered (hot → index → note bodies), prints path:line matches. status: vault path, groups, note counts, drift, hooks present, pending retries, last sync/rollup.test_memory_vault.py: init on fixture root (tmp repos w/ fake remotes) suggests groups; add writes valid note + index line + commit exists (git -C log); search finds by keyword across tiers; frontmatter round-trip; identity fallback (no remote → dir name); unmapped repo → solo group; JACKED_HOME/JACKED_VAULT_DIR isolation. CLI runner tests per test_cli_packs.py pattern.
M2 capture hooksjacked/memory/capture.py; data/hooks/memory_capture.py; cli.py _install_memory_hooks/_remove_memory_hooks using memory/hooks_config.py. SessionEnd: read stdin JSON (transcript_path, cwd, session_id); skip if <3 human messages; ONE claude -p triage (model from state) returning strict JSON {episodic: "one sentence", semantic: null | {type,title,body,tags}}; write episodic entry ALWAYS (deterministic fallback stub on triage failure + enqueue retry); write AT MOST one semantic note (tags += candidate). PreCompact(auto): handover episodic entry, promote decisions to candidates. Prompt default answer is "nothing".test_memory_capture_hook.py: fixture transcript → episodic file appears with ## HH:MM | branch entry; decision fixture (mock claude -p via subprocess monkeypatch) → exactly ONE semantic note; triage returning garbage → episodic stub + retry enqueued + exit 0; raised exception anywhere → main() returns, exit 0 (test_qa_hook.py stdin/capsys pattern); non-mapped cwd → solo group used, never a crash.
M3 post-mergejacked/memory/{merge_capture,githook}.py; data/git-hooks/post-merge-memory.sh (marker # jacked-memory-hook, backgrounds jacked memory capture-merge --repo "$PWD", unconditional exit 0). githook.py: install/remove per repo — .git required, refuse existing foreign post-merge unless --force, framework detection reused (guardrails.py:152-172), chmod +x (guardrails.py:245-251 pattern). capture-merge (hidden subcommand): only when HEAD is main/master; scan ORIG_HEAD..HEAD merge commits; per new PR/branch merge not in processed_merges: deterministic metadata (branch, PR title/body via gh pr view if available, changed files) + one distillation call → candidate decision note; dedupe by sha.test_memory_githook.py: template exists, #!/bin/sh, marker, exit 0; install into tmp repo writes executable .git/hooks/post-merge; refuses foreign hook without force; simulated merge in tmp repo (real git merge --no-ff) with mocked distillation → note with repos+group frontmatter; same merge twice → one note; pull on feature branch → no note.
M4 recalljacked/memory/recall.py; data/hooks/memory_recall.py (synchronous, stdout). Assembly order with hard 1500-token budget (len//4 estimate): imperative line reserved first ("IMPORTANT: before working on <group>, read <abs vault group path>"), hot.md, last episodic entry for cwd repo, top index decisions, index excerpt; trim lowest-priority first. Optional ff-only sync (see conventions). Drift nudge line when drift_added ≥ threshold. Silent (no output) when vault missing/disabled.test_memory_recall_hook.py: fixture vault → stdout under budget AND contains imperative with real existing path; oversized hot.md → trimmed, never over budget; missing vault → empty stdout exit 0; disabled state → empty; budget math unit tests; nudge appears past threshold, absent below.
M5 rollup + librarianjacked/memory/rollup.py + cli rollup + SessionEnd tail-call; data/skills/memory-librarian/SKILL.md (LLM guidance: dedupe, contradictions newest-wins-with-trail, archive stale hot, validate frontmatter, re-sync index; ends with hidden jacked memory mark-groomed). rollup: past today-*.md → recent.md per-day sections; recent >7d → archive.md week sections; idempotent; commits.test_memory_rollup.py: fixtures produce expected recent.md/archive.md exactly; idempotent re-run no-op; malformed today file tolerated (kept, logged, never dropped silently); mark-groomed resets drift.
M6 migratejacked/memory/migrate.py + cli migrate. Discover .remember/ dirs under vault roots + mapped repos; import today/recent/archive/core-memories/now → episodic/<repo>/ (tolerant parser: doubled # Recent headers, stray fences, Identity Candidates blocks — live-file anomalies are real); core-memories entries ALSO become candidate decision notes. VERIFY: per-source file count + entry count (## header regex) vs imported; print table; ANY mismatch → exit nonzero, vault changes rolled back (git reset of the migration commit is allowed ONLY on the vault repo commit we just made — implemented as: build in temp worktree dir, verify, then commit; never git-reset user data). NEVER touches source .remember/. Plugin retirement: prints exact summary, TTY click.confirm (default False), then disables via the existing enabledPlugins mechanism (features.py toggle_claude_plugin path); --keep-plugin skips.test_memory_migrate.py: fixture .remember (incl. malformed recent.md + .done.md files) migrates; counts printed + match; tampered fixture (entry removed post-count) → nonzero exit + no partial vault commit; sources untouched (mtime+content); core-memories → candidate notes; no TTY → plugin retirement skipped with notice.
M7 wiring + togglefeatures.py: VALID_HOOKS += "memory_vault"; hook_meta entry; _toggle_hook branch → jacked.memory.hooks_config + vault init on enable (non-interactive: auto-accept suggestions, response includes vault_path/groups/migration_available) + git hooks into mapped repos (collect per-repo results, refusals surfaced not forced); disable → remove settings hooks + our marker-verified git hooks, vault intact. _detect_hook_installed branch. cli.py install/uninstall parity (re-install hooks iff state.enabled, sounds pattern cli.py:2666/3394). Commands: NEW remember.md (routes durable facts → jacked memory add); learn.md, goal-maker.md, bhag.md, dcr.md, release.md, checkpoint/SKILL.md each get a short memory-vault routing block (milestone events → jacked memory add --type ... --tags milestone) that no-ops gracefully when the feature is off (guard: jacked memory status --quiet exit code).test_memory_features_toggle.py: PUT round-trip via TestClient (test_packs_endpoint.py hermetic pattern) → settings.json entries appear/disappear (markers), state.enabled flips, foreign hooks untouched; GET reflects installed state; guarded-app CSRF/host tests copied. test_web_js check only if custom markup added. Command-file assertions: routing block present in each edited md.
M8 shipREADME (features section + version history 0.83.0), dashboard copy, __init__.py 0.83.0, release notes. Gates: full pytest, /qa (dashboard card), /cso (Fable security audit — vault paths, settings mutation, subprocess, transcript handling), /dcr, CI mirror: uv run python -m pytest + python -m build (publish.yml build step). PR via gh, NOT merged.

4. Security / data-integrity invariants (test-enforced)