loop-memory 0.4.2__tar.gz → 0.4.4__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 (130) hide show
  1. {loop_memory-0.4.2 → loop_memory-0.4.4}/PKG-INFO +34 -1
  2. {loop_memory-0.4.2 → loop_memory-0.4.4}/README.md +33 -0
  3. loop_memory-0.4.4/loop_memory/cli/commands/rules.py +245 -0
  4. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/cli/main.py +5 -0
  5. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/jobs/cognitive.py +153 -19
  6. loop_memory-0.4.4/loop_memory/llm/openai_adapter.py +64 -0
  7. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/llm/providers.py +74 -6
  8. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/sdk_extensions.py +7 -0
  9. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/serve/routes/cognitive.py +7 -0
  10. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/storage/sqlite_store.py +47 -1
  11. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory.egg-info/PKG-INFO +34 -1
  12. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory.egg-info/SOURCES.txt +3 -0
  13. {loop_memory-0.4.2 → loop_memory-0.4.4}/pyproject.toml +1 -1
  14. loop_memory-0.4.4/tests/test_cli_rules.py +161 -0
  15. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_graph.py +52 -0
  16. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_llm_providers.py +111 -0
  17. loop_memory-0.4.4/tests/test_short_query.py +143 -0
  18. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_universal_memory.py +210 -0
  19. loop_memory-0.4.2/loop_memory/llm/openai_adapter.py +0 -31
  20. {loop_memory-0.4.2 → loop_memory-0.4.4}/LICENSE +0 -0
  21. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/__init__.py +0 -0
  22. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/backends/__init__.py +0 -0
  23. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/backends/embedding.py +0 -0
  24. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/backends/sentence_embedder.py +0 -0
  25. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/backends/vector_store.py +0 -0
  26. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/cli/__init__.py +0 -0
  27. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/cli/_common.py +0 -0
  28. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/cli/commands/__init__.py +0 -0
  29. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/cli/commands/cognitive.py +0 -0
  30. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/cli/commands/diag.py +0 -0
  31. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/cli/commands/graph.py +0 -0
  32. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/cli/commands/hooks.py +0 -0
  33. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/cli/commands/read.py +0 -0
  34. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/cli/commands/serve.py +0 -0
  35. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/cli/commands/write.py +0 -0
  36. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/engine/__init__.py +0 -0
  37. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/engine/loop.py +0 -0
  38. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/engine/reflect.py +0 -0
  39. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/examples/__init__.py +0 -0
  40. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/examples/demo.py +0 -0
  41. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/export/__init__.py +0 -0
  42. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/export/memory_md.py +0 -0
  43. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/graph/__init__.py +0 -0
  44. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/graph/build.py +0 -0
  45. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/graph/extract.py +0 -0
  46. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/ingest/__init__.py +0 -0
  47. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/ingest/loader.py +0 -0
  48. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/ingest/pipeline.py +0 -0
  49. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/jobs/__init__.py +0 -0
  50. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/jobs/compact.py +0 -0
  51. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/jobs/consolidate.py +0 -0
  52. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/jobs/contradiction.py +0 -0
  53. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/jobs/evolution.py +0 -0
  54. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/jobs/graph.py +0 -0
  55. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/jobs/llm_compact_pass.py +0 -0
  56. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/jobs/llm_consolidate.py +0 -0
  57. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/jobs/scheduler.py +0 -0
  58. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/llm/__init__.py +0 -0
  59. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/llm/base.py +0 -0
  60. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/mcp/__init__.py +0 -0
  61. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/memory/__init__.py +0 -0
  62. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/memory/types.py +0 -0
  63. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/privacy/__init__.py +0 -0
  64. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/privacy/private.py +0 -0
  65. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/privacy/redact.py +0 -0
  66. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/py.typed +0 -0
  67. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/sdk.py +0 -0
  68. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/security/__init__.py +0 -0
  69. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/security/secrets.py +0 -0
  70. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/serve/__init__.py +0 -0
  71. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/serve/app.py +0 -0
  72. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/serve/handlers.py +0 -0
  73. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/serve/routes/_shared.py +0 -0
  74. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/serve/routes/admin.py +0 -0
  75. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/serve/routes/export.py +0 -0
  76. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/serve/routes/graph.py +0 -0
  77. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/serve/routes/insights.py +0 -0
  78. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/serve/routes/memories.py +0 -0
  79. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/serve/routes/sessions.py +0 -0
  80. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/serve/routes/system.py +0 -0
  81. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/serve/routes/wiki.py +0 -0
  82. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/serve/static/__init__.py +0 -0
  83. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/serve/static/index.html +0 -0
  84. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/serve/watcher.py +0 -0
  85. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/storage/__init__.py +0 -0
  86. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/storage/retrieval.py +0 -0
  87. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/wiki/__init__.py +0 -0
  88. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/wiki/backfill.py +0 -0
  89. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/wiki/classifier.py +0 -0
  90. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/wiki/prompts.py +0 -0
  91. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory/wiki/scope.py +0 -0
  92. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory.egg-info/dependency_links.txt +0 -0
  93. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory.egg-info/entry_points.txt +0 -0
  94. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory.egg-info/requires.txt +0 -0
  95. {loop_memory-0.4.2 → loop_memory-0.4.4}/loop_memory.egg-info/top_level.txt +0 -0
  96. {loop_memory-0.4.2 → loop_memory-0.4.4}/setup.cfg +0 -0
  97. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_admin_ingest_route.py +0 -0
  98. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_agent_memory_api.py +0 -0
  99. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_agent_memory_sdk.py +0 -0
  100. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_auth_token_rotate.py +0 -0
  101. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_cli_v7.py +0 -0
  102. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_cli_version.py +0 -0
  103. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_contradictions.py +0 -0
  104. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_evolution.py +0 -0
  105. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_evolution_quality.py +0 -0
  106. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_expanduser.py +0 -0
  107. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_export_ask.py +0 -0
  108. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_ingest.py +0 -0
  109. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_llm_consolidator.py +0 -0
  110. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_llm_fingerprint_not_persisted.py +0 -0
  111. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_llm_test_endpoint.py +0 -0
  112. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_loop.py +0 -0
  113. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_mcp.py +0 -0
  114. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_memories_pagination.py +0 -0
  115. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_openclaw_loader.py +0 -0
  116. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_reflection.py +0 -0
  117. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_repo_framing.py +0 -0
  118. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_score_api.py +0 -0
  119. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_scoring_v2.py +0 -0
  120. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_secrets.py +0 -0
  121. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_serve_app.py +0 -0
  122. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_serve_handlers.py +0 -0
  123. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_session_order.py +0 -0
  124. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_store.py +0 -0
  125. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_summarization.py +0 -0
  126. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_vector_store.py +0 -0
  127. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_watcher.py +0 -0
  128. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_wiki_classifier.py +0 -0
  129. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_wiki_export_escape.py +0 -0
  130. {loop_memory-0.4.2 → loop_memory-0.4.4}/tests/test_wiki_prompts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: loop-memory
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: A general-purpose, local memory system for every AI agent you run. Loop Memory auto-captures conversations from Codex / Claude / Hermes / OpenClaw, scores them by importance × recency × usage × feedback, distils them into a curated wiki, and serves everything from a single web UI.
5
5
  Author: Loop Memory contributors <loop-memory@users.noreply.github.com>
