memgit 0.4.0__tar.gz → 0.5.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 (45) hide show
  1. {memgit-0.4.0 → memgit-0.5.0}/PKG-INFO +22 -3
  2. {memgit-0.4.0 → memgit-0.5.0}/README.md +21 -2
  3. {memgit-0.4.0 → memgit-0.5.0}/memgit/__init__.py +1 -1
  4. {memgit-0.4.0 → memgit-0.5.0}/memgit/cli.py +268 -3
  5. memgit-0.5.0/memgit/delivery.py +341 -0
  6. {memgit-0.4.0 → memgit-0.5.0}/memgit/hooks.py +7 -0
  7. {memgit-0.4.0 → memgit-0.5.0}/memgit/importer.py +1 -0
  8. {memgit-0.4.0 → memgit-0.5.0}/memgit/mcp_server.py +20 -6
  9. {memgit-0.4.0 → memgit-0.5.0}/memgit/repo.py +19 -0
  10. {memgit-0.4.0 → memgit-0.5.0}/memgit/toon.py +2 -2
  11. memgit-0.5.0/memgit/usage.py +73 -0
  12. {memgit-0.4.0 → memgit-0.5.0}/memgit.egg-info/PKG-INFO +22 -3
  13. {memgit-0.4.0 → memgit-0.5.0}/memgit.egg-info/SOURCES.txt +6 -0
  14. {memgit-0.4.0 → memgit-0.5.0}/pyproject.toml +1 -1
  15. memgit-0.5.0/tests/test_accrue.py +110 -0
  16. memgit-0.5.0/tests/test_aliases.py +64 -0
  17. memgit-0.5.0/tests/test_core.py +75 -0
  18. memgit-0.5.0/tests/test_delivery.py +121 -0
  19. {memgit-0.4.0 → memgit-0.5.0}/LICENSE +0 -0
  20. {memgit-0.4.0 → memgit-0.5.0}/memgit/cloud/__init__.py +0 -0
  21. {memgit-0.4.0 → memgit-0.5.0}/memgit/cloud/client.py +0 -0
  22. {memgit-0.4.0 → memgit-0.5.0}/memgit/cloud/commands.py +0 -0
  23. {memgit-0.4.0 → memgit-0.5.0}/memgit/cloud/crypto.py +0 -0
  24. {memgit-0.4.0 → memgit-0.5.0}/memgit/cloud/state.py +0 -0
  25. {memgit-0.4.0 → memgit-0.5.0}/memgit/cloud/sync.py +0 -0
  26. {memgit-0.4.0 → memgit-0.5.0}/memgit/gitdigest.py +0 -0
  27. {memgit-0.4.0 → memgit-0.5.0}/memgit/graph.py +0 -0
  28. {memgit-0.4.0 → memgit-0.5.0}/memgit/http_server.py +0 -0
  29. {memgit-0.4.0 → memgit-0.5.0}/memgit/models.py +0 -0
  30. {memgit-0.4.0 → memgit-0.5.0}/memgit/project.py +0 -0
  31. {memgit-0.4.0 → memgit-0.5.0}/memgit/scorer.py +0 -0
  32. {memgit-0.4.0 → memgit-0.5.0}/memgit/store.py +0 -0
  33. {memgit-0.4.0 → memgit-0.5.0}/memgit/tokens.py +0 -0
  34. {memgit-0.4.0 → memgit-0.5.0}/memgit.egg-info/dependency_links.txt +0 -0
  35. {memgit-0.4.0 → memgit-0.5.0}/memgit.egg-info/entry_points.txt +0 -0
  36. {memgit-0.4.0 → memgit-0.5.0}/memgit.egg-info/requires.txt +0 -0
  37. {memgit-0.4.0 → memgit-0.5.0}/memgit.egg-info/top_level.txt +0 -0
  38. {memgit-0.4.0 → memgit-0.5.0}/setup.cfg +0 -0
  39. {memgit-0.4.0 → memgit-0.5.0}/tests/test_advanced.py +0 -0
  40. {memgit-0.4.0 → memgit-0.5.0}/tests/test_setup.py +0 -0
  41. {memgit-0.4.0 → memgit-0.5.0}/tests/test_store_repo.py +0 -0
  42. {memgit-0.4.0 → memgit-0.5.0}/tests/test_toon.py +0 -0
  43. {memgit-0.4.0 → memgit-0.5.0}/tests/test_v020.py +0 -0
  44. {memgit-0.4.0 → memgit-0.5.0}/tests/test_v030.py +0 -0
  45. {memgit-0.4.0 → memgit-0.5.0}/tests/test_v040.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memgit
