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.
- {memgit-0.4.0 → memgit-0.5.0}/PKG-INFO +22 -3
- {memgit-0.4.0 → memgit-0.5.0}/README.md +21 -2
- {memgit-0.4.0 → memgit-0.5.0}/memgit/__init__.py +1 -1
- {memgit-0.4.0 → memgit-0.5.0}/memgit/cli.py +268 -3
- memgit-0.5.0/memgit/delivery.py +341 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit/hooks.py +7 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit/importer.py +1 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit/mcp_server.py +20 -6
- {memgit-0.4.0 → memgit-0.5.0}/memgit/repo.py +19 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit/toon.py +2 -2
- memgit-0.5.0/memgit/usage.py +73 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit.egg-info/PKG-INFO +22 -3
- {memgit-0.4.0 → memgit-0.5.0}/memgit.egg-info/SOURCES.txt +6 -0
- {memgit-0.4.0 → memgit-0.5.0}/pyproject.toml +1 -1
- memgit-0.5.0/tests/test_accrue.py +110 -0
- memgit-0.5.0/tests/test_aliases.py +64 -0
- memgit-0.5.0/tests/test_core.py +75 -0
- memgit-0.5.0/tests/test_delivery.py +121 -0
- {memgit-0.4.0 → memgit-0.5.0}/LICENSE +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit/cloud/__init__.py +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit/cloud/client.py +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit/cloud/commands.py +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit/cloud/crypto.py +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit/cloud/state.py +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit/cloud/sync.py +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit/gitdigest.py +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit/graph.py +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit/http_server.py +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit/models.py +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit/project.py +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit/scorer.py +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit/store.py +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit/tokens.py +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit.egg-info/dependency_links.txt +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit.egg-info/entry_points.txt +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit.egg-info/requires.txt +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/memgit.egg-info/top_level.txt +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/setup.cfg +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/tests/test_advanced.py +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/tests/test_setup.py +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/tests/test_store_repo.py +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/tests/test_toon.py +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/tests/test_v020.py +0 -0
- {memgit-0.4.0 → memgit-0.5.0}/tests/test_v030.py +0 -0
- {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.
|
|
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
|
[](https://pypi.org/project/memgit/)
|
|
46
46
|
[](LICENSE)
|
|
47
|
-
[](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 #
|
|
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
|
[](https://pypi.org/project/memgit/)
|
|
12
12
|
[](LICENSE)
|
|
13
|
-
[](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 #
|
|
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,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
|
-
|
|
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
|
|
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 ─────────────────────────────────────────────────────────────────────
|