6
6
  License-Expression: MIT
@@ -402,6 +402,15 @@ pass** over the store:
402
402
 
403
403
  Dry-run by default; pass `--apply` to commit.
404
404
 
405
+ **Observability (since 0.4.3).** The report carries per-stage
406
+ timings (``scan``, ``stale``, ``merge``, ``contradict``, ``apply``,
407
+ ``audit``) and an explicit ``aborted`` flag with an ``abort_reason``
408
+ that names the stage the budget fired in. Pass
409
+ ``--deadline-seconds <N>`` (HTTP: ``POST /api/v1/cognitive/sleep``
410
+ with ``{"deadline_seconds": N}``; SDK: ``client.cognitive_sleep(deadline_seconds=N)``)
411
+ to bound the sweep — useful for nightly cron, where a stuck
412
+ sweep should leave a loud trace instead of a silent spinner.
413
+
405
414
  ### Knowledge graph
406
415
 
407
416
  `loop-memory graph-rebuild` extracts entities from every distilled
@@ -423,6 +432,7 @@ automatically:
423
432
  | Command | What it does |
424
433
  | -------------------------------- | ---------------------------------------------------------------------------- |
425
434
  | `loop-memory install-hooks` | Auto-detect `~/.codex`, `~/.claude`, `~/.hermes` and write MCP + SessionStart hook configs in place. Idempotent — re-run any time. |