3
- Version: 0.4.0
3
+ Version: 0.5.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-54%20passing-brightgreen)](tests/)
47
+ [![Tests](https://img.shields.io/badge/tests-160%20passing-brightgreen)](tests/)
48
48
 
49
49
  ---
50
50
 
@@ -284,6 +284,16 @@ The tool descriptions teach the AI **judgment** — "does this request depend on
284
284
 
285
285
  ---
286
286
 
287
+ ## Core operating guide (v0.5.0)
288
+
289
+ A project's hardest onboarding problem isn't *what* it does — it's *how to work in it*: which skill to invoke, which command to run, which tool to reach for. That lives in a `CLAUDE.md` or a skills folder the AI host may or may not be configured to read. memgit carries it for you.
290
+
291
+ `memgit core seed` distills a compact operating guide from the project's existing skills + rule files. `memgit core sync` writes it into **every AI host's own rules surface** as a dedicated, memgit-owned file — `.claude/rules/memgit.md`, `.cursor/rules/memgit.mdc`, `.windsurf/rules/memgit.md`, `.clinerules/`, `.roo/rules/`, `.continue/rules/`, `.gemini/`, and a marker-block in Codex's `AGENTS.md`. It's **additive only** — memgit never touches your own config or content — and injected at session start, so any tool knows how to work in the project even when its native setup is missing.
292
+
293
+ And it **learns**: a sidecar usage ledger tracks which memories actually get recalled, and the most-used ones are auto-promoted as pointers into the guide over time (budget-capped, decaying, and always subordinate to the repo's own rules — it never restates or overrides them). Drifted? `memgit core heal` rebuilds it.
294
+
295
+ ---
296
+
287
297
  ## Commands
288
298
 
289
299
  ```bash
@@ -301,6 +311,13 @@ memgit search <query> # BM25 relevance search
301
311
  memgit rollback <ref> # restore state to a checkpoint (HEAD~N or SHA)
302
312
  memgit resume # where we left off — session-start digest
303
313
  memgit merge <thread> # three-way merge a thread into the current one
314
+ memgit remove <slug> # (aliases: delete, rm, del) — mistypes get a "did you mean?"
315
+
316
+ # Core operating guide — per-project, always-on, cross-host
317
+ memgit core seed # draft a guide from this project's skills + rule files
318
+ memgit core sync # deliver it into each AI host's own rules file (additive)
319
+ memgit core show / edit # view / curate the guide
320
+ memgit core heal # self-repair a guide that has drifted
304
321
 
305
322
  # Scale & proof
306
323
  memgit squash # compress old history (archives what it collapses)
@@ -415,7 +432,7 @@ git clone https://github.com/code4161/memgit.git
415
432
  cd memgit
416
433
  python -m venv .venv && source .venv/bin/activate
417
434
  pip install -e ".[dev]"
418
- pytest # 48 tests, all passing, < 1 second
435
+ pytest # 160 tests, all passing, < 3 seconds
419
436
  ```
420
437
 
421
438
  See [CONTRIBUTING.md](CONTRIBUTING.md).
@@ -435,6 +452,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
435
452
  - [x] Flat `memories/` directory — grep/diff/blame your memories
436
453
  - [x] D3.js graph visualization of memory relationships
437
454
  - [x] `memgit resume` + SessionStart hook — sessions start with "where we left off"
455
+ - [x] Guardrail hooks — per-prompt auto-recall + end-of-session capture guard (v0.4.0)
456
+ - [x] Core operating guide — per-project, always-on, cross-host, self-improving (v0.5.0)
438
457
  - [x] `memgit gc` — space reclamation (mark-and-sweep, lossless squash archive)
439
458
  - [x] Multi-agent write safety — store lock, auto-merge commits, `memgit merge`
440
459
  - [x] PyPI + Homebrew (tap) + npm published (v0.1.5)
@@ -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-54%20passing-brightgreen)](tests/)
13
+ [![Tests](https://img.shields.io/badge/tests-160%20passing-brightgreen)](tests/)
14
14
 
15
15
  ---
16
16
 
@@ -250,6 +250,16 @@ The tool descriptions teach the AI **judgment** — "does this request depend on
250
250
 
251
251
  ---
252
252
 
253
+ ## Core operating guide (v0.5.0)
254
+
255
+ A project's hardest onboarding problem isn't *what* it does — it's *how to work in it*: which skill to invoke, which command to run, which tool to reach for. That lives in a `CLAUDE.md` or a skills folder the AI host may or may not be configured to read. memgit carries it for you.
256
+
257
+ `memgit core seed` distills a compact operating guide from the project's existing skills + rule files. `memgit core sync` writes it into **every AI host's own rules surface** as a dedicated, memgit-owned file — `.claude/rules/memgit.md`, `.cursor/rules/memgit.mdc`, `.windsurf/rules/memgit.md`, `.clinerules/`, `.roo/rules/`, `.continue/rules/`, `.gemini/`, and a marker-block in Codex's `AGENTS.md`. It's **additive only** — memgit never touches your own config or content — and injected at session start, so any tool knows how to work in the project even when its native setup is missing.
258
+
259
+ And it **learns**: a sidecar usage ledger tracks which memories actually get recalled, and the most-used ones are auto-promoted as pointers into the guide over time (budget-capped, decaying, and always subordinate to the repo's own rules — it never restates or overrides them). Drifted? `memgit core heal` rebuilds it.
260
+
261
+ ---
262
+
253
263
  ## Commands
254
264
 
255
265
  ```bash
@@ -267,6 +277,13 @@ memgit search <query> # BM25 relevance search
267
277
  memgit rollback <ref> # restore state to a checkpoint (HEAD~N or SHA)
268
278
  memgit resume # where we left off — session-start digest
269
279
  memgit merge <thread> # three-way merge a thread into the current one
280
+ memgit remove <slug> # (aliases: delete, rm, del) — mistypes get a "did you mean?"
281
+
282
+ # Core operating guide — per-project, always-on, cross-host
283
+ memgit core seed # draft a guide from this project's skills + rule files
284
+ memgit core sync # deliver it into each AI host's own rules file (additive)
285
+ memgit core show / edit # view / curate the guide
286
+ memgit core heal # self-repair a guide that has drifted
270
287
 
271
288
  # Scale & proof
272
289
  memgit squash # compress old history (archives what it collapses)
@@ -381,7 +398,7 @@ git clone https://github.com/code4161/memgit.git
381
398
  cd memgit
382
399
  python -m venv .venv && source .venv/bin/activate
383
400
  pip install -e ".[dev]"
384
- pytest # 48 tests, all passing, < 1 second
401
+ pytest # 160 tests, all passing, < 3 seconds
385
402
  ```
386
403
 
387
404
  See [CONTRIBUTING.md](CONTRIBUTING.md).
@@ -401,6 +418,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
401
418
  - [x] Flat `memories/` directory — grep/diff/blame your memories
402
419
  - [x] D3.js graph visualization of memory relationships
403
420
  - [x] `memgit resume` + SessionStart hook — sessions start with "where we left off"
421
+ - [x] Guardrail hooks — per-prompt auto-recall + end-of-session capture guard (v0.4.0)
422
+ - [x] Core operating guide — per-project, always-on, cross-host, self-improving (v0.5.0)
404
423
  - [x] `memgit gc` — space reclamation (mark-and-sweep, lossless squash archive)
405
424
  - [x] Multi-agent write safety — store lock, auto-merge commits, `memgit merge`
406
425
  - [x] PyPI + Homebrew (tap) + npm published (v0.1.5)
@@ -1,3 +1,3 @@
1
1
  """memgit — git for AI memory."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.5.0"
@@ -1,6 +1,7 @@
1
1
  """memgit CLI — git for AI memory."""
2
2
 
3
3
  from __future__ import annotations
4
+ import difflib
4
5
  import sys
5
6
  from datetime import datetime, timezone
6
7
  from pathlib import Path
@@ -35,7 +36,42 @@ def _require_repo() -> Repository:
35
36
  from . import __version__ as _version
36
37
 
37
38
 
38
- @click.group()
39
+ class AliasedGroup(click.Group):
40
+ """Root group with command aliases and a did-you-mean fallback.
41
+
42
+ Two conveniences over a plain click.Group:
43
+ * ALIASES maps common alternative verbs to the real command, so
44
+ `memgit delete <slug>` works as `remove` (an AI reached for `delete`,
45
+ hit "No such command", and only found `remove` via help — smooth that).
46
+ * On an unknown command with no alias, suggest the closest real command
47
+ instead of a bare error, so a typo self-corrects.
48
+ """
49
+
50
+ #: alternative verb -> canonical command name
51
+ ALIASES = {
52
+ 'delete': 'remove',
53
+ 'rm': 'remove',
54
+ 'del': 'remove',
55
+ }
56
+
57
+ def get_command(self, ctx, cmd_name):
58
+ # 1. exact match wins
59
+ rv = super().get_command(ctx, cmd_name)
60
+ if rv is not None:
61
+ return rv
62
+ # 2. known alias -> canonical command
63
+ target = self.ALIASES.get(cmd_name)
64
+ if target is not None:
65
+ return super().get_command(ctx, target)
66
+ # 3. did-you-mean: closest real command (aliases included as options)
67
+ candidates = list(self.list_commands(ctx)) + list(self.ALIASES)
68
+ matches = difflib.get_close_matches(cmd_name, candidates, n=1, cutoff=0.6)
69
+ if matches:
70
+ ctx.fail(f"No such command '{cmd_name}'. Did you mean '{matches[0]}'?")
71
+ return None
72
+
73
+
74
+ @click.group(cls=AliasedGroup)
39
75
  @click.version_option(_version, prog_name='memgit')
40
76
  def cli():
41
77
  """memgit — git for AI memory.
@@ -116,8 +152,9 @@ def init(directory):
116
152
  @click.argument('slug')
117
153
  @click.argument('rule')
118
154
  @click.option('--type', '-t', 'type_code', default='fb',
119
- type=click.Choice(['fb', 'us', 'pj', 'rf', 'cn', 'lx']),
120
- help='fb=feedback us=user pj=project rf=reference cn=convention lx=lesson')
155
+ type=click.Choice(['fb', 'us', 'pj', 'rf', 'cn', 'lx', 'co']),
156
+ help='fb=feedback us=user pj=project rf=reference cn=convention '
157
+ 'lx=lesson co=core (prefer `memgit core set` for co)')
121
158
  @click.option('--why', '-w', default=None, help='Reasoning / why this rule exists')
122
159
  @click.option('--when', '-W', default=None, help='When / where to apply')
123
160
  @click.option('--tags', default=None, help='Comma-separated tags')
@@ -176,6 +213,219 @@ def remove(slug):
176
213
  console.print(f'[dim]not found: {slug}[/dim]')
177
214
 
178
215
 
216
+ # ── core ──────────────────────────────────────────────────────────────────────
217
+
218
+ def _current_project() -> Optional[str]:
219
+ from .project import project_label_from_path
220
+ return project_label_from_path(Path.cwd())
221
+
222
+
223
+ def _core_slug(project: Optional[str]) -> str:
224
+ """Globally-unique slug for a project's core guide (the index is store-wide,
225
+ so each project's core needs its own slug)."""
226
+ return f'core-{project}' if project else 'core'
227
+
228
+
229
+ def _get_core(repo, project: Optional[str]) -> Optional[Mnemonic]:
230
+ m = repo.get(_core_slug(project))
231
+ return m if (m is not None and m.type_code == 'co') else None
232
+
233
+
234
+ @cli.group(invoke_without_command=True)
235
+ @click.pass_context
236
+ def core(ctx):
237
+ """The per-project core operating guide — an always-injected navigation aid
238
+ (which tools/skills to reach for). Subordinate to the repo's own rules."""
239
+ if ctx.invoked_subcommand is None:
240
+ ctx.invoke(core_show)
241
+
242
+
243
+ @core.command('show')
244
+ def core_show():
245
+ """Print the current project's core operating guide."""
246
+ repo = _require_repo()
247
+ project = _current_project()
248
+ m = _get_core(repo, project)
249
+ if m is None:
250
+ console.print(f'[dim]no core guide for project {project or "(global)"} — '
251
+ 'set one with `memgit core set` or `memgit core seed`[/dim]')
252
+ return
253
+ print((m.body or m.rule).rstrip())
254
+
255
+
256
+ @core.command('set')
257
+ @click.option('--body', '-b', default=None,
258
+ help='Guide text; omit or pass "-" to read stdin')
259
+ @click.option('--rule', '-r', default='core operating guide',
260
+ help='One-line label for the core memory')
261
+ def core_set(body, rule):
262
+ """Create or replace the current project's core operating guide."""
263
+ repo = _require_repo()
264
+ project = _current_project()
265
+ if body is None or body == '-':
266
+ body = sys.stdin.read()
267
+ body = (body or '').strip()
268
+ if not body:
269
+ err.print('[red]empty core guide — nothing saved[/red]')
270
+ sys.exit(1)
271
+ m = Mnemonic(
272
+ type_code='co', slug=_core_slug(project),
273
+ timestamp=datetime.now(timezone.utc),
274
+ rule=rule, body=body, project=project, priority=2,
275
+ )
276
+ sha = repo.add(m)
277
+ console.print(f'[green]staged core[/green] {m.slug} [{sha[:8]}] '
278
+ f'({len(body)} chars) — commit or sync to checkpoint')
279
+
280
+
281
+ @core.command('seed')
282
+ @click.option('--force', is_flag=True, help='Overwrite an existing core guide')
283
+ def core_seed(force):
284
+ """Draft a core guide from existing host skills + rules (then edit/curate)."""
285
+ repo = _require_repo()
286
+ project = _current_project()
287
+ if _get_core(repo, project) is not None and not force:
288
+ console.print('[yellow]a core guide already exists[/yellow] — '
289
+ 'edit it (`memgit core edit`) or pass --force to redraft')
290
+ return
291
+ from .delivery import build_seed
292
+ body = build_seed(Path.cwd())
293
+ m = Mnemonic(
294
+ type_code='co', slug=_core_slug(project),
295
+ timestamp=datetime.now(timezone.utc),
296
+ rule='core operating guide', body=body, project=project, priority=2,
297
+ )
298
+ sha = repo.add(m)
299
+ console.print(f'[green]staged core[/green] {m.slug} [{sha[:8]}] '
300
+ f'({len(body)} chars) — review with `memgit core show`, '
301
+ 'refine with `memgit core edit`, then `memgit core sync`')
302
+
303
+
304
+ @core.command('sync')
305
+ @click.option('--host', 'hosts', multiple=True,
306
+ help='Deliver only to these hosts (repeatable); default = detected')
307
+ @click.option('--all', 'all_hosts', is_flag=True, help='Deliver to every supported host')
308
+ @click.option('--dry-run', is_flag=True, help='Show what would be written')
309
+ def core_sync(hosts, all_hosts, dry_run):
310
+ """Write the core guide into each host's memgit-owned rule file (additive)."""
311
+ repo = _require_repo()
312
+ project = _current_project()
313
+ m = _get_core(repo, project)
314
+ if m is None:
315
+ err.print('[red]no core guide for this project[/red] — '
316
+ 'run `memgit core seed` or `memgit core set` first')
317
+ sys.exit(1)
318
+ from .delivery import deliver
319
+ results = deliver(Path.cwd(), m.body or m.rule,
320
+ hosts=list(hosts) or None, all_hosts=all_hosts, dry_run=dry_run)
321
+ if not results:
322
+ console.print('[dim]no target hosts detected — pass --all or --host <name>[/dim]')
323
+ return
324
+ verb = 'would write' if dry_run else 'wrote'
325
+ for r in results:
326
+ color = {'created': 'green', 'updated': 'green', 'unchanged': 'dim',
327
+ 'over-cap': 'red'}.get(r.action, 'white')
328
+ note = ' (exceeds host size cap — trim the guide)' if r.action == 'over-cap' else ''
329
+ console.print(f' [{color}]{r.action:9}[/{color}] {r.label:14} '
330
+ f'{r.path} ({r.bytes}b){note}')
331
+ if not dry_run:
332
+ console.print(f'[green]{verb}[/green] core guide to '
333
+ f'{sum(1 for r in results if r.action in ("created", "updated"))} host file(s)')
334
+
335
+
336
+ def _refresh_core(repo, project, redeliver=True) -> bool:
337
+ """Recompute the auto (usage-driven) section of the project's core guide.
338
+ If it changed, checkpoint the new body and refresh already-delivered host
339
+ files. The curated region is never touched. Returns True if it changed."""
340
+ from .delivery import refresh_core_body, deliver
341
+ m = _get_core(repo, project)
342
+ if m is None:
343
+ return False
344
+ now = datetime.now(timezone.utc)
345
+ new_body = refresh_core_body(m.body or '', repo, project, now)
346
+ if new_body is None:
347
+ return False
348
+ repo.add(Mnemonic(type_code='co', slug=m.slug, timestamp=now,
349
+ rule=m.rule, body=new_body, project=project, priority=2))
350
+ repo.commit(message=f'core: auto-refresh {m.slug}', trigger='auto')
351
+ if redeliver:
352
+ try:
353
+ deliver(Path.cwd(), new_body, only_existing=True)
354
+ except OSError:
355
+ pass
356
+ return True
357
+
358
+
359
+ def _maybe_auto_core(repo) -> None:
360
+ """Best-effort auto-grow, called from `sync` (the Stop hook). Silent and
361
+ never raises — the AI is the operator, so this must just work in the
362
+ background without a human running anything."""
363
+ try:
364
+ _refresh_core(repo, _current_project())
365
+ except Exception:
366
+ pass
367
+
368
+
369
+ @core.command('refresh')
370
+ def core_refresh():
371
+ """Recompute the usage-driven section of the core guide now."""
372
+ repo = _require_repo()
373
+ changed = _refresh_core(repo, _current_project())
374
+ console.print('[green]core refreshed[/green]' if changed
375
+ else '[dim]core already current[/dim]')
376
+
377
+
378
+ @core.command('heal')
379
+ @click.option('--reset-usage', is_flag=True,
380
+ help='Also wipe the usage ledger (use if the signal looks corrupted)')
381
+ def core_heal(reset_usage):
382
+ """Self-heal a core guide that feels off — rebuild the curated base from a
383
+ fresh skill/rules seed, recompute the usage section, and re-deliver to hosts.
384
+ The AI runs this when the guide looks stale, wrong, or bloated."""
385
+ repo = _require_repo()
386
+ project = _current_project()
387
+ from .delivery import build_seed, refresh_core_body, deliver
388
+ from .usage import reset_usage as _reset
389
+ if reset_usage:
390
+ _reset(repo)
391
+ now = datetime.now(timezone.utc)
392
+ curated = build_seed(Path.cwd())
393
+ body = refresh_core_body(curated, repo, project, now) or (curated.rstrip() + '\n')
394
+ repo.add(Mnemonic(type_code='co', slug=_core_slug(project), timestamp=now,
395
+ rule='core operating guide', body=body,
396
+ project=project, priority=2))
397
+ repo.commit(message=f'core: heal {_core_slug(project)}', trigger='explicit')
398
+ results = deliver(Path.cwd(), body, only_existing=True)
399
+ rewritten = sum(1 for r in results if r.action in ('created', 'updated'))
400
+ console.print(f'[green]healed[/green] core guide (rebuilt {len(body)} chars, '
401
+ f'refreshed {rewritten} host file(s))')
402
+
403
+
404
+ @core.command('edit')
405
+ def core_edit():
406
+ """Open the current project's core guide in $EDITOR."""
407
+ repo = _require_repo()
408
+ project = _current_project()
409
+ existing = _get_core(repo, project)
410
+ current = (existing.body if existing else '') or ''
411
+ edited = click.edit(current)
412
+ if edited is None:
413
+ console.print('[dim]no changes[/dim]')
414
+ return
415
+ edited = edited.strip()
416
+ if not edited:
417
+ console.print('[dim]empty — not saved[/dim]')
418
+ return
419
+ m = Mnemonic(
420
+ type_code='co', slug=_core_slug(project),
421
+ timestamp=datetime.now(timezone.utc),
422
+ rule=(existing.rule if existing else 'core operating guide'),
423
+ body=edited, project=project, priority=2,
424
+ )
425
+ sha = repo.add(m)
426
+ console.print(f'[green]staged core[/green] {m.slug} [{sha[:8]}]')
427
+
428
+
179
429
  # ── commit ────────────────────────────────────────────────────────────────────
180
430
 
181
431
  @cli.command()
@@ -327,6 +577,18 @@ def _format_resume_plain(ctx: dict) -> str:
327
577
  f'# memgit resume — thread {ctx["thread"]} @ {ctx["head"] or "none"} '
328
578
  f'({ctx["checkpoint_count"]} checkpoints, {ctx["total_memories"]} memories{proj})',
329
579
  ]
580
+ # Core operating guide first: a per-project navigation aid rendered in FULL
581
+ # (body, not the clipped rule) so the host knows which tools/skills to use
582
+ # up front. Explicitly subordinate — the repo's own rules win on conflict.
583
+ if ctx.get('core_memories'):
584
+ lines.append('')
585
+ lines.append('## Core operating guide — always apply')
586
+ lines.append('(memgit-managed navigation aid. If this conflicts with the '
587
+ "repo's own CLAUDE.md / AGENTS.md / rules, THOSE win — not policy.)")
588
+ for m in ctx['core_memories']:
589
+ body = (m.get('body') or m['rule']).rstrip()
590
+ lines.append('')
591
+ lines.append(body)
330
592
  st = ctx['staged']
331
593
  if st['new'] or st['updated'] or st['removed']:
332
594
  lines.append('')
@@ -1054,7 +1316,9 @@ def sync(message, dry_run):
1054
1316
  sha = repo.commit(message=msg, trigger='session_end') if msg else None
1055
1317
  if sha:
1056
1318
  console.print(f'[green]sync[/green] {sha[:8]} {msg}')
1319
+ _maybe_auto_core(repo)
1057
1320
  return
1321
+ _maybe_auto_core(repo)
1058
1322
  console.print('[dim]No Claude Code memories found.[/dim]')
1059
1323
  return
1060
1324
 
@@ -1079,6 +1343,7 @@ def sync(message, dry_run):
1079
1343
  (f' [dim]({skipped} skipped)[/dim]' if skipped else ''))
1080
1344
  else:
1081
1345
  console.print(f'[dim]sync: no changes ({count} memories already current)[/dim]')
1346
+ _maybe_auto_core(repo)
1082
1347
 
1083
1348
 
1084
1349
  # ── graph ─────────────────────────────────────────────────────────────────────