memgit 0.5.0__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {memgit-0.5.0 → memgit-0.6.0}/PKG-INFO +20 -7
  2. {memgit-0.5.0 → memgit-0.6.0}/README.md +19 -6
  3. {memgit-0.5.0 → memgit-0.6.0}/memgit/__init__.py +1 -1
  4. {memgit-0.5.0 → memgit-0.6.0}/memgit/cli.py +129 -21
  5. {memgit-0.5.0 → memgit-0.6.0}/memgit/delivery.py +34 -5
  6. {memgit-0.5.0 → memgit-0.6.0}/memgit/graph.py +5 -1
  7. {memgit-0.5.0 → memgit-0.6.0}/memgit/hooks.py +168 -3
  8. {memgit-0.5.0 → memgit-0.6.0}/memgit/http_server.py +13 -1
  9. memgit-0.6.0/memgit/links.py +249 -0
  10. {memgit-0.5.0 → memgit-0.6.0}/memgit/mcp_server.py +111 -24
  11. {memgit-0.5.0 → memgit-0.6.0}/memgit/repo.py +45 -2
  12. {memgit-0.5.0 → memgit-0.6.0}/memgit/toon.py +2 -1
  13. {memgit-0.5.0 → memgit-0.6.0}/memgit.egg-info/PKG-INFO +20 -7
  14. {memgit-0.5.0 → memgit-0.6.0}/memgit.egg-info/SOURCES.txt +3 -1
  15. {memgit-0.5.0 → memgit-0.6.0}/pyproject.toml +1 -1
  16. memgit-0.6.0/tests/test_v060.py +531 -0
  17. {memgit-0.5.0 → memgit-0.6.0}/LICENSE +0 -0
  18. {memgit-0.5.0 → memgit-0.6.0}/memgit/cloud/__init__.py +0 -0
  19. {memgit-0.5.0 → memgit-0.6.0}/memgit/cloud/client.py +0 -0
  20. {memgit-0.5.0 → memgit-0.6.0}/memgit/cloud/commands.py +0 -0
  21. {memgit-0.5.0 → memgit-0.6.0}/memgit/cloud/crypto.py +0 -0
  22. {memgit-0.5.0 → memgit-0.6.0}/memgit/cloud/state.py +0 -0
  23. {memgit-0.5.0 → memgit-0.6.0}/memgit/cloud/sync.py +0 -0
  24. {memgit-0.5.0 → memgit-0.6.0}/memgit/gitdigest.py +0 -0
  25. {memgit-0.5.0 → memgit-0.6.0}/memgit/importer.py +0 -0
  26. {memgit-0.5.0 → memgit-0.6.0}/memgit/models.py +0 -0
  27. {memgit-0.5.0 → memgit-0.6.0}/memgit/project.py +0 -0
  28. {memgit-0.5.0 → memgit-0.6.0}/memgit/scorer.py +0 -0
  29. {memgit-0.5.0 → memgit-0.6.0}/memgit/store.py +0 -0
  30. {memgit-0.5.0 → memgit-0.6.0}/memgit/tokens.py +0 -0
  31. {memgit-0.5.0 → memgit-0.6.0}/memgit/usage.py +0 -0
  32. {memgit-0.5.0 → memgit-0.6.0}/memgit.egg-info/dependency_links.txt +0 -0
  33. {memgit-0.5.0 → memgit-0.6.0}/memgit.egg-info/entry_points.txt +0 -0
  34. {memgit-0.5.0 → memgit-0.6.0}/memgit.egg-info/requires.txt +0 -0
  35. {memgit-0.5.0 → memgit-0.6.0}/memgit.egg-info/top_level.txt +0 -0
  36. {memgit-0.5.0 → memgit-0.6.0}/setup.cfg +0 -0
  37. {memgit-0.5.0 → memgit-0.6.0}/tests/test_accrue.py +0 -0
  38. {memgit-0.5.0 → memgit-0.6.0}/tests/test_advanced.py +0 -0
  39. {memgit-0.5.0 → memgit-0.6.0}/tests/test_aliases.py +0 -0
  40. {memgit-0.5.0 → memgit-0.6.0}/tests/test_core.py +0 -0
  41. {memgit-0.5.0 → memgit-0.6.0}/tests/test_delivery.py +0 -0
  42. {memgit-0.5.0 → memgit-0.6.0}/tests/test_setup.py +0 -0
  43. {memgit-0.5.0 → memgit-0.6.0}/tests/test_store_repo.py +0 -0
  44. {memgit-0.5.0 → memgit-0.6.0}/tests/test_toon.py +0 -0
  45. {memgit-0.5.0 → memgit-0.6.0}/tests/test_v020.py +0 -0
  46. {memgit-0.5.0 → memgit-0.6.0}/tests/test_v030.py +0 -0
  47. {memgit-0.5.0 → memgit-0.6.0}/tests/test_v040.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memgit
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Git for AI memory — version-controlled context persistence across Claude, GPT, Gemini, Cursor, Windsurf, and more
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://memgit.dev
@@ -44,7 +44,7 @@ Version-controlled, cross-AI context that persists, diffs, rolls back, and syncs
44
44
 