435
+ | `loop-memory rules --write` | Append the three-phase memory-discipline block (task start / mid-task / wrap-up) into the agent's rule file (`AGENTS.md` for codex / hermes / openclaw, `CLAUDE.md` for claude). **Never overwrites user content.** |
426
436
  | `loop-memory inject [query]` | Print a `# Long-term memory context` markdown block (distilled wiki + recent relevant memories) for a SessionStart hook. |
427
437
  | `loop-memory mcp` | Run the **stdio MCP server** with memory, graph, and cognitive tools (`recall`, `remember`, `forget`, `feedback`, `remember_edge`, `subgraph`, `cognitive_sleep`, `audit`, and wiki tools). |
428
438
 
@@ -439,6 +449,11 @@ loop-memory install-hooks # writes ~/.codex/config.toml + ~/.claude/{mcp.j
439
449
  Manual smoke-test without restarting the client:
440
450
 
441
451
  ```bash
452
+ # one-shot: install the memory-discipline block into the agent's
453
+ # rule file so the client calls `recall` on every task start.
454
+ loop-memory rules --agent codex --write # writes ./AGENTS.md (append, never overwrite)
455
+ loop-memory rules --agent claude --write # writes ./CLAUDE.md
456
+
442
457
  loop-memory inject # dumps the warm-start block to stdout
443
458
  printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"wiki_summary"}}\n' \
444
459
  | loop-memory mcp # round-trips JSON-RPC over stdio
@@ -632,6 +647,24 @@ classifier is local and makes no model or network request on a wiki write.
632
647
 
633
648
  ---
634
649
 
650
+ ### LLM env-var overrides
651
+
652
+ The OpenAI-compat / Anthropic / Ollama providers (and the optional
653
+ ``openai`` adapter) honour two env-var knobs so you can pin
654
+ distillation deterministically without touching the behaviour
655
+ config:
656
+
657
+ - ``LLM_TEMPERATURE`` — float, defaults to ``0.3`` (or the explicit
658
+ ``kwargs.temperature``). Invalid values are ignored with a warning.
659
+ - ``LLM_SEED`` — int, sent as ``seed`` for OpenAI / Anthropic /
660
+ Ollama where supported. Omitting it preserves the existing
661
+ "no seed" behaviour so older call sites do not need to migrate.
662
+
663
+ Both env vars are read at every ``complete()`` call, so a single
664
+ ``export LLM_SEED=42`` plus a nightly cron makes wiki distillation
665
+ reproducible. Pinned by 11 cases in
666
+ ``tests/test_llm_providers.py::LLMEnvVarTests``.
667
+
635
668
  ## FAQ & troubleshooting
636
669
 
637
670
  **Q: `pip install loop-memory` succeeds but `loop-memory serve` says `ModuleNotFoundError: No module named 'fastapi'`.**
@@ -358,6 +358,15 @@ pass** over the store:
358
358
 
359
359
  Dry-run by default; pass `--apply` to commit.
360
360
 
361
+ **Observability (since 0.4.3).** The report carries per-stage
362
+ timings (``scan``, ``stale``, ``merge``, ``contradict``, ``apply``,
363
+ ``audit``) and an explicit ``aborted`` flag with an ``abort_reason``
364
+ that names the stage the budget fired in. Pass
365
+ ``--deadline-seconds <N>`` (HTTP: ``POST /api/v1/cognitive/sleep``
366
+ with ``{"deadline_seconds": N}``; SDK: ``client.cognitive_sleep(deadline_seconds=N)``)
367
+ to bound the sweep — useful for nightly cron, where a stuck
368
+ sweep should leave a loud trace instead of a silent spinner.
369
+
361
370
  ### Knowledge graph
362
371
 
363
372
  `loop-memory graph-rebuild` extracts entities from every distilled
@@ -379,6 +388,7 @@ automatically:
379
388
  | Command | What it does |
380
389
  | -------------------------------- | ---------------------------------------------------------------------------- |
381
390
  | `loop-memory install-hooks` | Auto-detect `~/.codex`, `~/.claude`, `~/.hermes` and write MCP + SessionStart hook configs in place. Idempotent — re-run any time. |
391
+ | `loop-memory rules --write` | Append the three-phase memory-discipline block (task start / mid-task / wrap-up) into the agent's rule file (`AGENTS.md` for codex / hermes / openclaw, `CLAUDE.md` for claude). **Never overwrites user content.** |
382
392
  | `loop-memory inject [query]` | Print a `# Long-term memory context` markdown block (distilled wiki + recent relevant memories) for a SessionStart hook. |
