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.
- {memkernel-0.4.0 → memkernel-0.5.0}/PKG-INFO +33 -3
- {memkernel-0.4.0 → memkernel-0.5.0}/README.md +30 -2
- {memkernel-0.4.0 → memkernel-0.5.0}/pyproject.toml +7 -1
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/__init__.py +1 -1
- memkernel-0.5.0/src/memkernel/config.py +132 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/mcp/tools/wiki.py +27 -2
- memkernel-0.5.0/src/memkernel/wiki/studio.py +193 -0
- memkernel-0.5.0/src/memkernel/wiki/sync.py +425 -0
- memkernel-0.4.0/src/memkernel/config.py +0 -70
- memkernel-0.4.0/src/memkernel/wiki/sync.py +0 -228
- {memkernel-0.4.0 → memkernel-0.5.0}/LICENSE +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/NOTICE +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/adapters/__init__.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/adapters/rlmstudio.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/api/__init__.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/api/app.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/api/routes/__init__.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/api/routes/admin.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/core/__init__.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/core/models.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/core/ports.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/core/services.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/factory.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/lifecycle.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/mcp/__init__.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/mcp/server.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/mcp/tools/__init__.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/mcp/tools/memory.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/memory/__init__.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/memory/dedup.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/memory/lifecycle.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/memory/types.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/py.typed +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/retrieval/__init__.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/retrieval/chunking.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/retrieval/embeddings.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/retrieval/reasoner.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/retrieval/search.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/retrieval/sparse.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/storage/__init__.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/storage/chroma.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/storage/documents.py +0 -0
- {memkernel-0.4.0 → memkernel-0.5.0}/src/memkernel/wiki/__init__.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
| [
|
|
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.
|
|
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
|
-
| [
|
|
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.
|
|
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"]
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|