Skip to content

Commit fea501a

Browse files
cdeustclaude
andcommitted
release: v3.10.1 — wiki_purge tool for cleaning audit-artefact pollution
New MCP tool: wiki_purge ------------------------ Re-evaluates every authored wiki page against the current classifier and deletes the ones that no longer pass the admission gate. Memories stay in the store — only the wiki markdown files are removed. Defaults to dry-run (apply=false) so users can inspect what would be purged before committing. Pass apply=true to actually delete. Complements the classifier fix shipped in be52855: that commit prevents NEW pollution, wiki_purge cleans up EXISTING pollution from before the fix or from more permissive classifier rules. Tool count: 40 → 41. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent be52855 commit fea501a

6 files changed

Lines changed: 184 additions & 7 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
},
77
"metadata": {
88
"description": "Persistent memory and cognitive profiling plugins for Claude Code",
9-
"version": "3.10.0"
9+
"version": "3.10.1"
1010
},
1111
"plugins": [
1212
{
1313
"name": "cortex",
1414
"source": "./",
15-
"description": "Persistent memory and cognitive profiling for Claude Code — thermodynamic memory with heat/decay, intent-aware retrieval, biological plasticity, codebase intelligence, and cognitive profiling. 40 MCP tools with enriched schemas. PostgreSQL + pgvector in CLI mode; automatic SQLite fallback in Cowork/sandboxed mode. Docker image available.",
16-
"version": "3.10.0",
15+
"description": "Persistent memory and cognitive profiling for Claude Code — thermodynamic memory with heat/decay, intent-aware retrieval, biological plasticity, codebase intelligence, and cognitive profiling. 41 MCP tools with enriched schemas. PostgreSQL + pgvector in CLI mode; automatic SQLite fallback in Cowork/sandboxed mode. Curated wiki (ADRs, specs, lessons) with audit-artefact filtering. Docker image available.",
16+
"version": "3.10.1",
1717
"author": {
1818
"name": "Clement Deust",
1919
"email": "admin@ai-architect.tools"

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "cortex",
33
"description": "Persistent memory for Claude Code — remembers across sessions automatically. Install and forget. Scientific retrieval backed by 41 published papers.",
4-
"version": "3.10.0",
4+
"version": "3.10.1",
55
"author": {
66
"name": "Clement Deust",
77
"email": "admin@ai-architect.tools"

mcp_server/handlers/wiki_purge.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
"""Handler: wiki_purge — remove wiki pages that fail the current classifier.
2+
3+
Re-evaluates every authored wiki page against the current classifier rules
4+
and deletes the ones that would no longer be admitted. Memories in the
5+
PostgreSQL/SQLite store are left untouched — only the markdown files in
6+
~/.claude/methodology/wiki/ are removed.
7+
8+
Use this after tightening classifier rules, after a backfill that
9+
polluted the wiki, or whenever the wiki has drifted away from curated
10+
knowledge toward session audit artefacts.
11+
"""
12+
13+
from __future__ import annotations
14+
15+
from pathlib import Path
16+
from typing import Any
17+
18+
from mcp_server.core.wiki_classifier import classify_memory
19+
from mcp_server.infrastructure.config import WIKI_ROOT
20+
from mcp_server.shared.yaml_parser import parse_yaml_frontmatter
21+
22+
# ── Schema ─────────────────────────────────────────────────────────────
23+
24+
schema = {
25+
"description": (
26+
"Re-evaluate every authored wiki page against the current classifier "
27+
"rules and delete the ones that no longer pass the admission gate. "
28+
"Memories remain in the store (still available via recall); only the "
29+
"wiki markdown files are removed. Use this after a backfill that "
30+
"polluted the wiki with session artefacts (file access, URL access, "
31+
"stage reports, code reviews), or after tightening classifier rules. "
32+
"Returns keep/purge counts plus the list of purged relative paths. "
33+
"Always runs a dry-run by default — pass apply=true to actually delete."
34+
),
35+
"inputSchema": {
36+
"type": "object",
37+
"required": [],
38+
"properties": {
39+
"apply": {
40+
"type": "boolean",
41+
"description": (
42+
"If true, actually delete the files. If false (default), "
43+
"only report what would be purged."
44+
),
45+
"default": False,
46+
},
47+
"kind": {
48+
"type": "string",
49+
"description": (
50+
"Restrict the purge to a single page-kind directory. "
51+
"Omit to scan all page kinds."
52+
),
53+
"enum": [
54+
"adr",
55+
"conventions",
56+
"guides",
57+
"journal",
58+
"lessons",
59+
"notes",
60+
"reference",
61+
"specs",
62+
],
63+
"examples": ["notes", "lessons"],
64+
},
65+
},
66+
},
67+
}
68+
69+
# Directories that hold authored page-kind content. Anything else under the
70+
# wiki root (_kinds, _rules, _views, _bibliography, _triggers, .generated)
71+
# is deliberately left alone.
72+
_PAGE_DIRS: frozenset[str] = frozenset(
73+
{
74+
"adr",
75+
"conventions",
76+
"guides",
77+
"journal",
78+
"lessons",
79+
"notes",
80+
"reference",
81+
"specs",
82+
}
83+
)
84+
85+
86+
def _parse_tags(raw: Any) -> list[str]:
87+
"""Extract a list of tag strings from frontmatter value (list or CSV)."""
88+
if isinstance(raw, list):
89+
return [str(t) for t in raw]
90+
if not isinstance(raw, str):
91+
return []
92+
stripped = raw.strip().strip("[]")
93+
return [t.strip().strip("'\"") for t in stripped.split(",") if t.strip()]
94+
95+
96+
def _evaluate_page(md_path: Path) -> tuple[str | None, list[str]]:
97+
"""Classify a single page. Returns (kind_or_None, tags)."""
98+
text = md_path.read_text(encoding="utf-8", errors="ignore")
99+
r = parse_yaml_frontmatter(text)
100+
tags = _parse_tags(r.meta.get("tags"))
101+
body = r.body or ""
102+
lines = body.strip().splitlines()
103+
if lines and lines[0].startswith("# "):
104+
lines = lines[1:]
105+
content = "\n".join(lines).strip() or str(r.meta.get("title", ""))
106+
return classify_memory(content, tags), tags
107+
108+
109+
async def handler(args: dict[str, Any] | None = None) -> dict[str, Any]:
110+
"""Purge wiki pages that no longer pass the classifier."""
111+
args = args or {}
112+
apply = bool(args.get("apply", False))
113+
kind_filter = args.get("kind")
114+
115+
root = Path(WIKI_ROOT).expanduser()
116+
if not root.exists():
117+
return {"error": f"wiki root does not exist: {root}"}
118+
119+
target_dirs = {kind_filter} if kind_filter else _PAGE_DIRS
120+
kept: list[str] = []
121+
purged: list[str] = []
122+
errors: list[str] = []
123+
124+
for md in root.rglob("*.md"):
125+
rel = md.relative_to(root)
126+
if rel.parts[0] not in target_dirs:
127+
continue
128+
try:
129+
decision, _tags = _evaluate_page(md)
130+
if decision is None:
131+
purged.append(str(rel))
132+
if apply:
133+
md.unlink()
134+
else:
135+
kept.append(str(rel))
136+
except (OSError, ValueError) as exc:
137+
errors.append(f"{rel}: {exc}")
138+
139+
# Clean up empty directories after an apply so the tree stays tidy.
140+
if apply and purged:
141+
for dir_path in sorted(root.rglob("*"), key=lambda p: -len(p.parts)):
142+
if (
143+
dir_path.is_dir()
144+
and not any(dir_path.iterdir())
145+
and not dir_path.name.startswith("_")
146+
and dir_path != root
147+
):
148+
try:
149+
dir_path.rmdir()
150+
except OSError:
151+
pass
152+
153+
return {
154+
"applied": apply,
155+
"scanned": len(kept) + len(purged),
156+
"kept": len(kept),
157+
"purged": len(purged),
158+
"purged_paths": purged,
159+
"errors": errors,
160+
"root": str(root),
161+
}

mcp_server/tool_registry_wiki.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Tool registration: wiki authoring tools (6 tools).
1+
"""Tool registration: wiki authoring tools (7 tools).
22
33
Registers the authoring surface that lets Claude maintain a first-class
44
Markdown wiki (ADRs, specs, file docs, notes) alongside PostgreSQL
@@ -14,6 +14,7 @@
1414
wiki_adr,
1515
wiki_link,
1616
wiki_list,
17+
wiki_purge,
1718
wiki_read,
1819
wiki_reindex,
1920
wiki_write,
@@ -29,6 +30,7 @@ def register(mcp: FastMCP) -> None:
2930
_register_wiki_link(mcp)
3031
_register_wiki_adr(mcp)
3132
_register_wiki_reindex(mcp)
33+
_register_wiki_purge(mcp)
3234

3335

3436
def _register_wiki_write(mcp: FastMCP) -> None:
@@ -104,3 +106,16 @@ def _register_wiki_reindex(mcp: FastMCP) -> None:
104106
async def tool_wiki_reindex() -> str:
105107
"""Regenerate the wiki table of contents at .generated/INDEX.md."""
106108
return await safe_handler(wiki_reindex.handler, {})
109+
110+
111+
def _register_wiki_purge(mcp: FastMCP) -> None:
112+
@mcp.tool(name="wiki_purge", description=wiki_purge.schema["description"])
113+
async def tool_wiki_purge(
114+
apply: bool = False,
115+
kind: str | None = None,
116+
) -> str:
117+
"""Re-evaluate and purge wiki pages that fail the current classifier."""
118+
return await safe_handler(
119+
wiki_purge.handler,
120+
{"apply": apply, "kind": kind},
121+
)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "neuro-cortex-memory"
7-
version = "3.10.0"
7+
version = "3.10.1"
88
description = "Scientifically-grounded memory system based on computational neuroscience research"
99
readme = "README.md"
1010
license = "MIT"

tests_py/test_main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ def test_mcp_server_has_tools(self):
5757
assert "wiki_link" in tool_names
5858
assert "wiki_adr" in tool_names
5959
assert "wiki_reindex" in tool_names
60-
assert len(tool_names) == 40
60+
assert "wiki_purge" in tool_names
61+
assert len(tool_names) == 41
6162

6263
def test_mcp_server_name_and_version(self):
6364
assert mcp.name == "methodology-agent"

0 commit comments

Comments
 (0)