383
393
  | `loop-memory mcp` | Run the **stdio MCP server** with memory, graph, and cognitive tools (`recall`, `remember`, `forget`, `feedback`, `remember_edge`, `subgraph`, `cognitive_sleep`, `audit`, and wiki tools). |
384
394
 
@@ -395,6 +405,11 @@ loop-memory install-hooks # writes ~/.codex/config.toml + ~/.claude/{mcp.j
395
405
  Manual smoke-test without restarting the client:
396
406
 
397
407
  ```bash
408
+ # one-shot: install the memory-discipline block into the agent's
409
+ # rule file so the client calls `recall` on every task start.
410
+ loop-memory rules --agent codex --write # writes ./AGENTS.md (append, never overwrite)
411
+ loop-memory rules --agent claude --write # writes ./CLAUDE.md
412
+
398
413
  loop-memory inject # dumps the warm-start block to stdout
399
414
  printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}\n{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"wiki_summary"}}\n' \
400
415
  | loop-memory mcp # round-trips JSON-RPC over stdio
@@ -588,6 +603,24 @@ classifier is local and makes no model or network request on a wiki write.
588
603
 
589
604
  ---
590
605
 
606
+ ### LLM env-var overrides
607
+
608
+ The OpenAI-compat / Anthropic / Ollama providers (and the optional
609
+ ``openai`` adapter) honour two env-var knobs so you can pin
610
+ distillation deterministically without touching the behaviour
611
+ config:
612
+
613
+ - ``LLM_TEMPERATURE`` — float, defaults to ``0.3`` (or the explicit
614
+ ``kwargs.temperature``). Invalid values are ignored with a warning.
615
+ - ``LLM_SEED`` — int, sent as ``seed`` for OpenAI / Anthropic /
616
+ Ollama where supported. Omitting it preserves the existing
617
+ "no seed" behaviour so older call sites do not need to migrate.
618
+
619
+ Both env vars are read at every ``complete()`` call, so a single
620
+ ``export LLM_SEED=42`` plus a nightly cron makes wiki distillation
621
+ reproducible. Pinned by 11 cases in
622
+ ``tests/test_llm_providers.py::LLMEnvVarTests``.
623
+
591
624
  ## FAQ & troubleshooting
592
625
 
593
626
  **Q: `pip install loop-memory` succeeds but `loop-memory serve` says `ModuleNotFoundError: No module named 'fastapi'`.**
@@ -0,0 +1,245 @@
1
+ """rules: install a memory-discipline block into the agent's rule file.
2
+
3
+ Audit 2026-08-23: pattern adopted from
4
+ ``2672243194/agentbrain`` v0.4.3 (``agentbrain rules --agent … --write``,
5
+ which appends a three-phase workflow block into CLAUDE.md /
6
+ AGENTS.md / Cursor / Trae rule files). Loop Memory's port wires
7
+ the same discipline into the four shipped agent targets:
8
+
9
+ * ``codex`` -> ``AGENTS.md`` (Codex CLI convention)
10
+ * ``claude`` -> ``CLAUDE.md`` (Claude Code convention)
11
+ * ``hermes`` -> ``AGENTS.md`` (Hermes CLI also reads AGENTS.md)
12
+ * ``openclaw`` -> ``AGENTS.md`` (OpenClaw reads AGENTS.md)
13
+ * ``generic`` -> ``AGENTS.md`` (any other AGENTS.md-aware client)
14
+
15
+ Safety properties:
16
+
17
+ * Existing rule files are **never overwritten** — the block is
18
+ appended after the user's content. Their rules stay byte-identical.
19
+ * **Idempotent** — a marker comment detects a prior install and
20
+ skips the write.
21
+ * The CLI exits 0 even when the target file does not exist (it
22
+ creates an empty one) and when no ``--agent`` is given (it just
23
+ prints the generic block to stdout).
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import sys
29
+ from pathlib import Path
30
+
31
+
32
+ # Marker line used to detect a prior install. The leading space +
33
+ # trailing space keep the marker from accidentally colliding with a
34
+ # user's prose like "loop-memory is great". Keep this string in sync
35
+ # with the appended block below.
36
+ MARKER = "<!-- loop-memory:rules:installed -->"
37
+
38
+ # Agent-name -> relative path under cwd. Unknown agents fall back
39
+ # to ``generic`` (AGENTS.md).
40
+ AGENT_TARGETS: dict[str, str] = {
41
+ "codex": "AGENTS.md",
42
+ "claude": "CLAUDE.md",
43
+ "hermes": "AGENTS.md",
44
+ "openclaw": "AGENTS.md",
45
+ "generic": "AGENTS.md",
46
+ }
47
+
48
+
49
+ def _generic_block() -> str:
50
+ """The discipline block written to every agent's rule file."""
51
+ return (
52
+ f"\n{MARKER}\n"
53
+ "# Memory discipline (auto-installed by `loop-memory rules`).\n"
54
+ "# Re-run the same command to refresh in place; manual edits\n"
55
+ "# between the marker lines will be preserved on the next refresh.\n"
56
+ "\n"
57
+ "## At task start\n"
58
+ "\n"
59
+ "Before you read or write anything in this project, query Loop\n"
60
+ "Memory first so you can act on prior work instead of rebuilding\n"
61
+ "it. Use the project's existing memory tooling — for example\n"
62
+ "`loop-memory recall \"<task topic>\"` (shell), the `recall` MCP\n"
63
+ "tool, or the `inject` SessionStart hook if one is wired up.\n"
64
+ "Read the top hits before you start typing.\n"
65
+ "\n"
66
+ "## Mid-task\n"
67
+ "\n"
68
+ "Re-query whenever you switch subtask, hit an unexpected error,\n"
69
+ "or follow up a topic the initial query did not cover. Plain\n"
70
+ "continuation of the same line of thought does not need a\n"
71
+ "re-query — that is what saves tokens.\n"
72
+ "\n"
73
+ "## Wrap-up\n"
74
+ "\n"
75
+ "Each distinct reusable lesson — a non-obvious gotcha, a\n"
76
+ "convention you wish you'd known, a fix to a bug class —\n"
77
+ "should be written back via `loop-memory ingest` / the `add`\n"
78
+ "MCP tool, with user confirmation so nothing private lands in\n"
79
+ "the vault. Never paste secrets, tokens, or local paths into\n"
80
+ "a memory; use `${ENV:VAR_NAME}` placeholders or omit them.\n"
81
+ f"\n{MARKER} (end)\n"
82
+ )
83
+
84
+
85
+ def _render_block(agent: str) -> str:
86
+ """Return the discipline block tailored to ``agent``."""
87
+ base = _generic_block()
88
+ if agent in {"claude", "codex"}:
89
+ return base
90
+ # Hermes / OpenClaw: same text but the explicit tool name in the
91
+ # hook line differs because the MCP tool prefix varies. The block
92
+ # already names the tool generically; we add one agent-specific
93
+ # clarifying line at the bottom for these two.
94
+ if agent == "hermes":
95
+ extra = (
96
+ "\n## Hermes-specific\n"
97
+ "\n"
98
+ "Hermes exposes Loop Memory through MCP too. The same\n"
99
+ "`recall` / `add` tool names apply; if you do not see\n"
100
+ "them in the MCP tool list, run\n"
101
+ "`loop-memory install-hooks` once and restart Hermes.\n"
102
+ )
103
+ return base.replace(f"\n{MARKER} (end)\n", extra + f"\n{MARKER} (end)\n")
104
+ if agent == "openclaw":
105
+ extra = (
106
+ "\n## OpenClaw-specific\n"
107
+ "\n"
108
+ "OpenClaw sessions are auto-ingested when\n"
109
+ "`loop-memory hook --source openclaw --watch <sessions-dir>`\n"
110
+ "is running. If the recall hits are empty, check that\n"
111
+ "the watcher is alive (`loop-memory doctor`) and that\n"
112
+ "the workspace log directory exists.\n"
113
+ )
114
+ return base.replace(f"\n{MARKER} (end)\n", extra + f"\n{MARKER} (end)\n")
115
+ return base
116
+
117
+
118
+ def _resolve_target(agent: str, cwd: Path) -> Path:
119
+ """Map an agent name to its rule-file path under ``cwd``."""
120
+ rel = AGENT_TARGETS.get(agent)
121
+ if rel is None:
122
+ # Unknown agent — keep the same path as ``generic`` so the
123
+ # user still gets a working AGENTS.md and can rename it.
124
+ rel = AGENT_TARGETS["generic"]
125
+ return cwd / rel
126
+
127
+
128
+ def _install(cwd: Path, agent: str, *, force: bool) -> tuple[str, str]:
129
+ """Install the block; return (status, message).
130
+
131
+ ``status`` is one of: ``installed``, ``already-installed``,
132
+ ``appended``, ``dry-run``.
133
+ """
134
+ target = _resolve_target(agent, cwd)
135
+ existing = ""
136
+ if target.exists():
137
+ try:
138
+ existing = target.read_text(encoding="utf-8")
139
+ except Exception as e:
140
+ return "error", f"could not read {target}: {e}"
141
+ if MARKER in existing and not force:
142
+ return "already-installed", str(target)
143
+ # ``run_rules`` does the actual write; this helper just classifies
144
+ # the request so callers can unit-test the marker-detection path.
145
+ return "installed", str(target)
146
+
147
+
148
+ def run_rules(args: list) -> int:
149
+ """``loop-memory rules [--agent NAME] [--write] [--force] [--cwd PATH]``.
150
+
151
+ Behaviour:
152
+ * no args -> print the generic block to stdout
153
+ * ``--agent NAME`` -> print the agent-tailored block
154
+ * ``--write`` -> install (append) into the agent's
155
+ rule file under cwd (or ``--cwd``)
156
+ * ``--force`` -> overwrite a prior install in place
157
+ (still never touches user content
158
+ outside the marker lines)
159
+ """
160
+ agent = "generic"
161
+ write = False
162
+ force = False
163
+ cwd = Path.cwd()
164
+ it = iter(args)
165
+ for tok in it:
166
+ if tok == "--agent":
167
+ try:
168
+ agent = next(it)
169
+ except StopIteration:
170
+ print("--agent requires a name", file=sys.stderr)
171
+ return 2
172
+ elif tok == "--write":
173
+ write = True
174
+ elif tok == "--force":
175
+ force = True
176
+ elif tok == "--cwd":
177
+ try:
178
+ cwd = Path(next(it)).expanduser()
179
+ except StopIteration:
180
+ print("--cwd requires a path", file=sys.stderr)
181
+ return 2
182
+ elif tok in {"-h", "--help"}:
183
+ print(__doc__ or "loop-memory rules [--agent NAME] [--write] [--force]")
184
+ return 0
185
+ else:
186
+ print(f"unknown argument: {tok}", file=sys.stderr)
187
+ return 2
188
+
189
+ if not write:
190
+ sys.stdout.write(_render_block(agent))
191
+ return 0
192
+
193
+ target = _resolve_target(agent, cwd)
194
+ existing = ""
195
+ if target.exists():
196
+ try:
197
+ existing = target.read_text(encoding="utf-8")
198
+ except Exception as e:
199
+ print(f"could not read {target}: {e}", file=sys.stderr)
200
+ return 1
201
+
202
+ block = _render_block(agent)
203
+ if MARKER in existing and not force:
204
+ print(f"[loop-memory] rules: already installed in {target}")
205
+ return 0
206
+ if MARKER in existing and force:
207
+ # Replace the existing marker span in place: keep everything
208
+ # before the start marker and everything after the close
209
+ # marker, splice a fresh block between them. User content
210
+ # outside the marker lines is preserved byte-for-byte.
211
+ start_idx = existing.index(MARKER)
212
+ close_idx = existing.find(MARKER + " (end)", start_idx)
213
+ if close_idx < 0:
214
+ # Partial / corrupted install — fall back to append.
215
+ close_idx = len(existing)
216
+ else:
217
+ close_idx = existing.index("\n", close_idx)
218
+ new_content = existing[:start_idx].rstrip("\n") + "\n" + block + existing[close_idx:]
219
+ action = "refreshed"
220
+ else:
221
+ sep = ""
222
+ if existing and not existing.endswith("\n"):
223
+ sep = "\n"
224
+ new_content = existing + sep + block
225
+ action = "appended" if existing else "created"
226
+
227
+ try:
228
+ target.parent.mkdir(parents=True, exist_ok=True)
229
+ target.write_text(new_content, encoding="utf-8")
230
+ except Exception as e:
231
+ print(f"[loop-memory] rules: write failed: {e}", file=sys.stderr)
232
+ return 1
233
+ print(f"[loop-memory] rules: {action} {target}")
234
+ return 0
235
+
236
+
237
+ __all__ = [
238
+ "AGENT_TARGETS",
239
+ "MARKER",
240
+ "_generic_block",
241
+ "_install",
242
+ "_render_block",
243
+ "_resolve_target",
244
+ "run_rules",
245
+ ]
@@ -17,6 +17,8 @@ Usage:
17
17
  loop-memory consolidate-now # ask the running server to trigger a pass right now
