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.
- {memgit-0.5.0 → memgit-0.6.0}/PKG-INFO +20 -7
- {memgit-0.5.0 → memgit-0.6.0}/README.md +19 -6
- {memgit-0.5.0 → memgit-0.6.0}/memgit/__init__.py +1 -1
- {memgit-0.5.0 → memgit-0.6.0}/memgit/cli.py +129 -21
- {memgit-0.5.0 → memgit-0.6.0}/memgit/delivery.py +34 -5
- {memgit-0.5.0 → memgit-0.6.0}/memgit/graph.py +5 -1
- {memgit-0.5.0 → memgit-0.6.0}/memgit/hooks.py +168 -3
- {memgit-0.5.0 → memgit-0.6.0}/memgit/http_server.py +13 -1
- memgit-0.6.0/memgit/links.py +249 -0
- {memgit-0.5.0 → memgit-0.6.0}/memgit/mcp_server.py +111 -24
- {memgit-0.5.0 → memgit-0.6.0}/memgit/repo.py +45 -2
- {memgit-0.5.0 → memgit-0.6.0}/memgit/toon.py +2 -1
- {memgit-0.5.0 → memgit-0.6.0}/memgit.egg-info/PKG-INFO +20 -7
- {memgit-0.5.0 → memgit-0.6.0}/memgit.egg-info/SOURCES.txt +3 -1
- {memgit-0.5.0 → memgit-0.6.0}/pyproject.toml +1 -1
- memgit-0.6.0/tests/test_v060.py +531 -0
- {memgit-0.5.0 → memgit-0.6.0}/LICENSE +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/memgit/cloud/__init__.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/memgit/cloud/client.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/memgit/cloud/commands.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/memgit/cloud/crypto.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/memgit/cloud/state.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/memgit/cloud/sync.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/memgit/gitdigest.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/memgit/importer.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/memgit/models.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/memgit/project.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/memgit/scorer.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/memgit/store.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/memgit/tokens.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/memgit/usage.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/memgit.egg-info/dependency_links.txt +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/memgit.egg-info/entry_points.txt +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/memgit.egg-info/requires.txt +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/memgit.egg-info/top_level.txt +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/setup.cfg +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/tests/test_accrue.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/tests/test_advanced.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/tests/test_aliases.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/tests/test_core.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/tests/test_delivery.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/tests/test_setup.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/tests/test_store_repo.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/tests/test_toon.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/tests/test_v020.py +0 -0
- {memgit-0.5.0 → memgit-0.6.0}/tests/test_v030.py +0 -0
- {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.
|
|
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
|
[](https://pypi.org/project/memgit/)
|
|
46
46
|
[](LICENSE)
|
|
47
|
-
[](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
|
|
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
|
|
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 #
|
|
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
|
[](https://pypi.org/project/memgit/)
|
|
12
12
|
[](LICENSE)
|
|
13
|
-
[](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
|
|
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
|
|
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 #
|
|
414
|
+
pytest # 245 tests, all passing, < 5 seconds
|
|
402
415
|
```
|
|
403
416
|
|
|
404
417
|
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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('(
|
|
639
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
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` —
|
|
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
|
-
|
|
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*(
|
|
131
|
-
|
|
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
|
-
"-
|
|
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
|
-
|
|
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>
|