memkernel 0.4.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {memkernel-0.4.0 → memkernel-0.5.0}/PKG-INFO +33 -3
  2. {memkernel-0.4.0 → memkernel-0.5.0}/README.md +30 -2
  3. {memkernel-0.4.0 → memkernel-0.5.0}/pyproject.toml +7 -1
  4. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/__init__.py +1 -1
  5. memkernel-0.5.0/src/memkernel/config.py +132 -0
  6. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/mcp/tools/wiki.py +27 -2
  7. memkernel-0.5.0/src/memkernel/wiki/studio.py +193 -0
  8. memkernel-0.5.0/src/memkernel/wiki/sync.py +425 -0
  9. memkernel-0.4.0/src/memkernel/config.py +0 -70
  10. memkernel-0.4.0/src/memkernel/wiki/sync.py +0 -228
  11. {memkernel-0.4.0 → memkernel-0.5.0}/LICENSE +0 -0
  12. {memkernel-0.4.0 → memkernel-0.5.0}/NOTICE +0 -0
  13. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/adapters/__init__.py +0 -0
  14. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/adapters/rlmstudio.py +0 -0
  15. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/api/__init__.py +0 -0
  16. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/api/app.py +0 -0
  17. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/api/routes/__init__.py +0 -0
  18. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/api/routes/admin.py +0 -0
  19. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/core/__init__.py +0 -0
  20. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/core/models.py +0 -0
  21. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/core/ports.py +0 -0
  22. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/core/services.py +0 -0
  23. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/factory.py +0 -0
  24. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/lifecycle.py +0 -0
  25. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/mcp/__init__.py +0 -0
  26. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/mcp/server.py +0 -0
  27. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/mcp/tools/__init__.py +0 -0
  28. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/mcp/tools/memory.py +0 -0
  29. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/memory/__init__.py +0 -0
  30. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/memory/dedup.py +0 -0
  31. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/memory/lifecycle.py +0 -0
  32. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/memory/types.py +0 -0
  33. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/py.typed +0 -0
  34. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/retrieval/__init__.py +0 -0
  35. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/retrieval/chunking.py +0 -0
  36. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/retrieval/embeddings.py +0 -0
  37. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/retrieval/reasoner.py +0 -0
  38. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/retrieval/search.py +0 -0
  39. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/retrieval/sparse.py +0 -0
  40. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/storage/__init__.py +0 -0
  41. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/storage/chroma.py +0 -0
  42. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/storage/documents.py +0 -0
  43. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/wiki/__init__.py +0 -0
  44. {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/wiki/pages.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memkernel
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Code-aware memory server for AI coding copilots
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Topic :: Software Development :: Libraries
16
+ Provides-Extra: wiki
16
17
  Requires-Dist: chromadb (>=1.0,<2)
17
18
  Requires-Dist: fastapi (>=0.115,<1)
18
19
  Requires-Dist: mcp (>=1.25,<2)
@@ -22,6 +23,7 @@ Requires-Dist: rank-bm25 (>=0.2)
22
23
  Requires-Dist: sentence-transformers (>=4.0,<6)
23
24
  Requires-Dist: tree-sitter (>=0.22,<1)
24
25
  Requires-Dist: tree-sitter-python (>=0.23)
26
+ Requires-Dist: wiki-studio (>=0.3.1,<1) ; extra == "wiki"
25
27
  Project-URL: Documentation, https://memkernel.dev
26
28
  Project-URL: Homepage, https://memkernel.dev
27
29
  Project-URL: Repository, https://github.com/gosha70/memkernel
@@ -47,6 +49,24 @@ MemKernel gives Claude Code (and any MCP-compatible copilot) persistent memory a
47
49
 
48
50
  **Tiered memory (0.2)**: memories live in three tiers with different persistence rules — *working* (the current task, hours), *short-term* (episodes and observations, decays unless recalled) and *long-term* (decisions, conventions, facts, procedures, code; never decays, replaced by supersession). Recall ranks by relevance, recency and importance; `consolidate` and `promote` move what proved useful into long-term memory; an LLM-wiki bridge keeps a human-readable, git-versioned copy of long-term knowledge. Design: [`docs/architecture/memory-tiers.md`](docs/architecture/memory-tiers.md).
49
51
 
52
+ ## What MemKernel is, and what it is not
53
+
54
+ **It is:**
55
+
56
+ - **Persistent memory for a coding agent.** What the agent retains survives context compaction and the end of a session, in a local ChromaDB store per project.
57
+ - **Explicit.** The agent decides what to retain (decisions, conventions, facts, code snapshots, tasks) and what to recall. The lifecycle hooks only automate the protocol around that: checkpoint before compaction, recovery after it, consolidation and revalidation at session start.
58
+ - **Code-aware.** AST chunking, hybrid search that finds identifiers as well as meaning, and revalidation of `code` memories against the files they came from.
59
+ - **No mandatory generative LLM.** Default retrieval uses a local sentence-transformer embedding model plus BM25 and RRF; nothing generates text inside MemKernel. `ask` with the built-in reasoner returns the best-matching chunks; synthesis is the agent's job, or an optional external reasoner's.
60
+
61
+ **It is not:**
62
+
63
+ - **A token optimizer or context compressor.** MemKernel does not shrink the context window, compress prompts, or lower model cost by itself. It adds tool calls and returned content to a session. Any token saving comes from the agent re-reading fewer files or documents after a resume; that is a side effect to measure, not a promise.
64
+ - **A replacement for files, specs and git.** Progress notes, task lists and review records already carry state between sessions. MemKernel adds retrieval by meaning and identifier, tiers with decay, supersession and revalidation on top. When a file already holds everything the next session needs, expect no gain.
65
+ - **An index of the whole repository.** `code` memories are the snapshots the agent chose to retain, not an automatic scan of every file.
66
+ - **A cross-project knowledge base.** One store per project id. Sharing is deliberate: a second server with a shared id, or the wiki bridge.
67
+
68
+ **Evidence.** `eval/run_eval.py` measures retrieval (recall@k, precision@k, MRR). The 0.5 lifecycle evaluation, a **maintainer-run supplemental evaluation** (memkernel's own runner, preregistered and announced in [#17](https://github.com/gosha70/memkernel/issues/17) before the run), ran the same agent in seven arms on 20 tasks: [report](eval/lifecycle-suite/report-0.5.0.md). **No gain is claimed.** Against files alone (`claude_md`), memkernel's memory store was ahead on 6 tasks, behind on none and tied on 14 (Holm-adjusted p 0.0625: "no detectable difference"; with so many ties this task set could not have reached significance); against earlier transcripts (`fs_grep`) it was equivalent. Two of the four task groups hit a ceiling, every arm passing every trial. The report's disclosures list its deviations and known defects. Results are published per minor release, including the cases with no gain.
69
+
50
70
  ---
51
71
 
52
72
  ## Requirements
@@ -66,6 +86,9 @@ MemKernel is a Python package on PyPI. Its `memkernel` command is the MCP server
66
86
  pipx install memkernel
67
87
  # if pipx's default interpreter is outside 3.11–3.13:
68
88
  pipx install --python python3.11 memkernel
89
+ # optional: wiki-studio lint and health findings on wiki_export (ADR-010)
90
+ pipx install "memkernel[wiki]"
91
+ pipx inject memkernel "wiki-studio>=0.3.1,<1" # add it to an existing install
69
92
  ```
70
93
 
71
94
  **As a library inside a project**, install it into that project's virtual environment:
@@ -221,6 +244,8 @@ ask(ref_id="<document ref_id>", question="Which endpoints need rate limiting?",
221
244
  ## Configuration
222
245
 
223
246
  All settings are read from environment variables with the `MEMKERNEL_` prefix, or from a `.env` file.
247
+ Entries in `.env` without the prefix (keys for other tools) are ignored; a `MEMKERNEL_` key that matches no
248
+ setting is logged as a warning at startup.
224
249
 
225
250
  | Variable | Default | Description |
226
251
  |----------|---------|-------------|
@@ -252,6 +277,7 @@ All settings are read from environment variables with the `MEMKERNEL_` prefix, o
252
277
  | `MEMKERNEL_RECALL_WEIGHT_RECENCY` | `0.3` | Weight of recency in the final recall score |
253
278
  | `MEMKERNEL_RECALL_WEIGHT_IMPORTANCE` | `0.3` | Weight of importance in the final recall score |
254
279
  | `MEMKERNEL_WIKI_DIR` | `` | Directory of the LLM wiki used by `wiki_export` / `wiki_import` (empty = pass `wiki_dir` explicitly) |
280
+ | `MEMKERNEL_WIKI_PROPOSALS_DIR` | `""` | Where wiki-studio patch-sets are written before promote (`memkernel[wiki]`); empty means `<project root>/.memkernel/wiki-proposals`. Setting it requests the gate: without wiki-studio installed, a warning is logged. |
255
281
  | `MEMKERNEL_TRANSPORT` | `stdio` | `stdio` or `streamable-http` |
256
282
  | `MEMKERNEL_HOST` | `127.0.0.1` | Host for `streamable-http` transport |
257
283
  | `MEMKERNEL_PORT` | `8765` | Port for `streamable-http` transport |
@@ -331,7 +357,11 @@ Long-term memory has a human-readable twin: a markdown wiki in the [LLM Wiki](ht
331
357
  - `wiki_export` compiles every long-term memory that has no page yet into a **draft** page under `decisions/`, `concepts/` or `workflows/`, with the required sections and a `memkernel://<project>/<ref_id>` citation. Existing pages are never overwritten.
332
358
  - `wiki_import` indexes curated pages as long-term memories so `recall` finds the reviewed version; a changed page supersedes its earlier memory.
333
359
 
334
- Set `MEMKERNEL_WIKI_DIR` (for example `knowledge/wiki`) or pass `wiki_dir` to the tools. Rationale and the proposed standalone `ai-wiki` repo: [`docs/adr/ADR-008-llm-wiki-as-semantic-layer.md`](docs/adr/ADR-008-llm-wiki-as-semantic-layer.md).
360
+ Set `MEMKERNEL_WIKI_DIR` (for example `knowledge/wiki`) or pass `wiki_dir` to the tools. The standalone maintainer that owns this page schema is [wiki-studio](https://github.com/gosha70/wiki-studio). With the `memkernel[wiki]` extra installed, every `wiki_export` response carries wiki-studio's lint and health findings for the whole wiki after the export, curated pages included, under `lint` (`violations`, `audit_errors`, `pages_checked`, `health` items with `kind`, `severity`, `pages`, `description`); without it `lint` is `null` and `lint_hint` says how to enable it; if the lint pass itself fails, `lint` is `null` and `lint_error` says why. The files written are the same in every case.
361
+
362
+ With wiki-studio 0.3.1 or later installed, `wiki_export` writes through wiki-studio's promote gate: it first writes `index.md` and `log.md` if they are missing (the gate cannot create them), then proposes every new page with its index and log lines as one patch-set and promotes it. A rejected export writes nothing and the response carries `gate_error`; so does an installed wiki-studio that fails to import or to build the patch-set, rather than falling back to the direct writer. A failed export leaves its patch-set under the proposals directory for inspection; delete it once the cause is fixed. Patch-sets go to `MEMKERNEL_WIKI_PROPOSALS_DIR`, by default `.memkernel/wiki-proposals/` under the project root. Commit `<wiki>/.audit/`, wiki-studio's audit trail of promoted proposals; ignore `.memkernel/wiki-proposals/`.
363
+
364
+ Rationale: [`docs/adr/ADR-008-llm-wiki-as-semantic-layer.md`](docs/adr/ADR-008-llm-wiki-as-semantic-layer.md) and [`docs/adr/ADR-010-wiki-studio-as-wiki-maintainer.md`](docs/adr/ADR-010-wiki-studio-as-wiki-maintainer.md).
335
365
 
336
366
  ### Revalidation and store health
337
367
 
@@ -411,7 +441,7 @@ MemKernel is the memory component of a small family of projects by the same auth
411
441
 
412
442
  | Project | Role | Relationship |
413
443
  |---|---|---|
414
- | [ai-wiki](https://github.com/gosha70/ai-wiki) | LLM-maintained markdown wiki (ingest / promote / query / lint) | the human-readable form of long-term memory; `wiki_export` / `wiki_import` speak its page schema (ADR-008) |
444
+ | [wiki-studio](https://github.com/gosha70/wiki-studio) | LLM-wiki maintainer (ingest / promote / query / lint / audit), stdlib-only | the human-readable form of long-term memory; `wiki_export` / `wiki_import` speak its page schema (ADR-008), and CI lints exporter output with it |
415
445
  | [rlm-studio](https://github.com/gosha70/rlm-studio) | Recursive Language Models workbench: Direct / RAG / RLM / auto over large documents | the document layer behind `document` memories and the `ask()` tool (`MEMKERNEL_REASONER=rlmstudio`, ADR-009) |
416
446
  | [code-copilot-team](https://github.com/gosha70/code-copilot-team) | Enforceable harness configuration for Claude Code, Pi and other copilots | consumes MemKernel through MCP and hooks |
417
447
  | [haa](https://github.com/gosha70/haa) | Home-built AI coding harness, future component assembler | consumes MemKernel in-process through `build_service()` |
@@ -18,6 +18,24 @@ MemKernel gives Claude Code (and any MCP-compatible copilot) persistent memory a
18
18
 
19
19
  **Tiered memory (0.2)**: memories live in three tiers with different persistence rules — *working* (the current task, hours), *short-term* (episodes and observations, decays unless recalled) and *long-term* (decisions, conventions, facts, procedures, code; never decays, replaced by supersession). Recall ranks by relevance, recency and importance; `consolidate` and `promote` move what proved useful into long-term memory; an LLM-wiki bridge keeps a human-readable, git-versioned copy of long-term knowledge. Design: [`docs/architecture/memory-tiers.md`](docs/architecture/memory-tiers.md).
20
20
 
21
+ ## What MemKernel is, and what it is not
22
+
23
+ **It is:**
24
+
25
+ - **Persistent memory for a coding agent.** What the agent retains survives context compaction and the end of a session, in a local ChromaDB store per project.
26
+ - **Explicit.** The agent decides what to retain (decisions, conventions, facts, code snapshots, tasks) and what to recall. The lifecycle hooks only automate the protocol around that: checkpoint before compaction, recovery after it, consolidation and revalidation at session start.
27
+ - **Code-aware.** AST chunking, hybrid search that finds identifiers as well as meaning, and revalidation of `code` memories against the files they came from.
28
+ - **No mandatory generative LLM.** Default retrieval uses a local sentence-transformer embedding model plus BM25 and RRF; nothing generates text inside MemKernel. `ask` with the built-in reasoner returns the best-matching chunks; synthesis is the agent's job, or an optional external reasoner's.
29
+
30
+ **It is not:**
31
+
32
+ - **A token optimizer or context compressor.** MemKernel does not shrink the context window, compress prompts, or lower model cost by itself. It adds tool calls and returned content to a session. Any token saving comes from the agent re-reading fewer files or documents after a resume; that is a side effect to measure, not a promise.
33
+ - **A replacement for files, specs and git.** Progress notes, task lists and review records already carry state between sessions. MemKernel adds retrieval by meaning and identifier, tiers with decay, supersession and revalidation on top. When a file already holds everything the next session needs, expect no gain.
34
+ - **An index of the whole repository.** `code` memories are the snapshots the agent chose to retain, not an automatic scan of every file.
35
+ - **A cross-project knowledge base.** One store per project id. Sharing is deliberate: a second server with a shared id, or the wiki bridge.
36
+
37
+ **Evidence.** `eval/run_eval.py` measures retrieval (recall@k, precision@k, MRR). The 0.5 lifecycle evaluation, a **maintainer-run supplemental evaluation** (memkernel's own runner, preregistered and announced in [#17](https://github.com/gosha70/memkernel/issues/17) before the run), ran the same agent in seven arms on 20 tasks: [report](eval/lifecycle-suite/report-0.5.0.md). **No gain is claimed.** Against files alone (`claude_md`), memkernel's memory store was ahead on 6 tasks, behind on none and tied on 14 (Holm-adjusted p 0.0625: "no detectable difference"; with so many ties this task set could not have reached significance); against earlier transcripts (`fs_grep`) it was equivalent. Two of the four task groups hit a ceiling, every arm passing every trial. The report's disclosures list its deviations and known defects. Results are published per minor release, including the cases with no gain.
38
+
21
39
  ---
22
40
 
23
41
  ## Requirements
@@ -37,6 +55,9 @@ MemKernel is a Python package on PyPI. Its `memkernel` command is the MCP server
37
55
  pipx install memkernel
38
56
  # if pipx's default interpreter is outside 3.11–3.13:
39
57
  pipx install --python python3.11 memkernel
58
+ # optional: wiki-studio lint and health findings on wiki_export (ADR-010)
59
+ pipx install "memkernel[wiki]"
60
+ pipx inject memkernel "wiki-studio>=0.3.1,<1" # add it to an existing install
40
61
  ```
41
62
 
42
63
  **As a library inside a project**, install it into that project's virtual environment:
@@ -192,6 +213,8 @@ ask(ref_id="<document ref_id>", question="Which endpoints need rate limiting?",
192
213
  ## Configuration
193
214
 
194
215
  All settings are read from environment variables with the `MEMKERNEL_` prefix, or from a `.env` file.
216
+ Entries in `.env` without the prefix (keys for other tools) are ignored; a `MEMKERNEL_` key that matches no
217
+ setting is logged as a warning at startup.
195
218
 
196
219
  | Variable | Default | Description |
197
220
  |----------|---------|-------------|
@@ -223,6 +246,7 @@ All settings are read from environment variables with the `MEMKERNEL_` prefix, o
223
246
  | `MEMKERNEL_RECALL_WEIGHT_RECENCY` | `0.3` | Weight of recency in the final recall score |
224
247
  | `MEMKERNEL_RECALL_WEIGHT_IMPORTANCE` | `0.3` | Weight of importance in the final recall score |
225
248
  | `MEMKERNEL_WIKI_DIR` | `` | Directory of the LLM wiki used by `wiki_export` / `wiki_import` (empty = pass `wiki_dir` explicitly) |
249
+ | `MEMKERNEL_WIKI_PROPOSALS_DIR` | `""` | Where wiki-studio patch-sets are written before promote (`memkernel[wiki]`); empty means `<project root>/.memkernel/wiki-proposals`. Setting it requests the gate: without wiki-studio installed, a warning is logged. |
226
250
  | `MEMKERNEL_TRANSPORT` | `stdio` | `stdio` or `streamable-http` |
227
251
  | `MEMKERNEL_HOST` | `127.0.0.1` | Host for `streamable-http` transport |
228
252
  | `MEMKERNEL_PORT` | `8765` | Port for `streamable-http` transport |
@@ -302,7 +326,11 @@ Long-term memory has a human-readable twin: a markdown wiki in the [LLM Wiki](ht
302
326
  - `wiki_export` compiles every long-term memory that has no page yet into a **draft** page under `decisions/`, `concepts/` or `workflows/`, with the required sections and a `memkernel://<project>/<ref_id>` citation. Existing pages are never overwritten.
303
327
  - `wiki_import` indexes curated pages as long-term memories so `recall` finds the reviewed version; a changed page supersedes its earlier memory.
304
328
 
305
- Set `MEMKERNEL_WIKI_DIR` (for example `knowledge/wiki`) or pass `wiki_dir` to the tools. Rationale and the proposed standalone `ai-wiki` repo: [`docs/adr/ADR-008-llm-wiki-as-semantic-layer.md`](docs/adr/ADR-008-llm-wiki-as-semantic-layer.md).
329
+ Set `MEMKERNEL_WIKI_DIR` (for example `knowledge/wiki`) or pass `wiki_dir` to the tools. The standalone maintainer that owns this page schema is [wiki-studio](https://github.com/gosha70/wiki-studio). With the `memkernel[wiki]` extra installed, every `wiki_export` response carries wiki-studio's lint and health findings for the whole wiki after the export, curated pages included, under `lint` (`violations`, `audit_errors`, `pages_checked`, `health` items with `kind`, `severity`, `pages`, `description`); without it `lint` is `null` and `lint_hint` says how to enable it; if the lint pass itself fails, `lint` is `null` and `lint_error` says why. The files written are the same in every case.
330
+
331
+ With wiki-studio 0.3.1 or later installed, `wiki_export` writes through wiki-studio's promote gate: it first writes `index.md` and `log.md` if they are missing (the gate cannot create them), then proposes every new page with its index and log lines as one patch-set and promotes it. A rejected export writes nothing and the response carries `gate_error`; so does an installed wiki-studio that fails to import or to build the patch-set, rather than falling back to the direct writer. A failed export leaves its patch-set under the proposals directory for inspection; delete it once the cause is fixed. Patch-sets go to `MEMKERNEL_WIKI_PROPOSALS_DIR`, by default `.memkernel/wiki-proposals/` under the project root. Commit `<wiki>/.audit/`, wiki-studio's audit trail of promoted proposals; ignore `.memkernel/wiki-proposals/`.
332
+
333
+ Rationale: [`docs/adr/ADR-008-llm-wiki-as-semantic-layer.md`](docs/adr/ADR-008-llm-wiki-as-semantic-layer.md) and [`docs/adr/ADR-010-wiki-studio-as-wiki-maintainer.md`](docs/adr/ADR-010-wiki-studio-as-wiki-maintainer.md).
306
334
 
307
335
  ### Revalidation and store health
308
336
 
@@ -382,7 +410,7 @@ MemKernel is the memory component of a small family of projects by the same auth
382
410
 
383
411
  | Project | Role | Relationship |
384
412
  |---|---|---|
385
- | [ai-wiki](https://github.com/gosha70/ai-wiki) | LLM-maintained markdown wiki (ingest / promote / query / lint) | the human-readable form of long-term memory; `wiki_export` / `wiki_import` speak its page schema (ADR-008) |
413
+ | [wiki-studio](https://github.com/gosha70/wiki-studio) | LLM-wiki maintainer (ingest / promote / query / lint / audit), stdlib-only | the human-readable form of long-term memory; `wiki_export` / `wiki_import` speak its page schema (ADR-008), and CI lints exporter output with it |
386
414
  | [rlm-studio](https://github.com/gosha70/rlm-studio) | Recursive Language Models workbench: Direct / RAG / RLM / auto over large documents | the document layer behind `document` memories and the `ask()` tool (`MEMKERNEL_REASONER=rlmstudio`, ADR-009) |
387
415
  | [code-copilot-team](https://github.com/gosha70/code-copilot-team) | Enforceable harness configuration for Claude Code, Pi and other copilots | consumes MemKernel through MCP and hooks |
388
416
  | [haa](https://github.com/gosha70/haa) | Home-built AI coding harness, future component assembler | consumes MemKernel in-process through `build_service()` |
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "memkernel"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "Code-aware memory server for AI coding copilots"
5
5
  authors = ["EGOGE"]
6
6
  license = "Apache-2.0"
@@ -31,6 +31,11 @@ tree-sitter-python = ">=0.23"
31
31
  pydantic = ">=2.0,<3"
32
32
  pydantic-settings = ">=2.0,<3"
33
33
  fastapi = ">=0.115,<1"
34
+ # Optional integrations (ADR-010): installed only through an extra; imported lazily.
35
+ wiki-studio = { version = ">=0.3.1,<1", optional = true }
36
+
37
+ [tool.poetry.extras]
38
+ wiki = ["wiki-studio"]
34
39
 
35
40
  [tool.poetry.group.dev.dependencies]
36
41
  pytest = ">=8"
@@ -40,6 +45,7 @@ ruff = ">=0.9"
40
45
  mypy = ">=1.13"
41
46
  httpx = ">=0.27"
42
47
  pip-audit = "^2.10.0"
48
+ wiki-studio = ">=0.3.1,<1"
43
49
 
44
50
  [build-system]
45
51
  requires = ["poetry-core"]
@@ -2,4 +2,4 @@
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  """MemKernel — Code-aware memory server for AI coding copilots."""
4
4
 
5
- __version__ = "0.4.0"
5
+ __version__ = "0.5.0"
@@ -0,0 +1,132 @@
1
+ # Copyright (c) 2026 EGOGE
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """MemKernel configuration via environment variables."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from typing import TYPE_CHECKING, Any, Literal
9
+
10
+ from pydantic_settings import BaseSettings, EnvSettingsSource, PydanticBaseSettingsSource, SettingsConfigDict
11
+
12
+ if TYPE_CHECKING:
13
+ from pydantic.fields import FieldInfo
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ ENV_PREFIX = "MEMKERNEL_"
18
+ ENV_FILE = ".env"
19
+
20
+
21
+ class MemKernelSettings(BaseSettings):
22
+ """All settings are configurable via MEMKERNEL_ prefixed env vars."""
23
+
24
+ # Unknown keys stay forbidden (constructor typos raise), but the .env source
25
+ # is filtered to known settings: a project .env usually holds keys for other
26
+ # tools and must not stop the server from starting (issue #15). Misspelled
27
+ # MEMKERNEL_ keys are reported by settings_customise_sources().
28
+ model_config = SettingsConfigDict(env_prefix=ENV_PREFIX, env_file=ENV_FILE)
29
+
30
+ project_id: str = "default"
31
+ default_session_id: str = "default"
32
+
33
+ # ChromaDB
34
+ chroma_persist_path: str = ".memkernel/chroma"
35
+ chroma_collection_prefix: str = "memkernel"
36
+
37
+ # Embedding
38
+ embedding_model: str = "all-MiniLM-L6-v2"
39
+ embedding_dimension: int = 384
40
+
41
+ # Retrieval
42
+ search_top_k: int = 5
43
+ rrf_k: int = 60
44
+ preview_length: int = 200
45
+
46
+ # Chunking
47
+ chunk_size: int = 1500 # max chars per chunk
48
+ chunk_overlap: int = 200 # char overlap between token chunks (fallback only)
49
+
50
+ # Documents and ask() (ADR-009)
51
+ document_store: Literal["local"] = "local" # where `document` bodies live
52
+ document_summary_chars: int = 1000 # characters of the body kept in the document's summary record
53
+ reasoner: Literal["local", "rlmstudio"] = "local" # ReasonerPort behind ask(); falls back to local
54
+ ask_top_k: int = 5 # chunks the local reasoner returns per ask()
55
+
56
+ # Sources, revalidation and store health (0.4)
57
+ project_root: str = "." # relative `source` paths resolve under this directory; nothing outside it is read
58
+ health_duplicate_threshold: float = (
59
+ 0.95 # cosine similarity in [0, 1] from which two long-term memories are near-duplicates
60
+ )
61
+ health_duplicate_limit: int = 500 # most recent long-term memories checked for near-duplicates
62
+ health_report_limit: int = 20 # items listed per health category
63
+
64
+ # Lifecycle / decay (see docs/architecture/memory-tiers.md)
65
+ episode_ttl_days: int = 30 # base TTL for short-term `episode` memories
66
+ observation_ttl_days: int = 7 # base TTL for short-term `observation` memories
67
+ task_ttl_hours: int = 24 # base TTL for working-memory `task` records
68
+ access_boost: float = 0.5 # each recall extends TTL by this fraction of the base TTL
69
+ recency_half_life_days: float = 7.0 # recency decay half-life for short-term memories
70
+
71
+ # Recall scoring: final = w_relevance * fused + w_recency * recency + w_importance * importance
72
+ recall_weight_relevance: float = 1.0
73
+ recall_weight_recency: float = 0.3
74
+ recall_weight_importance: float = 0.3
75
+
76
+ # LLM wiki bridge: directory holding the markdown wiki (index.md, decisions/, ...).
77
+ # Empty = no wiki configured; wiki_export/wiki_import then require an explicit path.
78
+ wiki_dir: str = ""
79
+ # Where wiki-studio patch-sets are written before promote (memkernel[wiki], ADR-010).
80
+ # Empty = <project_root>/.memkernel/wiki-proposals; set it to request the gate explicitly.
81
+ wiki_proposals_dir: str = ""
82
+
83
+ # Server
84
+ transport: Literal["stdio", "streamable-http"] = "stdio"
85
+ host: str = "127.0.0.1"
86
+ port: int = 8765
87
+
88
+ @classmethod
89
+ def settings_customise_sources(
90
+ cls,
91
+ settings_cls: type[BaseSettings],
92
+ init_settings: PydanticBaseSettingsSource,
93
+ env_settings: PydanticBaseSettingsSource,
94
+ dotenv_settings: PydanticBaseSettingsSource,
95
+ file_secret_settings: PydanticBaseSettingsSource,
96
+ ) -> tuple[PydanticBaseSettingsSource, ...]:
97
+ """Default source order with the .env source limited to known settings.
98
+
99
+ Warns about MEMKERNEL_ keys in the environment or in .env that match no setting.
100
+ """
101
+ for source, origin in ((env_settings, "the environment"), (dotenv_settings, ENV_FILE)):
102
+ for key in unknown_prefixed_keys(settings_cls, source):
103
+ logger.warning(
104
+ "memkernel: unknown setting %s in %s is ignored; check the spelling against the README", key, origin
105
+ )
106
+ return init_settings, env_settings, KnownFieldsOnly(dotenv_settings), file_secret_settings
107
+
108
+
109
+ class KnownFieldsOnly(PydanticBaseSettingsSource):
110
+ """Pass through only the values of ``inner`` that belong to a settings field."""
111
+
112
+ def __init__(self, inner: PydanticBaseSettingsSource) -> None:
113
+ super().__init__(inner.settings_cls)
114
+ self._inner = inner
115
+
116
+ def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool]:
117
+ return self._inner.get_field_value(field, field_name)
118
+
119
+ def __call__(self) -> dict[str, Any]:
120
+ return {key: value for key, value in self._inner().items() if key in self.settings_cls.model_fields}
121
+
122
+
123
+ def unknown_prefixed_keys(settings_cls: type[BaseSettings], source: PydanticBaseSettingsSource) -> list[str]:
124
+ """Return the MEMKERNEL_ keys ``source`` holds that are not fields of ``settings_cls``, upper-cased."""
125
+ if not isinstance(source, EnvSettingsSource):
126
+ return []
127
+ prefix = ENV_PREFIX.lower() # case-insensitive sources expose lower-cased keys
128
+ return sorted(
129
+ key.upper()
130
+ for key in source.env_vars
131
+ if key.lower().startswith(prefix) and key.lower()[len(prefix) :] not in settings_cls.model_fields
132
+ )
@@ -11,6 +11,7 @@ from typing import Any
11
11
  from mcp.server.fastmcp import FastMCP # noqa: TC002 — runtime import required by FastMCP introspection
12
12
  from mcp.server.fastmcp.server import Context # noqa: TC002
13
13
 
14
+ from memkernel.wiki.studio import INSTALL_HINT
14
15
  from memkernel.wiki.sync import WikiExporter, WikiImporter
15
16
 
16
17
 
@@ -38,11 +39,35 @@ def register_wiki_tools(server: FastMCP[Any]) -> None:
38
39
  """Compile long-term memories (decision, convention, fact, procedure) that have no wiki
39
40
  page yet into draft markdown pages under wiki_dir (default MEMKERNEL_WIKI_DIR), and
40
41
  append them to index.md and log.md. Review and curate the drafts before marking stable.
42
+ With the memkernel[wiki] extra installed, "lint" carries wiki-studio's violations, audit
43
+ errors and health findings for the whole wiki after the export (curated pages included);
44
+ without the extra it is null and "lint_hint" says how to enable it; if the lint pass itself
45
+ failed it is null and "lint_error" says why. With wiki-studio >= 0.3 the pages go through its
46
+ promote gate (index.md and log.md are scaffolded first); a rejected export, or an installed but
47
+ broken wiki-studio, writes nothing and reports "gate_error". The pages, index.md and log.md
48
+ written are the same either way.
41
49
  """
42
50
  app_ctx = _context(ctx)
43
- exporter = WikiExporter(app_ctx.memory_service, _resolve_wiki_dir(wiki_dir, app_ctx.settings))
51
+ proposals = app_ctx.settings.wiki_proposals_dir
52
+ exporter = WikiExporter(
53
+ app_ctx.memory_service,
54
+ _resolve_wiki_dir(wiki_dir, app_ctx.settings),
55
+ project_root=Path(app_ctx.settings.project_root),
56
+ proposals_dir=Path(proposals) if proposals else None,
57
+ )
44
58
  report = exporter.export()
45
- return json.dumps({"written": report.written, "skipped": report.skipped})
59
+ payload: dict[str, Any] = {"written": report.written, "skipped": report.skipped}
60
+ if report.gate_error is not None:
61
+ payload["gate_error"] = report.gate_error
62
+ if report.lint is not None:
63
+ payload["lint"] = report.lint.model_dump(mode="json")
64
+ elif report.lint_error is not None:
65
+ payload["lint"] = None
66
+ payload["lint_error"] = report.lint_error
67
+ else:
68
+ payload["lint"] = None
69
+ payload["lint_hint"] = INSTALL_HINT
70
+ return json.dumps(payload)
46
71
 
47
72
  @server.tool()
48
73
  async def wiki_import(
@@ -0,0 +1,193 @@
1
+ # Copyright (c) 2026 EGOGE
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Optional wiki-studio integration (the ``memkernel[wiki]`` extra, ADR-010).
4
+
5
+ wiki-studio owns the wiki page schema and its lint and health pass. This
6
+ module is the only place memkernel imports it, lazily: without the extra
7
+ every function here returns ``None`` and logs nothing, because a missing
8
+ optional extra is a normal state. A warning is logged only when the caller
9
+ asked for wiki-studio explicitly (``require_studio=True``). Nothing here ever calls
10
+ wiki-studio's ``ingest``, ``query`` or a copilot-CLI backend: those shell out
11
+ to a copilot CLI, and memkernel runs inside one.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ from dataclasses import dataclass
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ from pydantic import BaseModel, Field
21
+
22
+ if TYPE_CHECKING:
23
+ from pathlib import Path
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ INSTALL_HINT = "install memkernel[wiki] for lint and health"
28
+ GATE_BACKEND = "memkernel"
29
+ GATE_RATIONALE = "memkernel export"
30
+
31
+
32
+ class HealthItem(BaseModel):
33
+ """One knowledge-health finding, field for field as wiki-studio's ``HealthFinding``."""
34
+
35
+ kind: str
36
+ severity: str
37
+ pages: list[str]
38
+ description: str
39
+
40
+
41
+ class LintSummary(BaseModel):
42
+ """wiki-studio's structural lint, audit-format and health results for one wiki directory."""
43
+
44
+ violations: list[str] = Field(default_factory=list)
45
+ audit_errors: list[str] = Field(default_factory=list)
46
+ pages_checked: int = 0
47
+ health: list[HealthItem] | None = None
48
+
49
+
50
+ class LintError(RuntimeError):
51
+ """wiki-studio is installed but its lint pass failed; the export itself is unaffected."""
52
+
53
+
54
+ def load_studio(*, require_studio: bool = False) -> Any | None:
55
+ """Return the ``wiki_studio.api`` module, or ``None`` when the extra is not installed.
56
+
57
+ Only a missing ``wiki_studio`` package counts as absence. Any other import
58
+ failure means the package is present but broken or incompatible, which is
59
+ raised as :class:`LintError` so it is reported rather than mistaken for the
60
+ extra being absent.
61
+ """
62
+ try:
63
+ import wiki_studio.api as api
64
+ except ModuleNotFoundError as exc:
65
+ if exc.name == "wiki_studio":
66
+ if require_studio:
67
+ logger.warning("wiki-studio is not installed; %s", INSTALL_HINT)
68
+ return None
69
+ raise LintError(f"wiki-studio is installed but broken: {exc}") from exc
70
+ except Exception as exc: # ImportError or any error raised while the package initialises
71
+ raise LintError(f"wiki-studio is installed but broken: {exc}") from exc
72
+ return api
73
+
74
+
75
+ def lint_wiki(wiki_dir: Path, project_root: Path, *, require_studio: bool = False) -> LintSummary | None:
76
+ """Run wiki-studio's lint with the health checks over the whole wiki in ``wiki_dir``.
77
+
78
+ Returns ``None`` without the extra. Raises :class:`LintError` for every failure
79
+ of an installed wiki-studio: a broken or incompatible package, a page that is
80
+ not valid UTF-8, a malformed audit tree, a result whose shape memkernel does
81
+ not recognise. The caller decides what the failure means for its own result.
82
+ ``project_root`` is the root ``path:`` sources resolve against (memkernel's
83
+ ``MEMKERNEL_PROJECT_ROOT``). No backend is passed, so the LLM-dependent
84
+ contradiction check is skipped.
85
+ """
86
+ try:
87
+ api = load_studio(require_studio=require_studio)
88
+ if api is None:
89
+ return None
90
+ from wiki_studio.paths import WikiPaths
91
+
92
+ result = api.lint(WikiPaths.for_root(project_root, wiki_dir=wiki_dir), health=True)
93
+ health = (
94
+ [
95
+ HealthItem(
96
+ kind=str(finding.kind),
97
+ severity=str(finding.severity),
98
+ pages=list(finding.pages),
99
+ description=finding.description,
100
+ )
101
+ for finding in result.health.findings
102
+ ]
103
+ if result.health is not None
104
+ else None
105
+ )
106
+ return LintSummary(
107
+ violations=list(result.violations),
108
+ audit_errors=list(result.audit_errors),
109
+ pages_checked=result.pages_checked,
110
+ health=health,
111
+ )
112
+ except LintError as exc: # broken installation, already wrapped by load_studio
113
+ logger.warning("wiki-studio lint failed for %s: %s", wiki_dir, exc)
114
+ raise
115
+ except Exception as exc: # third-party failure of an installed package: wrapped, never silent
116
+ logger.warning("wiki-studio lint failed for %s: %s", wiki_dir, exc)
117
+ raise LintError(f"wiki-studio lint failed: {exc}") from exc
118
+
119
+
120
+ class GateError(RuntimeError):
121
+ """wiki-studio's gate rejected or failed to apply an export; the pages were not written."""
122
+
123
+
124
+ @dataclass(frozen=True)
125
+ class GatedPage:
126
+ """One page of an export as the gate needs it: the page itself plus its index and log lines."""
127
+
128
+ path: str # wiki-relative, e.g. "decisions/use-arq.md"
129
+ content: str
130
+ index_line: str | None # one line of the export's single append-index edit; None = no entry
131
+ log_line: str | None # one line of the export's single append-log edit; None = no entry
132
+
133
+
134
+ def gate_available() -> Any | None:
135
+ """Return ``wiki_studio.api`` when it offers the proposal gate (wiki-studio >= 0.3), else ``None``.
136
+
137
+ ``None`` means absent or an older wiki-studio without the gate, where the
138
+ direct writer is the supported fallback. A present but broken package
139
+ raises :class:`GateError`: the validator is installed, so its failure must
140
+ not be mistaken for its absence and let pages through unchecked.
141
+ """
142
+ try:
143
+ api = load_studio()
144
+ except LintError as exc:
145
+ raise GateError(str(exc)) from exc
146
+ if api is None or not hasattr(api, "propose") or not hasattr(api, "PageEdit"):
147
+ return None
148
+ return api
149
+
150
+
151
+ def propose_and_promote(
152
+ api: Any, pages: list[GatedPage], wiki_dir: Path, project_root: Path, proposals_dir: Path, name: str
153
+ ) -> None:
154
+ """Write ``pages`` as one patch-set and apply it through wiki-studio's promote gate.
155
+
156
+ ``index.md`` and ``log.md`` must already exist: the gate appends to them but
157
+ refuses to create them. Raises :class:`GateError` on any rejection or
158
+ failure; wiki-studio validates before writing, so a rejected patch-set
159
+ leaves the wiki as it was.
160
+ """
161
+ # One append per file: wiki-studio 0.3.1 keeps only the last of several appends to the same file in a
162
+ # patch-set (fixed in 0.4.0, wiki-studio#18; kept while the floor is 0.3.1), so every index line goes into a
163
+ # single append-index edit and every log line into one append-log.
164
+ index_lines = [page.index_line for page in pages if page.index_line is not None]
165
+ log_lines = [page.log_line for page in pages if page.log_line is not None]
166
+ try: # every wiki-studio call is inside the boundary, setup included, so the caller's cleanup always runs
167
+ from wiki_studio.paths import WikiPaths
168
+
169
+ edits = [
170
+ api.PageEdit(path=page.path, action="create", new_content=page.content, rationale=GATE_RATIONALE)
171
+ for page in pages
172
+ ]
173
+ if index_lines:
174
+ edits.append(
175
+ api.PageEdit(
176
+ path="index.md",
177
+ action="append-index",
178
+ new_content="\n".join(index_lines),
179
+ rationale=GATE_RATIONALE,
180
+ )
181
+ )
182
+ if log_lines:
183
+ edits.append(
184
+ api.PageEdit(
185
+ path="log.md", action="append-log", new_content="\n".join(log_lines), rationale=GATE_RATIONALE
186
+ )
187
+ )
188
+ paths = WikiPaths.for_root(project_root, wiki_dir=wiki_dir, proposals_dir=proposals_dir)
189
+ proposal = api.propose(edits, paths, name=name, source="memkernel wiki_export", backend=GATE_BACKEND)
190
+ api.promote(proposal, paths)
191
+ except Exception as exc: # any rejection or failure of the installed gate is reported, never raised raw
192
+ logger.warning("wiki-studio gate rejected the export to %s: %s", wiki_dir, exc)
193
+ raise GateError(f"wiki-studio gate rejected the export: {exc}") from exc