memgit 0.13.0__tar.gz → 0.14.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.13.0 → memgit-0.14.0}/PKG-INFO +36 -7
- {memgit-0.13.0 → memgit-0.14.0}/README.md +33 -4
- {memgit-0.13.0 → memgit-0.14.0}/memgit/__init__.py +1 -1
- {memgit-0.13.0 → memgit-0.14.0}/memgit/cli.py +222 -38
- memgit-0.14.0/memgit/cloud/__init__.py +7 -0
- memgit-0.14.0/memgit/cloud/auto.py +278 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/cloud/commands.py +131 -4
- {memgit-0.13.0 → memgit-0.14.0}/memgit/cloud/sync.py +17 -0
- memgit-0.14.0/memgit/drift.py +191 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/env.py +13 -0
- memgit-0.14.0/memgit/gate.py +431 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/hooks.py +161 -7
- {memgit-0.13.0 → memgit-0.14.0}/memgit/links.py +76 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/markup.py +2 -1
- {memgit-0.13.0 → memgit-0.14.0}/memgit/mcp_server.py +108 -2
- {memgit-0.13.0 → memgit-0.14.0}/memgit/metrics.py +22 -1
- {memgit-0.13.0 → memgit-0.14.0}/memgit/models.py +5 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/repo.py +57 -10
- {memgit-0.13.0 → memgit-0.14.0}/memgit/scorer.py +19 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/toon.py +29 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit.egg-info/PKG-INFO +36 -7
- {memgit-0.13.0 → memgit-0.14.0}/memgit.egg-info/SOURCES.txt +7 -0
- {memgit-0.13.0 → memgit-0.14.0}/pyproject.toml +9 -2
- memgit-0.14.0/tests/test_action_gate.py +424 -0
- memgit-0.14.0/tests/test_cloud_autosync.py +248 -0
- memgit-0.14.0/tests/test_cloud_two_machines.py +136 -0
- memgit-0.14.0/tests/test_mcp_store_bootstrap.py +69 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_v030.py +3 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_version_stamps.py +16 -4
- memgit-0.13.0/memgit/cloud/__init__.py +0 -6
- {memgit-0.13.0 → memgit-0.14.0}/LICENSE +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/audit.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/backup.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/cloud/client.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/cloud/crypto.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/cloud/state.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/delivery.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/evaluate.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/gitdigest.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/graph.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/http_server.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/importer.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/license.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/org.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/project.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/sanitize.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/savings.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/store.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/tiers.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/tokens.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit/usage.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit.egg-info/dependency_links.txt +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit.egg-info/entry_points.txt +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/memgit.egg-info/requires.txt +2 -2
- {memgit-0.13.0 → memgit-0.14.0}/memgit.egg-info/top_level.txt +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/setup.cfg +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_accrue.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_advanced.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_aliases.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_audit.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_backup.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_cloud_guard.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_core.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_delivery.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_dependency_floor.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_env_inventory.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_git_args.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_license.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_lock_exclusion.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_markup.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_org.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_savings.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_server_lifecycle.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_setup.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_store_repo.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_tiers.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_toon.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_v020.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_v040.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_v060.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_v070.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_v080.py +0 -0
- {memgit-0.13.0 → memgit-0.14.0}/tests/test_v081.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memgit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.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
|
|
@@ -22,14 +22,14 @@ License-File: LICENSE
|
|
|
22
22
|
Requires-Dist: click>=8.0
|
|
23
23
|
Requires-Dist: rich>=13.0
|
|
24
24
|
Requires-Dist: mcp<2,>=1.28.1
|
|
25
|
+
Requires-Dist: pynacl>=1.5
|
|
26
|
+
Requires-Dist: httpx>=0.27
|
|
25
27
|
Provides-Extra: dev
|
|
26
28
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
27
29
|
Requires-Dist: pytest-anyio>=0.0.0; extra == "dev"
|
|
28
30
|
Provides-Extra: tokens
|
|
29
31
|
Requires-Dist: tiktoken>=0.7; extra == "tokens"
|
|
30
32
|
Provides-Extra: cloud
|
|
31
|
-
Requires-Dist: pynacl>=1.5; extra == "cloud"
|
|
32
|
-
Requires-Dist: httpx>=0.27; extra == "cloud"
|
|
33
33
|
Dynamic: license-file
|
|
34
34
|
|
|
35
35
|
<p align="center">
|
|
@@ -160,7 +160,7 @@ pip install memgit
|
|
|
160
160
|
```
|
|
161
161
|
(The Chocolatey package is live on community.chocolatey.org; newly pushed versions can take a few days to clear moderation — `pip install memgit` always has the latest.)
|
|
162
162
|
|
|
163
|
-
**Any AI tool config (
|
|
163
|
+
**Any AI tool config (needs Python 3.11+ on PATH; npx installs memgit into its own environment on first run and creates the store):**
|
|
164
164
|
```json
|
|
165
165
|
{ "mcpServers": { "memgit": { "command": "npx", "args": ["-y", "memgit-mcp"] } } }
|
|
166
166
|
```
|
|
@@ -216,15 +216,16 @@ memgit resume --json # for tooling
|
|
|
216
216
|
Wire it into Claude Code so memory becomes **automatic** — no tool call, no judgment required:
|
|
217
217
|
|
|
218
218
|
```bash
|
|
219
|
-
memgit setup hooks # installs all
|
|
219
|
+
memgit setup hooks # installs all six hooks (~/.claude/settings.json)
|
|
220
220
|
```
|
|
221
221
|
|
|
222
222
|
| Hook | What it enforces |
|
|
223
223
|
|---|---|
|
|
224
224
|
| `SessionStart` | every session opens with the resume digest in context — status board, checkpoints, critical rules, memory index |
|
|
225
225
|
| `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 |
|
|
226
|
+
| `PreToolUse` | a Bash or file-write call that matches a memory's triggers is refused with the rule as the reason (priority-3 feedback and conventions) or gets the rule as context (the rest). memgit allow SLUG opens a short override window. Trigger cache only, `--no-gate` to skip |
|
|
226
227
|
| `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 |
|
|
227
|
-
| `Stop` (guard) | a session that did real work but saved nothing gets ONE
|
|
228
|
+
| `Stop` (guard) | a governed call that went through without the gate, or a session that did real work but saved nothing, gets ONE block before finishing. `--no-guard` to skip |
|
|
228
229
|
| `Stop` (sync) | markdown memories are checkpointed asynchronously at session end |
|
|
229
230
|
|
|
230
231
|
Why hooks and not just good tool descriptions? We measured it: across 166 real sessions, hook-injected context was delivered in **100%** of them while voluntary memory-tool calls happened in **6%**. What a hook enforces happens.
|
|
@@ -325,6 +326,29 @@ memgit pro deactivate # remove the key from this machine
|
|
|
325
326
|
exports keep working.
|
|
326
327
|
- If this store is logged in to memgit cloud, `pro activate` also upgrades that account.
|
|
327
328
|
|
|
329
|
+
### Sync between machines and with a team
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
memgit cloud signup --email you@example.com # 14-day trial; keys are made on this machine
|
|
333
|
+
memgit cloud team create myteam
|
|
334
|
+
memgit cloud link myteam/memory # creates the cloud repo on first link
|
|
335
|
+
memgit cloud auto on # every save syncs in the background from now on
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
With autosync on, each save (including every `save_memory` your AI makes) starts one
|
|
339
|
+
background pull-then-push and returns immediately, and each session start pulls what other
|
|
340
|
+
machines saved. `memgit cloud auto status` shows the last run and why it would not run.
|
|
341
|
+
|
|
342
|
+
On a machine that runs an agent but no interactive sessions (a Mac mini, a VM), log in and
|
|
343
|
+
link once, then keep it current under systemd or launchd:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
memgit cloud watch --interval 60 # pull and push forever, one JSON line per run
|
|
347
|
+
memgit cloud watch --export ~/agent/.hermes.md # also write the digest for agents that read context files
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Everything leaves the machine end-to-end encrypted; the server stores ciphertext only.
|
|
351
|
+
|
|
328
352
|
---
|
|
329
353
|
|
|
330
354
|
## Backups that actually happen
|
|
@@ -445,7 +469,9 @@ memgit setup cline
|
|
|
445
469
|
memgit setup continue
|
|
446
470
|
memgit setup gemini-cli
|
|
447
471
|
memgit setup hooks # Claude Code hooks: resume at start, per-prompt recall,
|
|
448
|
-
#
|
|
472
|
+
# action gate on Bash and file writes, compliance and
|
|
473
|
+
# capture guard + auto-sync at stop (--no-recall / --no-guard / --no-gate)
|
|
474
|
+
memgit doctor --rules # critical rules next to same-topic lines in CLAUDE.md, AGENTS.md, ...
|
|
449
475
|
|
|
450
476
|
# Pro (licence key)
|
|
451
477
|
memgit pro activate <key> # Polar-issued key; unlocks hosted E2E sync
|
|
@@ -510,6 +536,9 @@ memgit talks to nothing.
|
|
|
510
536
|
| Cloud sync | `MEMGIT_CLOUD_API` | Overrides the memgit cloud API base URL. |
|
|
511
537
|
| | `MEMGIT_CLOUD_APP` | Overrides the memgit cloud web app base URL used in printed links. |
|
|
512
538
|
| | `MEMGIT_CLOUD_NO_CACHE` | Set to 1/true/yes to keep decrypted keys out of credentials.json; the passphrase is then re-prompted per command. |
|
|
539
|
+
| | `MEMGIT_NO_AUTOSYNC` | Set to 1/true/yes to stop saves and session starts from syncing to memgit cloud in the background, for this process only. |
|
|
540
|
+
| | `MEMGIT_AUTOSYNC_MAX_AGE` | Seconds after which a read (search, recall, an MCP lookup) starts a background fetch from memgit cloud (default 300). 0 turns read-triggered fetching off; saves and session starts still sync. |
|
|
541
|
+
| | `MEMGIT_AUTOSYNC_WAIT` | Seconds a session start waits for the fetch from memgit cloud before rendering the digest (default 3). 0 never waits. |
|
|
513
542
|
| Tuning | `MEMGIT_LOCK_TIMEOUT` | Seconds a writer waits for the store lock before giving up (default 10). |
|
|
514
543
|
| | `MEMGIT_IDLE_EVICT_SECONDS` | Idle seconds before the MCP server drops its in-memory caches while staying connected (default 900). |
|
|
515
544
|
| | `MEMGIT_HOUSEKEEPING_INTERVAL` | Seconds between the MCP server's housekeeping passes. |
|
|
@@ -126,7 +126,7 @@ pip install memgit
|
|
|
126
126
|
```
|
|
127
127
|
(The Chocolatey package is live on community.chocolatey.org; newly pushed versions can take a few days to clear moderation — `pip install memgit` always has the latest.)
|
|
128
128
|
|
|
129
|
-
**Any AI tool config (
|
|
129
|
+
**Any AI tool config (needs Python 3.11+ on PATH; npx installs memgit into its own environment on first run and creates the store):**
|
|
130
130
|
```json
|
|
131
131
|
{ "mcpServers": { "memgit": { "command": "npx", "args": ["-y", "memgit-mcp"] } } }
|
|
132
132
|
```
|
|
@@ -182,15 +182,16 @@ memgit resume --json # for tooling
|
|
|
182
182
|
Wire it into Claude Code so memory becomes **automatic** — no tool call, no judgment required:
|
|
183
183
|
|
|
184
184
|
```bash
|
|
185
|
-
memgit setup hooks # installs all
|
|
185
|
+
memgit setup hooks # installs all six hooks (~/.claude/settings.json)
|
|
186
186
|
```
|
|
187
187
|
|
|
188
188
|
| Hook | What it enforces |
|
|
189
189
|
|---|---|
|
|
190
190
|
| `SessionStart` | every session opens with the resume digest in context — status board, checkpoints, critical rules, memory index |
|
|
191
191
|
| `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 |
|
|
192
|
+
| `PreToolUse` | a Bash or file-write call that matches a memory's triggers is refused with the rule as the reason (priority-3 feedback and conventions) or gets the rule as context (the rest). memgit allow SLUG opens a short override window. Trigger cache only, `--no-gate` to skip |
|
|
192
193
|
| `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 |
|
|
193
|
-
| `Stop` (guard) | a session that did real work but saved nothing gets ONE
|
|
194
|
+
| `Stop` (guard) | a governed call that went through without the gate, or a session that did real work but saved nothing, gets ONE block before finishing. `--no-guard` to skip |
|
|
194
195
|
| `Stop` (sync) | markdown memories are checkpointed asynchronously at session end |
|
|
195
196
|
|
|
196
197
|
Why hooks and not just good tool descriptions? We measured it: across 166 real sessions, hook-injected context was delivered in **100%** of them while voluntary memory-tool calls happened in **6%**. What a hook enforces happens.
|
|
@@ -291,6 +292,29 @@ memgit pro deactivate # remove the key from this machine
|
|
|
291
292
|
exports keep working.
|
|
292
293
|
- If this store is logged in to memgit cloud, `pro activate` also upgrades that account.
|
|
293
294
|
|
|
295
|
+
### Sync between machines and with a team
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
memgit cloud signup --email you@example.com # 14-day trial; keys are made on this machine
|
|
299
|
+
memgit cloud team create myteam
|
|
300
|
+
memgit cloud link myteam/memory # creates the cloud repo on first link
|
|
301
|
+
memgit cloud auto on # every save syncs in the background from now on
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
With autosync on, each save (including every `save_memory` your AI makes) starts one
|
|
305
|
+
background pull-then-push and returns immediately, and each session start pulls what other
|
|
306
|
+
machines saved. `memgit cloud auto status` shows the last run and why it would not run.
|
|
307
|
+
|
|
308
|
+
On a machine that runs an agent but no interactive sessions (a Mac mini, a VM), log in and
|
|
309
|
+
link once, then keep it current under systemd or launchd:
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
memgit cloud watch --interval 60 # pull and push forever, one JSON line per run
|
|
313
|
+
memgit cloud watch --export ~/agent/.hermes.md # also write the digest for agents that read context files
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Everything leaves the machine end-to-end encrypted; the server stores ciphertext only.
|
|
317
|
+
|
|
294
318
|
---
|
|
295
319
|
|
|
296
320
|
## Backups that actually happen
|
|
@@ -411,7 +435,9 @@ memgit setup cline
|
|
|
411
435
|
memgit setup continue
|
|
412
436
|
memgit setup gemini-cli
|
|
413
437
|
memgit setup hooks # Claude Code hooks: resume at start, per-prompt recall,
|
|
414
|
-
#
|
|
438
|
+
# action gate on Bash and file writes, compliance and
|
|
439
|
+
# capture guard + auto-sync at stop (--no-recall / --no-guard / --no-gate)
|
|
440
|
+
memgit doctor --rules # critical rules next to same-topic lines in CLAUDE.md, AGENTS.md, ...
|
|
415
441
|
|
|
416
442
|
# Pro (licence key)
|
|
417
443
|
memgit pro activate <key> # Polar-issued key; unlocks hosted E2E sync
|
|
@@ -476,6 +502,9 @@ memgit talks to nothing.
|
|
|
476
502
|
| Cloud sync | `MEMGIT_CLOUD_API` | Overrides the memgit cloud API base URL. |
|
|
477
503
|
| | `MEMGIT_CLOUD_APP` | Overrides the memgit cloud web app base URL used in printed links. |
|
|
478
504
|
| | `MEMGIT_CLOUD_NO_CACHE` | Set to 1/true/yes to keep decrypted keys out of credentials.json; the passphrase is then re-prompted per command. |
|
|
505
|
+
| | `MEMGIT_NO_AUTOSYNC` | Set to 1/true/yes to stop saves and session starts from syncing to memgit cloud in the background, for this process only. |
|
|
506
|
+
| | `MEMGIT_AUTOSYNC_MAX_AGE` | Seconds after which a read (search, recall, an MCP lookup) starts a background fetch from memgit cloud (default 300). 0 turns read-triggered fetching off; saves and session starts still sync. |
|
|
507
|
+
| | `MEMGIT_AUTOSYNC_WAIT` | Seconds a session start waits for the fetch from memgit cloud before rendering the digest (default 3). 0 never waits. |
|
|
479
508
|
| Tuning | `MEMGIT_LOCK_TIMEOUT` | Seconds a writer waits for the store lock before giving up (default 10). |
|
|
480
509
|
| | `MEMGIT_IDLE_EVICT_SECONDS` | Idle seconds before the MCP server drops its in-memory caches while staying connected (default 900). |
|
|
481
510
|
| | `MEMGIT_HOUSEKEEPING_INTERVAL` | Seconds between the MCP server's housekeeping passes. |
|
|
@@ -657,6 +657,15 @@ def resume(checkpoints, recent, plain, fmt_json, project):
|
|
|
657
657
|
import json as _j
|
|
658
658
|
from .project import detect_project
|
|
659
659
|
repo = _require_repo()
|
|
660
|
+
# Session start is when another machine's memories should arrive. The pull
|
|
661
|
+
# runs detached and the digest waits a bounded moment for it (3 s unless
|
|
662
|
+
# MEMGIT_AUTOSYNC_WAIT says otherwise), so a quick network puts them in
|
|
663
|
+
# this digest and a slow one never holds it up.
|
|
664
|
+
try:
|
|
665
|
+
from .cloud.auto import fetch_for_session
|
|
666
|
+
fetch_for_session(repo.path)
|
|
667
|
+
except Exception:
|
|
668
|
+
pass
|
|
660
669
|
if project is None:
|
|
661
670
|
project = detect_project()
|
|
662
671
|
ctx = repo.resume_context(checkpoints=checkpoints, recent=recent, project=project)
|
|
@@ -743,9 +752,15 @@ def resume(checkpoints, recent, plain, fmt_json, project):
|
|
|
743
752
|
console.print()
|
|
744
753
|
|
|
745
754
|
|
|
746
|
-
#:
|
|
747
|
-
#:
|
|
748
|
-
|
|
755
|
+
#: Size at which Claude Code stops inlining hook output: above it the digest
|
|
756
|
+
#: is saved to a file and the model sees a ~2 KB preview, so everything past
|
|
757
|
+
#: the preview (status board, critical rules, index) never reaches context.
|
|
758
|
+
#: Measured 2026-10-23: an 11,297-character digest was persisted, not inlined.
|
|
759
|
+
HOST_INLINE_LIMIT_CHARS = 10_000
|
|
760
|
+
#: Hard budget for the injected resume digest, with headroom under the host
|
|
761
|
+
#: limit. Measured in UTF-16 code units, the way a JavaScript host counts
|
|
762
|
+
#: string length (an emoji is two). Paid at the start of EVERY session.
|
|
763
|
+
RESUME_BUDGET_CHARS = 9_000
|
|
749
764
|
|
|
750
765
|
#: A status-board tracker not re-saved in this many days is flagged as possibly
|
|
751
766
|
#: stale — trackers are only as fresh as their last write, so an old one may
|
|
@@ -759,33 +774,82 @@ def _tracker_age_days(ts) -> int:
|
|
|
759
774
|
return max(0, (now - ts).days)
|
|
760
775
|
|
|
761
776
|
|
|
762
|
-
def
|
|
777
|
+
def _host_len(text: str) -> int:
|
|
778
|
+
"""Length as a JavaScript host counts it (UTF-16 code units)."""
|
|
779
|
+
return len(text.encode('utf-16-le')) // 2
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def _strip_auto_block(body: str) -> str:
|
|
783
|
+
"""A core-guide body without its usage-ranked pointer block."""
|
|
784
|
+
from .delivery import AUTO_START, AUTO_END
|
|
785
|
+
if AUTO_START not in body:
|
|
786
|
+
return body
|
|
787
|
+
head, _, rest = body.partition(AUTO_START)
|
|
788
|
+
_, _, tail = rest.partition(AUTO_END)
|
|
789
|
+
return (head.rstrip() + '\n(auto pointers cut to fit the session-start '
|
|
790
|
+
'budget — memgit core show has them)\n' + tail.lstrip()).rstrip()
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def _format_resume_plain(ctx: dict, budget: int = None) -> str:
|
|
763
794
|
"""Plain-text resume digest — injected into AI context by the SessionStart
|
|
764
|
-
hook,
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
status board
|
|
771
|
-
|
|
795
|
+
hook, never longer than RESUME_BUDGET_CHARS.
|
|
796
|
+
|
|
797
|
+
The budget is a guarantee, not a target: a digest over the host's inline
|
|
798
|
+
limit is saved to a file and almost none of it is read, which is worse
|
|
799
|
+
than any trim. Sections trim in a fixed order, re-rendering after each
|
|
800
|
+
cut: recent memories, checkpoints, index and rule text first; then the
|
|
801
|
+
status board and critical list shrink; then the core guide loses its
|
|
802
|
+
auto-pointer block; and only as a last resort is the core guide body cut,
|
|
803
|
+
with a pointer to the full text.
|
|
772
804
|
"""
|
|
805
|
+
budget = budget or RESUME_BUDGET_CHARS
|
|
773
806
|
text = _render_resume_plain(ctx)
|
|
774
|
-
if
|
|
807
|
+
if _host_len(text) <= budget:
|
|
775
808
|
return text
|
|
776
809
|
ctx = dict(ctx) # never mutate the caller's context
|
|
810
|
+
|
|
811
|
+
def _core_no_auto(c):
|
|
812
|
+
c['core_memories'] = [dict(m, body=_strip_auto_block(m.get('body') or ''))
|
|
813
|
+
for m in c.get('core_memories') or []]
|
|
814
|
+
|
|
777
815
|
trims = (
|
|
778
816
|
lambda c: c.update(recent_memories=c.get('recent_memories', [])[:5]),
|
|
779
817
|
lambda c: c.update(checkpoints=c.get('checkpoints', [])[:3]),
|
|
780
818
|
lambda c: c.update(_crit_clip=160),
|
|
781
819
|
lambda c: c.update(entity_index=c.get('entity_index', [])[:5]),
|
|
782
820
|
lambda c: c.update(recent_memories=c.get('recent_memories', [])[:3]),
|
|
821
|
+
lambda c: c.update(_tracker_clip=100, _crit_clip=130),
|
|
822
|
+
lambda c: c.update(_crit_max=12),
|
|
823
|
+
_core_no_auto,
|
|
824
|
+
lambda c: c.update(tracker_memories=c.get('tracker_memories', [])[:5],
|
|
825
|
+
recent_memories=[], checkpoints=c.get('checkpoints', [])[:1]),
|
|
826
|
+
lambda c: c.update(_crit_max=8, _crit_clip=110, entity_index=[]),
|
|
783
827
|
)
|
|
784
828
|
for trim in trims:
|
|
785
829
|
trim(ctx)
|
|
786
830
|
text = _render_resume_plain(ctx)
|
|
787
|
-
if
|
|
788
|
-
|
|
831
|
+
if _host_len(text) <= budget:
|
|
832
|
+
return text
|
|
833
|
+
|
|
834
|
+
# Last resort: cut the core guide body to what is left, with a pointer.
|
|
835
|
+
over = _host_len(text) - budget
|
|
836
|
+
cores = ctx.get('core_memories') or []
|
|
837
|
+
if cores and not ctx.get('core_delivered'):
|
|
838
|
+
note = '\n…(core guide cut to fit; full text: memgit core show)'
|
|
839
|
+
total = sum(len(m.get('body') or '') for m in cores)
|
|
840
|
+
keep = max(0, total - over - len(note) * len(cores) - 50)
|
|
841
|
+
cut = []
|
|
842
|
+
for m in cores:
|
|
843
|
+
body = m.get('body') or ''
|
|
844
|
+
share = int(keep * len(body) / total) if total else 0
|
|
845
|
+
cut.append(dict(m, body=(body[:share].rstrip() + note) if share < len(body) else body))
|
|
846
|
+
ctx['core_memories'] = cut
|
|
847
|
+
text = _render_resume_plain(ctx)
|
|
848
|
+
if _host_len(text) > budget:
|
|
849
|
+
tail = '\n…(digest cut at the session-start budget)'
|
|
850
|
+
while _host_len(text) + _host_len(tail) > budget and text:
|
|
851
|
+
text = text[:max(0, len(text) - (_host_len(text) + _host_len(tail) - budget) - 1)]
|
|
852
|
+
text += tail
|
|
789
853
|
return text
|
|
790
854
|
|
|
791
855
|
|
|
@@ -800,6 +864,8 @@ def _render_resume_plain(ctx: dict) -> str:
|
|
|
800
864
|
return text if len(text) <= n else text[:n - 1] + '…'
|
|
801
865
|
|
|
802
866
|
crit_clip = int(ctx.get('_crit_clip', 200))
|
|
867
|
+
crit_max = int(ctx.get('_crit_max', 20))
|
|
868
|
+
tracker_clip = int(ctx.get('_tracker_clip', 160))
|
|
803
869
|
|
|
804
870
|
proj = f' · project {ctx["project"]}' if ctx.get('project') else ''
|
|
805
871
|
lines = [
|
|
@@ -827,7 +893,20 @@ def _render_resume_plain(ctx: dict) -> str:
|
|
|
827
893
|
body = (m.get('body') or m['rule']).rstrip()
|
|
828
894
|
lines.append('')
|
|
829
895
|
lines.append(body)
|
|
830
|
-
#
|
|
896
|
+
# Critical rules right after the guide, before the status board: a host
|
|
897
|
+
# that previews only the head of the digest must still see the rules,
|
|
898
|
+
# ranked (see Repository.resume_context) so the cut keeps the ones most
|
|
899
|
+
# likely to be broken.
|
|
900
|
+
if ctx.get('critical_memories'):
|
|
901
|
+
lines.append('')
|
|
902
|
+
lines.append('## Critical rules — always apply')
|
|
903
|
+
crit = ctx['critical_memories']
|
|
904
|
+
for m in crit[:crit_max]:
|
|
905
|
+
lines.append(f'- {m["slug"]}: {clip(m["rule"], crit_clip)}')
|
|
906
|
+
if len(crit) > crit_max:
|
|
907
|
+
lines.append(f'- …and {len(crit) - crit_max} more — run list_memories '
|
|
908
|
+
'with min_priority=3')
|
|
909
|
+
# Status board: live entity state. Rendered right after the rules because
|
|
831
910
|
# orientation ("what is the CURRENT state of things") is worth more than
|
|
832
911
|
# history. The freshness stamp is what tells the model to trust or
|
|
833
912
|
# re-verify a line.
|
|
@@ -840,7 +919,7 @@ def _render_resume_plain(ctx: dict) -> str:
|
|
|
840
919
|
stamp = m['timestamp'].strftime('%m-%d')
|
|
841
920
|
age = _tracker_age_days(m['timestamp'])
|
|
842
921
|
stale = f' ⚠ {age}d old' if age >= STATUS_STALE_DAYS else ''
|
|
843
|
-
lines.append(f'- {m["slug"]} (upd {stamp}{stale}): {clip(m["rule"],
|
|
922
|
+
lines.append(f'- {m["slug"]} (upd {stamp}{stale}): {clip(m["rule"], tracker_clip)}')
|
|
844
923
|
lines.append('(State changed? Update the tracker: save_memory with the '
|
|
845
924
|
'same slug. ⚠ = not updated recently, may lag the files.)')
|
|
846
925
|
st = ctx['staged']
|
|
@@ -869,14 +948,6 @@ def _render_resume_plain(ctx: dict) -> str:
|
|
|
869
948
|
if m.get('project') and m['project'] != ctx.get('project') else '')
|
|
870
949
|
unv = ' ⚠unverified' if m.get('unverified') else ''
|
|
871
950
|
lines.append(f'- {m["slug"]} ({m["type"]}, {ts}){other}{unv}: {clip(m["rule"], 160)}')
|
|
872
|
-
if ctx['critical_memories']:
|
|
873
|
-
lines.append('')
|
|
874
|
-
lines.append('## Critical rules — always apply')
|
|
875
|
-
crit = ctx['critical_memories']
|
|
876
|
-
for m in crit[:20]:
|
|
877
|
-
lines.append(f'- {m["slug"]}: {clip(m["rule"], crit_clip)}')
|
|
878
|
-
if len(crit) > 20:
|
|
879
|
-
lines.append(f'- …and {len(crit) - 20} more — run list_memories with min_priority=3')
|
|
880
951
|
if ctx.get('maintenance'):
|
|
881
952
|
lines.append('')
|
|
882
953
|
lines.append(f'## Maintenance needed\n- {ctx["maintenance"]}')
|
|
@@ -940,6 +1011,34 @@ def hook_stop_guard():
|
|
|
940
1011
|
sys.exit(stop_guard())
|
|
941
1012
|
|
|
942
1013
|
|
|
1014
|
+
@hook.command('action-gate')
|
|
1015
|
+
def hook_action_gate():
|
|
1016
|
+
"""PreToolUse: refuse or annotate calls governed by a rule's triggers (stdin JSON)."""
|
|
1017
|
+
from .hooks import action_gate
|
|
1018
|
+
sys.exit(action_gate())
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
@cli.command('allow')
|
|
1022
|
+
@click.argument('slug')
|
|
1023
|
+
@click.option('--minutes', '-m', default=15, show_default=True,
|
|
1024
|
+
help='How long the override lasts (max 120)')
|
|
1025
|
+
def allow_cmd(slug, minutes):
|
|
1026
|
+
"""Let calls governed by SLUG's triggers through for a short window.
|
|
1027
|
+
|
|
1028
|
+
For when the user has explicitly asked for an action a saved rule would
|
|
1029
|
+
refuse. The override is logged, and the session's Stop check counts it as
|
|
1030
|
+
a decision rather than a violation.
|
|
1031
|
+
"""
|
|
1032
|
+
from .gate import allow
|
|
1033
|
+
repo = _require_repo()
|
|
1034
|
+
if repo.get(slug) is None:
|
|
1035
|
+
err.print(f'[red]no memory with slug {slug!r}[/red]')
|
|
1036
|
+
sys.exit(1)
|
|
1037
|
+
until = allow(repo, slug, minutes)
|
|
1038
|
+
console.print(f'[yellow]allowed[/yellow] calls governed by [cyan]{slug}[/cyan] '
|
|
1039
|
+
f'until {until.strftime("%H:%M:%S")} UTC')
|
|
1040
|
+
|
|
1041
|
+
|
|
943
1042
|
@hook.command('context-recall')
|
|
944
1043
|
def hook_context_recall():
|
|
945
1044
|
"""PostToolUse: hint when memories exist about the file being read (stdin JSON)."""
|
|
@@ -2507,8 +2606,13 @@ def gc(dry_run, squash_keep, reflog_keep, fmt_json):
|
|
|
2507
2606
|
help='With --repair-markup: only this project')
|
|
2508
2607
|
@click.option('--yes', 'confirm', is_flag=True,
|
|
2509
2608
|
help='With --repair-markup: write the repair as one checkpoint')
|
|
2609
|
+
@click.option('--rules', 'check_rules', is_flag=True,
|
|
2610
|
+
help="Compare this project's critical rules with the rule files "
|
|
2611
|
+
'its hosts load (CLAUDE.md, AGENTS.md, ...) and list '
|
|
2612
|
+
'same-topic passages to check for contradictions')
|
|
2510
2613
|
def doctor(relabel_path, prune_slugs, clean_caches, prune_sessions,
|
|
2511
|
-
full_audit, fmt_json, repair_markup, repair_project, confirm
|
|
2614
|
+
full_audit, fmt_json, repair_markup, repair_project, confirm,
|
|
2615
|
+
check_rules):
|
|
2512
2616
|
"""Diagnose and repair store hygiene: provenance, usage ledger, caches.
|
|
2513
2617
|
|
|
2514
2618
|
With no options, prints a report: quarantined (`_unknown`) and
|
|
@@ -2595,6 +2699,10 @@ def doctor(relabel_path, prune_slugs, clean_caches, prune_sessions,
|
|
|
2595
2699
|
acted = True
|
|
2596
2700
|
_repair_markup(repo, repair_project, confirm)
|
|
2597
2701
|
|
|
2702
|
+
if check_rules:
|
|
2703
|
+
acted = True
|
|
2704
|
+
_doctor_rules(repo, fmt_json)
|
|
2705
|
+
|
|
2598
2706
|
if acted:
|
|
2599
2707
|
return
|
|
2600
2708
|
|
|
@@ -2656,6 +2764,31 @@ def doctor(relabel_path, prune_slugs, clean_caches, prune_sessions,
|
|
|
2656
2764
|
else:
|
|
2657
2765
|
console.print('no memory carries tool-call markup in its fields')
|
|
2658
2766
|
|
|
2767
|
+
# Critical behaviour rules with no trigger are enforced only by recall —
|
|
2768
|
+
# the backlog the action gate cannot cover until each gets one.
|
|
2769
|
+
from .gate import needs_trigger
|
|
2770
|
+
from .links import filter_active
|
|
2771
|
+
from .project import detect_project, project_affinity
|
|
2772
|
+
here = detect_project()
|
|
2773
|
+
untrig = [m for m in filter_active(mnemonics)
|
|
2774
|
+
if needs_trigger(m.type_code, m.priority) and not m.triggers
|
|
2775
|
+
and not m.unverified]
|
|
2776
|
+
trig = [m for m in filter_active(mnemonics) if m.triggers]
|
|
2777
|
+
if untrig:
|
|
2778
|
+
mine = [m for m in untrig
|
|
2779
|
+
if here and m.project and project_affinity(m.project, here) >= 1]
|
|
2780
|
+
console.print(f'[yellow]{len(untrig)} critical behaviour rules (p3 fb/cn) '
|
|
2781
|
+
f'have no triggers[/yellow] — the action gate cannot '
|
|
2782
|
+
f'enforce them; {len(trig)} memories carry triggers')
|
|
2783
|
+
for m in sorted(mine, key=lambda m: m.timestamp, reverse=True)[:10]:
|
|
2784
|
+
console.print(f' [dim]{m.slug}[/dim]')
|
|
2785
|
+
if mine:
|
|
2786
|
+
console.print(f' [dim]({len(mine)} in {here}; add triggers with '
|
|
2787
|
+
f'save_memory triggers=[...])[/dim]')
|
|
2788
|
+
else:
|
|
2789
|
+
console.print(f'every critical behaviour rule carries triggers '
|
|
2790
|
+
f'({len(trig)} memories with triggers)')
|
|
2791
|
+
|
|
2659
2792
|
from .audit import audit as _audit
|
|
2660
2793
|
rep = _audit(repo)
|
|
2661
2794
|
_render_scope_losses(rep, full_audit)
|
|
@@ -2663,6 +2796,45 @@ def doctor(relabel_path, prune_slugs, clean_caches, prune_sessions,
|
|
|
2663
2796
|
console.print_json(data=rep)
|
|
2664
2797
|
|
|
2665
2798
|
|
|
2799
|
+
def _doctor_rules(repo, fmt_json: bool = False) -> None:
|
|
2800
|
+
"""Report same-topic pairs between critical rules and host rule files."""
|
|
2801
|
+
import json as _j
|
|
2802
|
+
from .drift import find_drift, rule_files
|
|
2803
|
+
from .project import detect_project
|
|
2804
|
+
project = detect_project()
|
|
2805
|
+
files = rule_files(Path.cwd())
|
|
2806
|
+
pairs = find_drift(repo.list(), files, project) if project else []
|
|
2807
|
+
if fmt_json:
|
|
2808
|
+
print(_j.dumps({
|
|
2809
|
+
'project': project,
|
|
2810
|
+
'files': [str(f) for f in files],
|
|
2811
|
+
'pairs': [{'slug': d.slug, 'rule': d.rule, 'file': str(d.passage.path),
|
|
2812
|
+
'line': d.passage.line, 'passage': d.passage.text,
|
|
2813
|
+
'score': d.score} for d in pairs],
|
|
2814
|
+
}, indent=2))
|
|
2815
|
+
return
|
|
2816
|
+
console.print(f'[bold]memgit doctor --rules[/bold] — project {project or "(none)"}, '
|
|
2817
|
+
f'{len(files)} rule file(s)')
|
|
2818
|
+
for f in files:
|
|
2819
|
+
console.print(f' [dim]{f}[/dim]')
|
|
2820
|
+
if not project:
|
|
2821
|
+
console.print('[yellow]no project detected here — run from the project '
|
|
2822
|
+
'directory[/yellow]')
|
|
2823
|
+
return
|
|
2824
|
+
if not pairs:
|
|
2825
|
+
console.print('no rule-file passage shares a topic with a critical rule')
|
|
2826
|
+
return
|
|
2827
|
+
console.print(f'{len(pairs)} same-topic pair(s). Each needs a reader: if the '
|
|
2828
|
+
'file says the opposite of the rule, change the file or '
|
|
2829
|
+
'supersede the rule, so the two stop disagreeing.\n')
|
|
2830
|
+
for d in pairs:
|
|
2831
|
+
rule = d.rule if len(d.rule) <= 200 else d.rule[:199] + '…'
|
|
2832
|
+
text = d.passage.text if len(d.passage.text) <= 240 else d.passage.text[:239] + '…'
|
|
2833
|
+
console.print(f'[cyan]{d.slug}[/cyan] [dim](score {d.score})[/dim]')
|
|
2834
|
+
console.print(f' rule: {rule}')
|
|
2835
|
+
console.print(f' {d.passage.path}:{d.passage.line}: {text}\n')
|
|
2836
|
+
|
|
2837
|
+
|
|
2666
2838
|
def _repair_markup(repo, project, confirm) -> None:
|
|
2667
2839
|
"""Split damaged memories back into their fields; dry run without confirm.
|
|
2668
2840
|
|
|
@@ -3480,7 +3652,7 @@ def setup_antigravity(dry_run):
|
|
|
3480
3652
|
|
|
3481
3653
|
#: substrings identifying a hook command as one of ours (any generation)
|
|
3482
3654
|
_MEMGIT_HOOK_SIGNS = ('resume --plain', 'hook prompt-recall', 'hook stop-guard',
|
|
3483
|
-
'hook context-recall', ' sync')
|
|
3655
|
+
'hook context-recall', 'hook action-gate', ' sync')
|
|
3484
3656
|
|
|
3485
3657
|
|
|
3486
3658
|
def _is_memgit_hook_entry(h: dict) -> bool:
|
|
@@ -3499,11 +3671,13 @@ def _is_memgit_hook_entry(h: dict) -> bool:
|
|
|
3499
3671
|
help='Skip the end-of-session capture guard (Stop)')
|
|
3500
3672
|
@click.option('--no-ctx-recall', is_flag=True,
|
|
3501
3673
|
help='Skip the context-recall hook (PostToolUse on Read/Grep/Glob)')
|
|
3674
|
+
@click.option('--no-gate', is_flag=True,
|
|
3675
|
+
help='Skip the action gate (PreToolUse on Bash and file writes)')
|
|
3502
3676
|
@click.option('--dry-run', is_flag=True, help='Show the change without writing')
|
|
3503
|
-
def setup_hooks(remove, no_recall, no_guard, no_ctx_recall, dry_run):
|
|
3677
|
+
def setup_hooks(remove, no_recall, no_guard, no_ctx_recall, no_gate, dry_run):
|
|
3504
3678
|
"""Install the Claude Code hooks that make memory automatic.
|
|
3505
3679
|
|
|
3506
|
-
|
|
3680
|
+
Six hooks, one principle: what a hook enforces happens, what a tool
|
|
3507
3681
|
description suggests mostly doesn't (measured: 6% voluntary engagement
|
|
3508
3682
|
vs 100% hook delivery).
|
|
3509
3683
|
|
|
@@ -3513,13 +3687,16 @@ def setup_hooks(remove, no_recall, no_guard, no_ctx_recall, dry_run):
|
|
|
3513
3687
|
memory index
|
|
3514
3688
|
UserPromptSubmit inject memories relevant to each prompt (BM25,
|
|
3515
3689
|
silent when nothing clears the relevance bar)
|
|
3690
|
+
PreToolUse action gate — a Bash or file-write call matching a
|
|
3691
|
+
memory's triggers is refused (critical rules) or
|
|
3692
|
+
annotated (the rest); trigger cache only
|
|
3516
3693
|
PostToolUse context recall — reading a file whose path matches
|
|
3517
3694
|
a memory tag surfaces a one-line depth hint
|
|
3518
3695
|
(tagmap cache only, capped 3/session)
|
|
3519
|
-
Stop capture guard — a
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
markdown memories
|
|
3696
|
+
Stop compliance replay + capture guard — a governed call
|
|
3697
|
+
that went through, or a substantive session ending
|
|
3698
|
+
with zero memory writes, gets ONE block; plus async
|
|
3699
|
+
`memgit sync` to checkpoint markdown memories
|
|
3523
3700
|
|
|
3524
3701
|
Hooks live in ~/.claude/settings.json (unlike MCP servers, which live
|
|
3525
3702
|
in ~/.claude.json).
|
|
@@ -3550,6 +3727,10 @@ def setup_hooks(remove, no_recall, no_guard, no_ctx_recall, dry_run):
|
|
|
3550
3727
|
{'type': 'command',
|
|
3551
3728
|
'command': f'{base} hook prompt-recall 2>/dev/null || true'},
|
|
3552
3729
|
],
|
|
3730
|
+
'PreToolUse': [] if no_gate else [
|
|
3731
|
+
{'type': 'command',
|
|
3732
|
+
'command': f'{base} hook action-gate 2>/dev/null || true'},
|
|
3733
|
+
],
|
|
3553
3734
|
'PostToolUse': [] if no_ctx_recall else [
|
|
3554
3735
|
{'type': 'command',
|
|
3555
3736
|
'command': f'{base} hook context-recall 2>/dev/null || true'},
|
|
@@ -3569,10 +3750,11 @@ def setup_hooks(remove, no_recall, no_guard, no_ctx_recall, dry_run):
|
|
|
3569
3750
|
}
|
|
3570
3751
|
#: PostToolUse fires per tool call — matcher limits it to the file-reading
|
|
3571
3752
|
#: tools so the hook binary isn't spawned on every Bash/Edit.
|
|
3572
|
-
|
|
3753
|
+
from .gate import GATE_MATCHER
|
|
3754
|
+
_MATCHERS = {'PreToolUse': GATE_MATCHER, 'PostToolUse': 'Read|Grep|Glob'}
|
|
3573
3755
|
|
|
3574
3756
|
changed = []
|
|
3575
|
-
for event in ('SessionStart', 'UserPromptSubmit', 'PostToolUse', 'Stop'):
|
|
3757
|
+
for event in ('SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop'):
|
|
3576
3758
|
entries = hooks.setdefault(event, [])
|
|
3577
3759
|
had = [h for h in entries if isinstance(h, dict) and _is_memgit_hook_entry(h)]
|
|
3578
3760
|
entries[:] = [h for h in entries if h not in had]
|
|
@@ -3600,7 +3782,9 @@ def setup_hooks(remove, no_recall, no_guard, no_ctx_recall, dry_run):
|
|
|
3600
3782
|
tag = ' [dim](async)[/dim]' if inner.get('async') else ''
|
|
3601
3783
|
console.print(f' [cyan]{event}[/cyan] [dim]{inner["command"]}[/dim]{tag}')
|
|
3602
3784
|
console.print('[dim]Resume at session start, relevant memories per prompt, '
|
|
3603
|
-
'
|
|
3785
|
+
'rule triggers checked before Bash and file writes, '
|
|
3786
|
+
'context hints on file reads, compliance + capture guard + '
|
|
3787
|
+
'sync at stop.[/dim]')
|
|
3604
3788
|
|
|
3605
3789
|
|
|
3606
3790
|
@setup.command('print-config')
|
|
@@ -3758,8 +3942,8 @@ except Exception:
|
|
|
3758
3942
|
# extras missing or broken — plain memgit must keep working untouched
|
|
3759
3943
|
@cli.group()
|
|
3760
3944
|
def cloud():
|
|
3761
|
-
"""E2E-encrypted team sync —
|
|
3945
|
+
"""E2E-encrypted team sync — its dependencies failed to import; reinstall memgit."""
|
|
3762
3946
|
|
|
3763
3947
|
@cloud.command('setup', help="Show how to enable cloud sync.")
|
|
3764
3948
|
def _cloud_setup_hint():
|
|
3765
|
-
console.print(
|
|
3949
|
+
console.print("Cloud sync could not load pynacl or httpx. Reinstall: [bold]pip install --upgrade --force-reinstall memgit[/bold]")
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""memgit cloud — E2E-encrypted team sync (app.memgit.dev).
|
|
2
|
+
|
|
3
|
+
Nothing here runs until a store is logged in and linked. The rest of memgit
|
|
4
|
+
imports only `auto`, lazily, to start a background sync after a commit, at
|
|
5
|
+
session start, and from a read when the last sync is old. pynacl and httpx are
|
|
6
|
+
ordinary dependencies of the package.
|
|
7
|
+
"""
|