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.
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)
jacked_home()-style $JACKED_HOME override (packs.py:93). Vault dir: Path(os.getenv("JACKED_VAULT_DIR") or jacked_home() / "jacked-vault"). State file: <home>/.claude/jacked-memory.json.os.replace save — packs.py:200-294). Keys: version, enabled, vault_dir, triage_model ("haiku" default, user-overridable), drift_added, drift_threshold (15), last_rollup, last_sync, retry_queue [], processed_merges {sha: iso} (age-pruned 90d, never count-capped).main() never raises out — top-level try/except, always effective exit 0 (pattern: session_account_tracker.py; unknown-hook shim already exits 0, cli.py:959-965). Slow work in a daemon thread with t.join(timeout=...) only where the event allows; SessionEnd capture may run in-process (session is ending) but MUST swallow all exceptions.# jacked-memory-capture / # jacked-memory-recall; match-then-update-in-place, never blind append, never touch foreign entries (cli.py:1653-1667 pattern); write via _write_settings_atomic (cli.py:1135). Recall hook is SYNCHRONOUS (no async key — async stdout is not injected, cli.py:1791-1801). Capture entries carry "async": True. Events: ("SessionEnd",""), ("PreCompact","auto") for capture; ("SessionStart","") for recall.@main.group(name="memory") + lazy from jacked import memory imports inside command bodies (packs group template, cli.py:3648). Rich console + _rich_escape for arbitrary text; click.confirm gated on sys.stdin.isatty().git -C <vault>; init creates the repo; every add/rollup/migrate commits. Sync is git pull --ff-only ONLY, timeout-guarded, skipped unless a remote exists and last_sync > 10 min ago; failure = skip + record in state (surfaced by status and the dashboard card). Never reset/checkout -f.~/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).
| M | Code | Tests (all under tests/unit/, uv run python -m pytest) |
|---|---|---|
| M1 vault core | jacked/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 hooks | jacked/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-merge | jacked/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 recall | jacked/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 + librarian | jacked/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 migrate | jacked/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 + toggle | features.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 ship | README (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. | — |