18
18
  loop-memory export # legacy markdown export (no positional path)
19
19
  loop-memory digest [--out PATH] # compact knowledge digest for AGENTS.md (≤ max-chars bytes)
20
+ loop-memory rules [--agent codex|claude|hermes|openclaw] [--write] [--force]
21
+ # install the three-phase memory discipline block into the agent's rule file
20
22
  loop-memory ask "what about…" # print a paste-ready context block for any LLM client
21
23
  loop-memory cognitive-sleep [--apply] # dry-run / apply cognitive sweep (v7)
22
24
  loop-memory audit [--kind X] [--action Y] # read the cognitive audit trail
@@ -38,6 +40,7 @@ from .commands import diag as diag_cmd
38
40
  from .commands import graph as graph_cmd
39
41
  from .commands import hooks as hooks_cmd
40
42
  from .commands import read as read_cmd
43
+ from .commands import rules as rules_cmd
41
44
  from .commands import serve as serve_cmd
42
45
  from .commands import write as write_cmd
43
46
 
@@ -113,6 +116,7 @@ COMMANDS = {
113
116
  "subgraph": cognitive_cmd.run_subgraph,
114
117
  "graph-rebuild": cognitive_cmd.run_graph_rebuild,
115
118
  "wiki-reclassify-legacy": cognitive_cmd.run_wiki_reclassify_legacy,
119
+ "rules": rules_cmd.run_rules,
116
120
  "version": _run_version,
117
121
  }
118
122
 
@@ -152,6 +156,7 @@ COMMAND_HELP: dict[str, str] = {
152
156
  "subgraph": "loop-memory subgraph <query> # print a small subgraph.",
153
157
  "version": "loop-memory version # print the installed distribution version.",
154
158
  "wiki-reclassify-legacy": "loop-memory wiki-reclassify-legacy # back-fill scope + scope_filter for H4+H5 pages.",
159
+ "rules": "loop-memory rules [--agent codex|claude|hermes|openclaw] [--write] [--force]\n Print, or append into the agent's rule file, the three-phase memory discipline (task start / mid-task / wrap-up). Never overwrites user content.",
155
160
  }
156
161
 
157
162