45
45
  [![PyPI](https://img.shields.io/pypi/v/memgit)](https://pypi.org/project/memgit/)
46
46
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
47
- [![Tests](https://img.shields.io/badge/tests-160%20passing-brightgreen)](tests/)
47
+ [![Tests](https://img.shields.io/badge/tests-245%20passing-brightgreen)](tests/)
48
48
 
49
49
  ---
50
50
 
@@ -215,13 +215,14 @@ memgit resume --json # for tooling
215
215
  Wire it into Claude Code so memory becomes **automatic** — no tool call, no judgment required:
216
216
 
217
217
  ```bash
218
- memgit setup hooks # installs all four hooks (~/.claude/settings.json)
218
+ memgit setup hooks # installs all five hooks (~/.claude/settings.json)
219
219
  ```
220
220
 
221
221
  | Hook | What it enforces |
222
222
  |---|---|
223
- | `SessionStart` | every session opens with the resume digest in context |
224
- | `UserPromptSubmit` | each prompt is BM25-matched against the store; relevant memories are injected (silent when nothing clears the relevance bar; never repeats within a session) — `--no-recall` to skip |
223
+ | `SessionStart` | every session opens with the resume digest in context — status board, checkpoints, critical rules, memory index |
224
+ | `UserPromptSubmit` | each prompt is BM25-matched against the store; relevant memories are injected, ending with a "+N more on '<topic>'" depth hint when more exists (silent when nothing clears the relevance bar; never repeats within a session) — `--no-recall` to skip |
225
+ | `PostToolUse` | reading a file whose path matches a memory tag surfaces a one-line hint ("6 memories tagged 'x' relate to this path") — tagmap cache only, capped 3/session, `--no-ctx-recall` to skip |
225
226
  | `Stop` (guard) | a session that did real work but saved nothing gets ONE nudge to save durable facts before finishing — `--no-guard` to skip |
226
227
  | `Stop` (sync) | markdown memories are checkpointed asynchronously at session end |
227
228
 
@@ -294,13 +295,25 @@ And it **learns**: a sidecar usage ledger tracks which memories actually get rec
294
295
 
295
296
  ---
296
297
 
298
+ ## Depth advertisement, trackers & supersession (v0.6.0)
299
+
300
+ Measured across 289 real sessions: injected recall reached ~59% of them, but only **6.8%** ever ran an active search — the injected top-3 reads as "memory consulted", so the model never learns there's a queryable store behind it. 0.6.0 makes the passive layer advertise what the active layer knows:
301
+
302
+ - **Memory index** — the resume digest ends with tag→count pairs (`8a8f4ec (6) · instagram (5)`) and the exact call to go deeper. Counts are truthful: superseded memories are excluded, and every advertised topic is guaranteed to return search results.
303
+ - **"+N more" recall hints** — when the per-prompt recall block has more on-topic memories behind it, it says so, with the one call to get them.
304
+ - **Context-triggered recall** — a `PostToolUse` hook: reading a file whose path matches a memory tag surfaces `memgit: 6 memories tagged 'x' relate to this path`. Reads only a commit-time tagmap cache (never the store), capped 3/session.
305
+ - **Trackers (`tr`)** — one memory per in-flight entity (`<entity>-status`), updated by re-saving the same slug. They render as a **status board** at the top of every session: memgit is the authority for entity status; files may lag.
306
+ - **Supersession** — a correction names what it replaces (`supersedes=[old-slug]`) instead of a "CORRECTED:" prefix. Superseded memories vanish from search/recall/resume (history preserved; `list` still shows them marked ⊘), so injected context is never stale.
307
+
308
+ ---
309
+
297
310
  ## Commands
298
311
 
299
312
  ```bash
300
313
  # Core (git-like)
301
314
  memgit init # initialize store (auto-detects best path)
302
315
  memgit onboard # bootstrap brief for an existing codebase
303
- memgit add <slug> <rule> # stage a memory (--body for full detail, --project to scope)
316
+ memgit add <slug> <rule> # stage a memory (--body detail, --project scope, --supersedes old-slug)
304
317
  memgit commit -m "message" # checkpoint current state
305
318
  memgit log # history
306
319
  memgit diff [sha1] [sha2] # what changed
@@ -432,7 +445,7 @@ git clone https://github.com/code4161/memgit.git
432
445
  cd memgit
433
446
  python -m venv .venv && source .venv/bin/activate
434
447
  pip install -e ".[dev]"
435
- pytest # 160 tests, all passing, < 3 seconds
448
+ pytest # 245 tests, all passing, < 5 seconds
436
449
  ```
437
450
 
438
451
  See [CONTRIBUTING.md](CONTRIBUTING.md).
@@ -10,7 +10,7 @@ Version-controlled, cross-AI context that persists, diffs, rolls back, and syncs
10
10
 
11
11
  [![PyPI](https://img.shields.io/pypi/v/memgit)](https://pypi.org/project/memgit/)
12
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
13
- [![Tests](https://img.shields.io/badge/tests-160%20passing-brightgreen)](tests/)
13
+ [![Tests](https://img.shields.io/badge/tests-245%20passing-brightgreen)](tests/)
14
14
 
15
15
  ---
16
16
 
@@ -181,13 +181,14 @@ memgit resume --json # for tooling
181
181
  Wire it into Claude Code so memory becomes **automatic** — no tool call, no judgment required:
182
182
 
183
183
  ```bash
184
- memgit setup hooks # installs all four hooks (~/.claude/settings.json)
184
+ memgit setup hooks # installs all five hooks (~/.claude/settings.json)
185
185
  ```
186
186
 
187
187
  | Hook | What it enforces |
188
188
  |---|---|
189
- | `SessionStart` | every session opens with the resume digest in context |
190
- | `UserPromptSubmit` | each prompt is BM25-matched against the store; relevant memories are injected (silent when nothing clears the relevance bar; never repeats within a session) — `--no-recall` to skip |
189
+ | `SessionStart` | every session opens with the resume digest in context — status board, checkpoints, critical rules, memory index |
190
+ | `UserPromptSubmit` | each prompt is BM25-matched against the store; relevant memories are injected, ending with a "+N more on '<topic>'" depth hint when more exists (silent when nothing clears the relevance bar; never repeats within a session) — `--no-recall` to skip |
191
+ | `PostToolUse` | reading a file whose path matches a memory tag surfaces a one-line hint ("6 memories tagged 'x' relate to this path") — tagmap cache only, capped 3/session, `--no-ctx-recall` to skip |
191
192
  | `Stop` (guard) | a session that did real work but saved nothing gets ONE nudge to save durable facts before finishing — `--no-guard` to skip |
192
193
  | `Stop` (sync) | markdown memories are checkpointed asynchronously at session end |
193
194
 
@@ -260,13 +261,25 @@ And it **learns**: a sidecar usage ledger tracks which memories actually get rec
260
261
 
261
262
  ---
262
263
 
264
+ ## Depth advertisement, trackers & supersession (v0.6.0)
265
+
266
+ Measured across 289 real sessions: injected recall reached ~59% of them, but only **6.8%** ever ran an active search — the injected top-3 reads as "memory consulted", so the model never learns there's a queryable store behind it. 0.6.0 makes the passive layer advertise what the active layer knows:
267
+
268
+ - **Memory index** — the resume digest ends with tag→count pairs (`8a8f4ec (6) · instagram (5)`) and the exact call to go deeper. Counts are truthful: superseded memories are excluded, and every advertised topic is guaranteed to return search results.
269
+ - **"+N more" recall hints** — when the per-prompt recall block has more on-topic memories behind it, it says so, with the one call to get them.
270
+ - **Context-triggered recall** — a `PostToolUse` hook: reading a file whose path matches a memory tag surfaces `memgit: 6 memories tagged 'x' relate to this path`. Reads only a commit-time tagmap cache (never the store), capped 3/session.
271
+ - **Trackers (`tr`)** — one memory per in-flight entity (`<entity>-status`), updated by re-saving the same slug. They render as a **status board** at the top of every session: memgit is the authority for entity status; files may lag.
272
+ - **Supersession** — a correction names what it replaces (`supersedes=[old-slug]`) instead of a "CORRECTED:" prefix. Superseded memories vanish from search/recall/resume (history preserved; `list` still shows them marked ⊘), so injected context is never stale.
273
+
274
+ ---
275
+
263
276
  ## Commands
264
277
 
265
278
  ```bash
266
279
  # Core (git-like)
267
280
  memgit init # initialize store (auto-detects best path)
268
281
  memgit onboard # bootstrap brief for an existing codebase
269
- memgit add <slug> <rule> # stage a memory (--body for full detail, --project to scope)
282
+ memgit add <slug> <rule> # stage a memory (--body detail, --project scope, --supersedes old-slug)
270
283
  memgit commit -m "message" # checkpoint current state
271
284
  memgit log # history
272
285
  memgit diff [sha1] [sha2] # what changed
@@ -398,7 +411,7 @@ git clone https://github.com/code4161/memgit.git
398
411
  cd memgit
399
412
  python -m venv .venv && source .venv/bin/activate
400
413
  pip install -e ".[dev]"
401
- pytest # 160 tests, all passing, < 3 seconds
414
+ pytest # 245 tests, all passing, < 5 seconds
402
415
  ```
403
416
 
404
417
  See [CONTRIBUTING.md](CONTRIBUTING.md).
@@ -1,3 +1,3 @@
1
1
  """memgit — git for AI memory."""
2
2
 
3
- __version__ = "0.5.0"
3
+ __version__ = "0.6.0"
@@ -152,9 +152,10 @@ def init(directory):
152
152
  @click.argument('slug')
153
153
  @click.argument('rule')
154
154
  @click.option('--type', '-t', 'type_code', default='fb',
155
- type=click.Choice(['fb', 'us', 'pj', 'rf', 'cn', 'lx', 'co']),
155
+ type=click.Choice(['fb', 'us', 'pj', 'rf', 'cn', 'lx', 'co', 'tr']),
156
156
  help='fb=feedback us=user pj=project rf=reference cn=convention '
157
- 'lx=lesson co=core (prefer `memgit core set` for co)')
157
+ 'lx=lesson co=core (prefer `memgit core set` for co) '
158
+ 'tr=tracker (live status of one entity; slug <entity>-status)')
158
159
  @click.option('--why', '-w', default=None, help='Reasoning / why this rule exists')
159
160
  @click.option('--when', '-W', default=None, help='When / where to apply')
160
161
  @click.option('--tags', default=None, help='Comma-separated tags')
@@ -165,7 +166,14 @@ def init(directory):
165
166
  @click.option('--project', '-P', default=None,
166
167
  help='Project this memory belongs to (default: derived from the '
167
168
  'current directory; pass "" for a global memory)')
168
- def add(slug, rule, type_code, why, when, tags, priority, body, project):
169
+ @click.option('--supersedes', default=None,
170
+ help='Comma-separated slugs this memory REPLACES (corrections, '
171
+ 'updated decisions) — superseded memories stop surfacing in '
172
+ 'search/recall/resume')
173
+ @click.option('--related', default=None,
174
+ help='Comma-separated slugs of related memories')
175
+ def add(slug, rule, type_code, why, when, tags, priority, body, project,
176
+ supersedes, related):
169
177
  """Add or update a mnemonic.
170
178
 
171
179
  SLUG kebab-case identifier (e.g. ig-pipeline-no-fallback)\n
@@ -183,6 +191,12 @@ def add(slug, rule, type_code, why, when, tags, priority, body, project):
183
191
  elif not project.strip():
184
192
  project = None
185
193
 
194
+ from .links import validate_relations
195
+ sup_list, rel_list, warnings = validate_relations(
196
+ slug, supersedes, related, repo.list())
197
+ for w in warnings:
198
+ err.print(f'[yellow]{w}[/yellow]')
199
+
186
200
  m = Mnemonic(
187
201
  type_code=type_code,
188
202
  slug=slug,
@@ -194,10 +208,13 @@ def add(slug, rule, type_code, why, when, tags, priority, body, project):
194
208
  priority=priority,
195
209
  body=body,
196
210
  project=project,
211
+ supersedes=sup_list,
212
+ related=rel_list,
197
213
  )
198
214
  sha = repo.add(m)
199
215
  from rich.markup import escape as _mesc
200
- console.print(f'[green]staged[/green] {_mesc(m.slug)} {_mesc("[" + sha[:8] + "]")}')
216
+ sup_note = f' supersedes {len(sup_list)}' if sup_list else ''
217
+ console.print(f'[green]staged[/green] {_mesc(m.slug)} {_mesc("[" + sha[:8] + "]")}{sup_note}')
201
218
 
202
219
 
203
220
  # ── remove ────────────────────────────────────────────────────────────────────
@@ -540,6 +557,14 @@ def resume(checkpoints, recent, plain, fmt_json, project):
540
557
  console.print(f' [red]removed[/red] {s}')
541
558
  console.print()
542
559
 
560
+ if ctx.get('tracker_memories'):
561
+ console.print('[bold]Status board (live entity state):[/bold]')
562
+ for m in ctx['tracker_memories']:
563
+ ts = m['timestamp'].strftime('%m-%d')
564
+ rule = m['rule'][:80] + '..' if len(m['rule']) > 80 else m['rule']
565
+ console.print(f' [red]●[/red] [cyan]{m["slug"]}[/cyan] [dim](upd {ts})[/dim] {rule}')
566
+ console.print()
567
+
543
568
  console.print('[bold]Last checkpoints:[/bold]')
544
569
  for ck in ctx['checkpoints']:
545
570
  ts = ck['timestamp'].strftime('%Y-%m-%d %H:%M')
@@ -557,6 +582,10 @@ def resume(checkpoints, recent, plain, fmt_json, project):
557
582
  console.print('\n[bold red]Critical rules (always apply):[/bold red]')
558
583
  for m in ctx['critical_memories']:
559
584
  console.print(f' [red]![/red] [cyan]{m["slug"]}[/cyan] {m["rule"]}')
585
+ if ctx.get('entity_index'):
586
+ idx = ' · '.join(f'{tag} ({n})' for tag, n in ctx['entity_index'])
587
+ console.print(f'\n[bold]Memory index:[/bold] {idx} '
588
+ f'[dim](memgit search "<topic>")[/dim]')
560
589
  if ctx.get('maintenance'):
561
590
  console.print(f'\n[yellow]maintenance:[/yellow] {ctx["maintenance"]}')
562
591
  console.print()
@@ -589,6 +618,19 @@ def _format_resume_plain(ctx: dict) -> str:
589
618
  body = (m.get('body') or m['rule']).rstrip()
590
619
  lines.append('')
591
620
  lines.append(body)
621
+ # Status board: live entity state. Rendered right after the guide because
622
+ # orientation ("what is the CURRENT state of things") is worth more than
623
+ # history. The freshness stamp is what tells the model to trust or
624
+ # re-verify a line.
625
+ if ctx.get('tracker_memories'):
626
+ lines.append('')
627
+ lines.append('## Status board — live entity state '
628
+ '(memgit is authoritative; files may lag)')
629
+ for m in ctx['tracker_memories']:
630
+ ts = m['timestamp'].strftime('%m-%d')
631
+ lines.append(f'- {m["slug"]} (upd {ts}): {clip(m["rule"], 160)}')
632
+ lines.append('(State changed? Update the tracker: save_memory with the '
633
+ 'same slug.)')
592
634
  st = ctx['staged']
593
635
  if st['new'] or st['updated'] or st['removed']:
594
636
  lines.append('')
@@ -634,9 +676,26 @@ def _format_resume_plain(ctx: dict) -> str:
634
676
  'digest + seeding brief, then save 10-20 durable facts '
635
677
  '(purpose, architecture, conventions, state, gotchas) via save_memory.'
636
678
  )
679
+ if ctx.get('core_missing'):
680
+ lines.append('')
681
+ lines.append(
682
+ '(No core operating guide for this project yet — seed it once: '
683
+ '`memgit core seed`, review with `memgit core show`, then '
684
+ '`memgit core sync` to deliver it to every AI tool.)'
685
+ )
686
+ # Entity index LAST: recency position is where a model decides its next
687
+ # action, and this section exists purely to convert reading into querying.
688
+ if ctx.get('entity_index'):
689
+ lines.append('')
690
+ lines.append('## Memory index — depth beyond this digest')
691
+ lines.append('- ' + ' · '.join(f'{tag} ({n})'
692
+ for tag, n in ctx['entity_index']))
693
+ lines.append('(each is one call away: search_memories("<topic>", top_k=10))')
637
694
  lines.append('')
638
- lines.append('(Check work-in-flight and the last checkpoints before assuming state; '
639
- 'use memgit search for anything task-specific.)')
695
+ lines.append('(This digest is a teaser, not the memory. Trust the status board '
696
+ 'over files for entity state; check work-in-flight and the last '
697
+ 'checkpoints before assuming state. Anything deeper: '
698
+ 'search_memories("<topic from the index above>").)')
640
699
  return '\n'.join(lines)
641
700
 
642
701
 
@@ -661,6 +720,13 @@ def hook_stop_guard():
661
720
  sys.exit(stop_guard())
662
721
 
663
722
 
723
+ @hook.command('context-recall')
724
+ def hook_context_recall():
725
+ """PostToolUse: hint when memories exist about the file being read (stdin JSON)."""
726
+ from .hooks import context_recall
727
+ sys.exit(context_recall())
728
+
729
+
664
730
  # ── onboard ───────────────────────────────────────────────────────────────────
665
731
 
666
732
  ONBOARD_BRIEF = """\
@@ -683,6 +749,9 @@ Extract 10–20 DURABLE facts about this project and save each one as a memory
683
749
  - `rf` reference: key entry points, dashboards, external services, URLs
684
750
  - `fb` feedback: known constraints ("never touch X", "Y is production")
685
751
  - `lx` lesson: past incidents or gotchas documented in the repo
752
+ - `tr` tracker: LIVE status of each in-flight entity (a deploy, a migration,
753
+ a draft) — slug `<entity>-status`; update it by re-saving the same slug.
754
+ memgit is the authority for this status; files may lag.
686
755
 
687
756
  Rules for good memories: one fact per memory; kebab-case slug; a one-line
688
757
  `rule` stating the fact; details in `body`; set `project` to "{project}";
@@ -936,21 +1005,31 @@ def show(slug, toon, fmt_markdown):
936
1005
  console.print(f'[dim]Related: {_mesc(", ".join(m.related))}[/dim]')
937
1006
  if m.supersedes:
938
1007
  console.print(f'[dim]Supersedes: {_mesc(", ".join(m.supersedes))}[/dim]')
1008
+ from .links import superseded_by, resolve_head
1009
+ all_mems = repo.list()
1010
+ heirs = superseded_by(m.slug, all_mems)
1011
+ if heirs:
1012
+ head = resolve_head(m.slug, all_mems)
1013
+ console.print(f'[yellow]⊘ SUPERSEDED by: {_mesc(", ".join(heirs))}'
1014
+ f' — current head: {_mesc(head)}[/yellow]')
939
1015
 
940
1016
 
941
1017
  # ── list ──────────────────────────────────────────────────────────────────────
942
1018
 
943
1019
  @cli.command(name='list')
944
1020
  @click.option('--type', '-t', 'type_filter', default=None,
945
- type=click.Choice(['fb', 'us', 'pj', 'rf', 'cn', 'lx']),
1021
+ type=click.Choice(['fb', 'us', 'pj', 'rf', 'cn', 'lx', 'co', 'tr']),
946
1022
  help='Filter by type')
947
1023
  @click.option('--priority', '-p', default=None, type=click.IntRange(1, 3), help='Filter by priority')
948
1024
  @click.option('--project', '-P', 'project_filter', default=None, help='Filter by project')
949
1025
  @click.option('--toon', is_flag=True, help='Show TOON format')
950
1026
  def list_cmd(type_filter, priority, project_filter, toon):
951
- """List all mnemonics in the current thread."""
1027
+ """List all mnemonics in the current thread (the audit view — superseded
1028
+ memories are shown, marked ⊘)."""
1029
+ from .links import superseded_slugs
952
1030
  repo = _require_repo()
953
1031
  mnemonics = repo.list()
1032
+ hidden = superseded_slugs(mnemonics)
954
1033
  if type_filter:
955
1034
  mnemonics = [m for m in mnemonics if m.type_code == type_filter]
956
1035
  if priority:
@@ -980,10 +1059,14 @@ def list_cmd(type_filter, priority, project_filter, toon):
980
1059
  p_str = '!' if m.priority == 3 else str(m.priority)
981
1060
  proj = (m.project or '')[:18]
982
1061
  rule_preview = m.rule[:58] + '..' if len(m.rule) > 58 else m.rule
1062
+ if m.slug in hidden:
1063
+ rule_preview = '⊘ ' + rule_preview
983
1064
  table.add_row(m.slug, m.type_code, p_str, proj, rule_preview)
984
1065
 
985
1066
  console.print(table)
986
- console.print(f'\n[dim]{len(mnemonics)} mnemonic{"s" if len(mnemonics) != 1 else ""}[/dim]')
1067
+ shown_hidden = sum(1 for m in mnemonics if m.slug in hidden)
1068
+ hid_note = f' · {shown_hidden} superseded (⊘)' if shown_hidden else ''
1069
+ console.print(f'\n[dim]{len(mnemonics)} mnemonic{"s" if len(mnemonics) != 1 else ""}{hid_note}[/dim]')
987
1070
 
988
1071
 
989
1072
  # ── import ────────────────────────────────────────────────────────────────────
@@ -1183,11 +1266,15 @@ import re # noqa: E402 — needed for lint command
1183
1266
  @click.option('--toon', is_flag=True, help='Output TOON format (token-efficient)')
1184
1267
  @click.option('--json', 'fmt_json', is_flag=True, help='Output JSON')
1185
1268
  @click.option('--type', '-t', 'type_filter', default=None,
1186
- type=click.Choice(['fb', 'us', 'pj', 'rf', 'cn', 'lx']),
1269
+ type=click.Choice(['fb', 'us', 'pj', 'rf', 'cn', 'lx', 'co', 'tr']),
1187
1270
  help='Filter by type before scoring')
1188
1271
  @click.option('--project', '-P', 'project_filter', default=None,
1189
1272
  help='Only memories from this project (as shown in `memgit list`)')
1190
- def search(query, top, toon, fmt_json, type_filter, project_filter):
1273
+ @click.option('--include-superseded', is_flag=True,
1274
+ help='Also score memories that a newer memory supersedes '
1275
+ '(hidden by default)')
1276
+ def search(query, top, toon, fmt_json, type_filter, project_filter,
1277
+ include_superseded):
1191
1278
  """Search memories by relevance.
1192
1279
 
1193
1280
  Returns the top-k mnemonics scored against QUERY using BM25.
@@ -1197,6 +1284,9 @@ def search(query, top, toon, fmt_json, type_filter, project_filter):
1197
1284
 
1198
1285
  repo = _require_repo()
1199
1286
  mnemonics = repo.list()
1287
+ if not include_superseded:
1288
+ from .links import filter_active
1289
+ mnemonics = filter_active(mnemonics)
1200
1290
  if type_filter:
1201
1291
  mnemonics = [m for m in mnemonics if m.type_code == type_filter]
1202
1292
  if project_filter:
@@ -1359,7 +1449,7 @@ def graph(output, auto_open):
1359
1449
  """Generate an interactive HTML graph of the memory store.
1360
1450
 
1361
1451
  Visualizes all mnemonics as a force-directed graph with:
1362
- - Nodes colored by type (fb/us/pj/rf/cn/lx)
1452
+ - Nodes colored by type (fb/us/pj/rf/cn/lx/co/tr)
1363
1453
  - Node size by priority
1364
1454
  - Edges from [[wikilink]] references and explicit related/supersedes links
1365
1455
  - Filter by type, search by keyword, click to highlight neighbours
@@ -1468,7 +1558,8 @@ def stats(fmt_json):
1468
1558
  return
1469
1559
 
1470
1560
  type_labels = {'fb': 'feedback', 'us': 'user', 'pj': 'project',
1471
- 'rf': 'reference', 'cn': 'convention', 'lx': 'lesson'}
1561
+ 'rf': 'reference', 'cn': 'convention', 'lx': 'lesson',
1562
+ 'co': 'core', 'tr': 'tracker'}
1472
1563
 
1473
1564
  type_str = ' · '.join(
1474
1565
  f"{s['by_type'].get(tc, 0)} {lbl}"
@@ -2245,7 +2336,8 @@ def setup_gemini_cli(dry_run):
2245
2336
 
2246
2337
 
2247
2338
  #: substrings identifying a hook command as one of ours (any generation)
2248
- _MEMGIT_HOOK_SIGNS = ('resume --plain', 'hook prompt-recall', 'hook stop-guard', ' sync')
2339
+ _MEMGIT_HOOK_SIGNS = ('resume --plain', 'hook prompt-recall', 'hook stop-guard',
2340
+ 'hook context-recall', ' sync')
2249
2341
 
2250
2342
 
2251
2343
  def _is_memgit_hook_entry(h: dict) -> bool:
@@ -2262,19 +2354,25 @@ def _is_memgit_hook_entry(h: dict) -> bool:
2262
2354
  help='Skip the per-prompt auto-recall hook (UserPromptSubmit)')
2263
2355
  @click.option('--no-guard', is_flag=True,
2264
2356
  help='Skip the end-of-session capture guard (Stop)')
2357
+ @click.option('--no-ctx-recall', is_flag=True,
2358
+ help='Skip the context-recall hook (PostToolUse on Read/Grep/Glob)')
2265
2359
  @click.option('--dry-run', is_flag=True, help='Show the change without writing')
2266
- def setup_hooks(remove, no_recall, no_guard, dry_run):
2360
+ def setup_hooks(remove, no_recall, no_guard, no_ctx_recall, dry_run):
2267
2361
  """Install the Claude Code hooks that make memory automatic.
2268
2362
 
2269
- Four hooks, one principle: what a hook enforces happens, what a tool
2363
+ Five hooks, one principle: what a hook enforces happens, what a tool
2270
2364
  description suggests mostly doesn't (measured: 6% voluntary engagement
2271
2365
  vs 100% hook delivery).
2272
2366
 
2273
2367
  \b
2274
- SessionStart inject `memgit resume` — last checkpoints, work in
2275
- flight, critical rules
2368
+ SessionStart inject `memgit resume` — status board, last
2369
+ checkpoints, work in flight, critical rules,
2370
+ memory index
2276
2371
  UserPromptSubmit inject memories relevant to each prompt (BM25,
2277
2372
  silent when nothing clears the relevance bar)
2373
+ PostToolUse context recall — reading a file whose path matches
2374
+ a memory tag surfaces a one-line depth hint
2375
+ (tagmap cache only, capped 3/session)
2278
2376
  Stop capture guard — a substantive session ending with
2279
2377
  zero memory writes gets ONE nudge to save durable
2280
2378
  facts; plus async `memgit sync` to checkpoint
@@ -2313,6 +2411,10 @@ def setup_hooks(remove, no_recall, no_guard, dry_run):
2313
2411
  {'type': 'command',
2314
2412
  'command': f'{base} hook prompt-recall 2>/dev/null || true'},
2315
2413
  ],
2414
+ 'PostToolUse': [] if no_ctx_recall else [
2415
+ {'type': 'command',
2416
+ 'command': f'{base} hook context-recall 2>/dev/null || true'},
2417
+ ],
2316
2418
  'Stop': ([] if no_guard else [
2317
2419
  {'type': 'command',
2318
2420
  'command': f'{base} hook stop-guard 2>/dev/null || true'},
@@ -2322,14 +2424,20 @@ def setup_hooks(remove, no_recall, no_guard, dry_run):
2322
2424
  'async': True},
2323
2425
  ],
2324
2426
  }
2427
+ #: PostToolUse fires per tool call — matcher limits it to the file-reading
2428
+ #: tools so the hook binary isn't spawned on every Bash/Edit.
2429
+ _MATCHERS = {'PostToolUse': 'Read|Grep|Glob'}
2325
2430
 
2326
2431
  changed = []
2327
- for event in ('SessionStart', 'UserPromptSubmit', 'Stop'):
2432
+ for event in ('SessionStart', 'UserPromptSubmit', 'PostToolUse', 'Stop'):
2328
2433
  entries = hooks.setdefault(event, [])
2329
2434
  had = [h for h in entries if isinstance(h, dict) and _is_memgit_hook_entry(h)]
2330
2435
  entries[:] = [h for h in entries if h not in had]
2331
2436
  if not remove and plan[event]:
2332
- entries.append({'hooks': plan[event]})
2437
+ entry: dict = {'hooks': plan[event]}
2438
+ if event in _MATCHERS:
2439
+ entry['matcher'] = _MATCHERS[event]
2440
+ entries.append(entry)
2333
2441
  changed.append(event)
2334
2442
  if not entries:
2335
2443
  hooks.pop(event, None)
@@ -2349,7 +2457,7 @@ def setup_hooks(remove, no_recall, no_guard, dry_run):
2349
2457
  tag = ' [dim](async)[/dim]' if inner.get('async') else ''
2350
2458
  console.print(f' [cyan]{event}[/cyan] [dim]{inner["command"]}[/dim]{tag}')
2351
2459
  console.print('[dim]Resume at session start, relevant memories per prompt, '
2352
- 'capture guard + sync at stop.[/dim]')
2460
+ 'context hints on file reads, capture guard + sync at stop.[/dim]')
2353
2461
 
2354
2462
 
2355
2463
  @setup.command('print-config')
@@ -122,13 +122,29 @@ def _upsert_marker_block(existing: str, block_body: str) -> str:
122
122
  # ── seed: ingest existing host skills/rules into a draft core guide ───────────
123
123
 
124
124
  def _frontmatter_field(text: str, key: str) -> Optional[str]:
125
- """Pull a top-level `key: value` from a leading `---` YAML frontmatter."""
125
+ """Pull a top-level `key: value` from a leading `---` YAML frontmatter.
126
+
127
+ Handles folded/literal block scalars (`key: >` / `key: |`): the value is
128
+ the first indented line that follows — one line is enough for a routing
129
+ entry, and a real YAML parser is not worth the dependency here.
130
+ """
126
131
  if not text.startswith("---"):
127
132
  return None
128
133
  end = text.find("\n---", 3)
129
134
  block = text[3:end] if end != -1 else text[3:]
130
- m = re.search(rf"^{re.escape(key)}:\s*(.+)$", block, flags=re.M)
131
- return m.group(1).strip().strip('"\'') if m else None
135
+ m = re.search(rf"^{re.escape(key)}:\s*(.*)$", block, flags=re.M)
136
+ if not m:
137
+ return None
138
+ value = m.group(1).strip().strip('"\'')
139
+ if value in (">", "|", ">-", "|-", ""):
140
+ tail = block[m.end():]
141
+ for line in tail.splitlines():
142
+ if line.strip() and line[:1] in (" ", "\t"):
143
+ return line.strip()
144
+ if line.strip(): # next top-level key — no scalar body
145
+ break
146
+ return None
147
+ return value
132
148
 
133
149
 
134
150
  #: (label, glob) pairs — where each host keeps model-invoked skills.
@@ -173,7 +189,13 @@ def build_seed(root: Path, home: Optional[Path] = None) -> str:
173
189
  "## memgit",
174
190
  "- Before answering anything that depends on past work, call resume/search "
175
191
  "(memgit) — the record of prior sessions lives there, not in this file.",
176
- "- Save durable facts/decisions/lessons as you learn them.",
192
+ "- memgit is the authority for entity STATUS ('tr' tracker memories: "
193
+ "deploys, drafts, migrations, campaigns); files and READMEs are "
194
+ "downstream and may lag. Changed an entity's state? Update its "
195
+ "<entity>-status tracker (save, same slug).",
196
+ "- Save durable facts/decisions/lessons as you learn them. A memory "
197
+ "that corrects an old one should supersede it (supersedes=[old-slug]), "
198
+ "not sit beside it.",
177
199
  ]
178
200
 
179
201
  skills = collect_skills(root, home)
@@ -235,11 +257,18 @@ def compute_auto_section(repo, project, now, curated: str = "") -> str:
235
257
  if not usage:
236
258
  return ""
237
259
  candidates = []
238
- for m in repo.list():
260
+ from .links import superseded_slugs
261
+ all_mems = repo.list()
262
+ hidden = superseded_slugs(all_mems)
263
+ for m in all_mems:
239
264
  if m.type_code in ("co", "cn"): # skip core + conventions (rules)
240
265
  continue
266
+ if m.type_code == "tr": # skip trackers — live state must
267
+ continue # never fossilize in static files
241
268
  if m.priority == 3: # skip always-on criticals (rules)
242
269
  continue
270
+ if m.slug in hidden: # skip superseded (stale by definition)
271
+ continue
243
272
  if project and project_affinity(m.project, project) < 1:
244
273
  continue
245
274
  if not project and m.project:
@@ -17,6 +17,8 @@ _TYPE_COLOR = {
17
17
  "rf": "#8b5cf6", # violet — reference
18
18
  "cn": "#eab308", # yellow — convention
19
19
  "lx": "#ec4899", # pink — lesson
20
+ "co": "#64748b", # slate — core guide
21
+ "tr": "#ef4444", # red — tracker (live status)
20
22
  }
21
23
  _TYPE_LABEL = {
22
24
  "fb": "feedback",
@@ -25,6 +27,8 @@ _TYPE_LABEL = {
25
27
  "rf": "reference",
26
28
  "cn": "convention",
27
29
  "lx": "lesson",
30
+ "co": "core",
31
+ "tr": "tracker",
28
32
  }
29
33
 
30
34
  _WIKILINK_RE = re.compile(r'\[\[([a-z0-9_-]+)\]\]', re.IGNORECASE)
@@ -372,7 +376,7 @@ window.addEventListener('resize', () => {
372
376
  const tooltip = document.getElementById('tooltip');
373
377
 
374
378
  node.on('mouseover', (event, d) => {
375
- const tags = d.tags.filter(t => !['fb','pj','us','rf','cn','lx'].includes(t));
379
+ const tags = d.tags.filter(t => !['fb','pj','us','rf','cn','lx','co','tr'].includes(t));
376
380
  tooltip.innerHTML = `
377
381
  <div class="t-slug">${d.id}</div>
378
382
  <div class="t-type">[${d.type}] ${d.type_label} · priority ${d.priority}</div>