From fd8db388b92de777a25d382de931fc3be568a5f9 Mon Sep 17 00:00:00 2001 From: attaboy11 Date: Sun, 31 May 2026 09:01:17 +0100 Subject: [PATCH 1/8] Add Claude Code skills Memanto bridge demo --- examples/claudecode-skills-memanto/README.md | 116 +++++++++++ .../claudecode-skills-memanto/assets/demo.gif | Bin 0 -> 91391 bytes .../make_demo_gif.py | 123 ++++++++++++ .../memory_backends.py | 182 ++++++++++++++++++ .../requirements.txt | 3 + .../run_cross_skill_demo.py | 73 +++++++ .../skill_memory_bridge.py | 62 ++++++ 7 files changed, 559 insertions(+) create mode 100644 examples/claudecode-skills-memanto/README.md create mode 100644 examples/claudecode-skills-memanto/assets/demo.gif create mode 100644 examples/claudecode-skills-memanto/make_demo_gif.py create mode 100644 examples/claudecode-skills-memanto/memory_backends.py create mode 100644 examples/claudecode-skills-memanto/requirements.txt create mode 100644 examples/claudecode-skills-memanto/run_cross_skill_demo.py create mode 100644 examples/claudecode-skills-memanto/skill_memory_bridge.py diff --git a/examples/claudecode-skills-memanto/README.md b/examples/claudecode-skills-memanto/README.md new file mode 100644 index 00000000..ba0c7344 --- /dev/null +++ b/examples/claudecode-skills-memanto/README.md @@ -0,0 +1,116 @@ +# Claude Code Skills + Memanto Memory Bridge + +This example shows how Memanto can act as a global memory companion across separate developer skill executions. + +The bridge has two lifecycle hooks: + +- `before_skill(...)`: recall relevant engineering memory and return a concise context block that can be appended to a skill prompt. +- `after_skill(...)`: extract durable project decisions, coding preferences, and codebase quirks from a completed skill transcript, then store them in Memanto. + +![Cross-skill memory demo](assets/demo.gif) + +## Why This Matters + +Developer skills are intentionally small and focused. That is useful, but it means a decision captured during a review skill can disappear before a later testing or implementation skill starts. + +This example keeps those decisions outside the individual skill run: + +1. `/grill-with-docs` reviews an architecture plan and records durable decisions. +2. A fresh `/tdd` run asks for tests in the same project. +3. Memanto recalls the earlier decisions and injects them as a compact engineering-memory block. + +## Files + +```text +examples/claudecode-skills-memanto/ +|-- README.md +|-- assets/demo.gif +|-- make_demo_gif.py +|-- memory_backends.py +|-- requirements.txt +|-- run_cross_skill_demo.py +`-- skill_memory_bridge.py +``` + +## Quick Start + +```bash +cd examples/claudecode-skills-memanto +python -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt + +# Runs without external keys by using the local JSON backend. +python run_cross_skill_demo.py --backend file --reset +``` + +## Run With Memanto + +Install and configure Memanto first: + +```bash +pip install memanto +memanto +``` + +Then run the same bridge against the real Memanto CLI backend: + +```bash +python run_cross_skill_demo.py --backend memanto --agent-id claudecode-skills-demo +``` + +## Integration Pattern + +Wrap skill execution with the bridge: + +```python +bridge = SkillMemoryBridge(memory_backend) + +memory_context = bridge.before_skill( + skill_name="/tdd", + task="Add tests for invoice webhook idempotency", + file_paths=["apps/billing/webhooks/stripe.ts"], +) + +skill_prompt = f"{memory_context}\n\n{original_skill_prompt}" + +result = run_skill(skill_prompt) + +bridge.after_skill( + skill_name="/tdd", + transcript=result.transcript, + file_paths=result.files_touched, +) +``` + +The bridge deliberately stores only durable engineering facts: + +- `Decision: Keep Stripe webhook handlers idempotent by event id.` +- `Preference: Tests should cover replayed webhook payloads.` +- `Quirk: Billing code stores timestamps as UTC ISO strings.` + +It avoids saving full prompts, private credentials, or large transient logs. + +## Verification + +Regenerate the GIF: + +```bash +python make_demo_gif.py +``` + +Run the demo: + +```bash +python run_cross_skill_demo.py --backend file --reset +``` + +Expected output includes: + +```text +MEMANTO ENGINEERING MEMORY +- Decision: Keep billing writes idempotent by Stripe event id. +- Preference: Add replay tests before changing webhook behavior. +- Quirk: Billing timestamps are stored as UTC ISO strings. +``` + diff --git a/examples/claudecode-skills-memanto/assets/demo.gif b/examples/claudecode-skills-memanto/assets/demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..ab170bd2aea94923f79a056a57c590dde6626530 GIT binary patch literal 91391 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 None: + assets_dir = Path(__file__).parent / "assets" + assets_dir.mkdir(exist_ok=True) + output = assets_dir / "demo.gif" + frames: list[Image.Image] = [] + font = _font(30) + small = _font(23) + tiny = _font(20) + + for index, (title, lines) in enumerate(STEPS, start=1): + for _ in range(6): + image = Image.new("RGB", (WIDTH, HEIGHT), BG) + draw = ImageDraw.Draw(image) + draw.rounded_rectangle((44, 44, WIDTH - 44, HEIGHT - 44), radius=18, fill=PANEL) + draw.text((82, 82), "Claude Code Skills + Memanto", font=font, fill=GREEN) + draw.text((82, 126), f"Step {index}/5: {title}", font=small, fill=TEXT) + draw.line((82, 174, WIDTH - 82, 174), fill=(63, 77, 94), width=2) + + y = 218 + for line in lines: + color = BLUE if line.startswith("MEMANTO") else TEXT + draw.text((104, y), line, font=small, fill=color) + y += 50 + + draw.text( + (82, HEIGHT - 96), + "Global active memory across isolated developer skills", + font=tiny, + fill=MUTED, + ) + frames.append(image) + + frames[0].save( + output, + save_all=True, + append_images=frames[1:], + duration=1000, + loop=0, + optimize=True, + ) + print(output) + + +def _font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + candidates = [ + "/System/Library/Fonts/SFNS.ttf", + "/System/Library/Fonts/Supplemental/Arial.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + ] + for candidate in candidates: + path = Path(candidate) + if path.exists(): + return ImageFont.truetype(str(path), size=size) + return ImageFont.load_default() + + +if __name__ == "__main__": + main() + diff --git a/examples/claudecode-skills-memanto/memory_backends.py b/examples/claudecode-skills-memanto/memory_backends.py new file mode 100644 index 00000000..972eee91 --- /dev/null +++ b/examples/claudecode-skills-memanto/memory_backends.py @@ -0,0 +1,182 @@ +"""Memory backends for the Claude Code skills + Memanto demo.""" + +from __future__ import annotations + +import json +import re +import subprocess +import warnings +from dataclasses import asdict, dataclass +from datetime import datetime, timezone +from pathlib import Path + + +@dataclass +class MemoryRecord: + content: str + memory_type: str = "learning" + source: str = "claudecode-skills-demo" + tags: str = "claudecode,skills,memanto" + created_at: str = "" + + def to_json(self) -> dict[str, str]: + payload = asdict(self) + payload["created_at"] = payload["created_at"] or datetime.now( + timezone.utc + ).isoformat() + return payload + + +class BaseMemoryBackend: + def remember( + self, + content: str, + *, + memory_type: str = "learning", + tags: str = "claudecode,skills,memanto", + ) -> None: + raise NotImplementedError + + def recall(self, query: str, *, limit: int = 6) -> list[str]: + raise NotImplementedError + + +class FileMemoryBackend(BaseMemoryBackend): + """Local JSON backend for offline reviewer demos.""" + + def __init__(self, path: Path, *, source: str = "claudecode-skills-demo") -> None: + self.path = path + self.source = source + + def remember( + self, + content: str, + *, + memory_type: str = "learning", + tags: str = "claudecode,skills,memanto", + ) -> None: + self.path.parent.mkdir(parents=True, exist_ok=True) + records = self._load() + records.append( + MemoryRecord( + content=content, + memory_type=memory_type, + source=self.source, + tags=tags, + ).to_json() + ) + self.path.write_text(json.dumps(records, indent=2) + "\n", encoding="utf-8") + + def recall(self, query: str, *, limit: int = 6) -> list[str]: + query_terms = _terms(query) + ranked: list[tuple[int, str]] = [] + for record in self._load(): + content = str(record.get("content", "")) + score = len(query_terms.intersection(_terms(content))) + if score: + ranked.append((score, content)) + ranked.sort(key=lambda item: item[0], reverse=True) + return [content for _, content in ranked[:limit]] + + def _load(self) -> list[dict[str, str]]: + if not self.path.exists(): + return [] + try: + return json.loads(self.path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + warnings.warn( + f"Ignoring malformed demo memory file: {self.path}", + RuntimeWarning, + stacklevel=2, + ) + return [] + except OSError as exc: + warnings.warn( + f"Could not read demo memory file {self.path}: {exc}", + RuntimeWarning, + stacklevel=2, + ) + return [] + + +class MemantoCliBackend(BaseMemoryBackend): + """Backend that talks to the installed Memanto CLI.""" + + def __init__(self, agent_id: str) -> None: + self.agent_id = agent_id + self._ensure_agent() + + def remember( + self, + content: str, + *, + memory_type: str = "learning", + tags: str = "claudecode,skills,memanto", + ) -> None: + _run( + [ + "memanto", + "remember", + content, + "--type", + memory_type, + "--source", + self.agent_id, + "--tags", + tags, + ] + ) + + def recall(self, query: str, *, limit: int = 6) -> list[str]: + output = _run( + ["memanto", "recall", query, "--limit", str(limit)], + capture=True, + ) + return [ + line.strip(" -") + for line in output.splitlines() + if line.strip() and not line.lstrip().startswith(("MEMANTO", "Agent:")) + ][:limit] + + def _ensure_agent(self) -> None: + try: + create = subprocess.run( + ["memanto", "agent", "create", self.agent_id], + capture_output=True, + text=True, + check=False, + ) + except OSError as exc: + raise _missing_memanto_error() from exc + if create.returncode == 0: + return + _run(["memanto", "agent", "activate", self.agent_id]) + + +def _terms(text: str) -> set[str]: + return set(re.findall(r"[a-z0-9]{3,}", text.lower())) + + +def _run(cmd: list[str], *, capture: bool = False) -> str: + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + ) + except OSError as exc: + raise _missing_memanto_error() from exc + except subprocess.CalledProcessError as exc: + detail = exc.stderr.strip() or exc.stdout.strip() or "unknown CLI error" + raise RuntimeError(f"Memanto CLI command failed: {detail}") from exc + return result.stdout if capture else "" + + +def _missing_memanto_error() -> RuntimeError: + return RuntimeError( + "The `memanto` CLI was not found. Run `pip install memanto` and " + "`memanto` to configure your Moorcheh API key, or use " + "`--backend file` for the offline demo." + ) + diff --git a/examples/claudecode-skills-memanto/requirements.txt b/examples/claudecode-skills-memanto/requirements.txt new file mode 100644 index 00000000..b53b718b --- /dev/null +++ b/examples/claudecode-skills-memanto/requirements.txt @@ -0,0 +1,3 @@ +memanto>=0.1.0 +python-dotenv>=1.0.0 + diff --git a/examples/claudecode-skills-memanto/run_cross_skill_demo.py b/examples/claudecode-skills-memanto/run_cross_skill_demo.py new file mode 100644 index 00000000..266ed77a --- /dev/null +++ b/examples/claudecode-skills-memanto/run_cross_skill_demo.py @@ -0,0 +1,73 @@ +"""Demonstrate Memanto memory moving between separate skill runs.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from memory_backends import FileMemoryBackend, MemantoCliBackend +from skill_memory_bridge import SkillMemoryBridge, SkillRun + + +REVIEW_TRANSCRIPT = """ +/grill-with-docs reviewed the billing webhook plan. + +Decision: Keep billing writes idempotent by Stripe event id. +Preference: Add replay tests before changing webhook behavior. +Quirk: Billing timestamps are stored as UTC ISO strings. +Constraint: Do not persist raw Stripe payloads after signature verification. +""" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Run the Claude Code skills + Memanto memory bridge demo.", + ) + parser.add_argument("--backend", choices=["file", "memanto"], default="file") + parser.add_argument("--agent-id", default="claudecode-skills-demo") + parser.add_argument("--memory-file", default=".demo_skill_memory.json") + parser.add_argument("--reset", action="store_true") + return parser.parse_args() + + +def main() -> None: + args = parse_args() + if args.backend == "file" and args.reset: + Path(args.memory_file).unlink(missing_ok=True) + + memory = ( + MemantoCliBackend(args.agent_id) + if args.backend == "memanto" + else FileMemoryBackend(Path(args.memory_file), source=args.agent_id) + ) + bridge = SkillMemoryBridge(memory) + + review_run = SkillRun( + skill_name="/grill-with-docs", + task="Review billing webhook architecture", + file_paths=["apps/billing/webhooks/stripe.ts", "apps/billing/db/events.ts"], + ) + print("Session 1: /grill-with-docs finishes and stores durable memory") + stored = bridge.after_skill(review_run, REVIEW_TRANSCRIPT) + for item in stored: + print(f"- remembered: {item}") + + tdd_run = SkillRun( + skill_name="/tdd", + task="Add tests for Stripe webhook replay and invoice creation", + file_paths=["apps/billing/webhooks/stripe.ts", "apps/billing/webhooks/stripe.test.ts"], + ) + print("\nSession 2: /tdd starts fresh and asks Memanto for context") + context = bridge.before_skill(tdd_run) + print(context) + + print("\nPrompt fragment for the next skill:") + print( + "Use the recalled engineering memory above when choosing test cases, " + "fixtures, and persistence assertions." + ) + + +if __name__ == "__main__": + main() + diff --git a/examples/claudecode-skills-memanto/skill_memory_bridge.py b/examples/claudecode-skills-memanto/skill_memory_bridge.py new file mode 100644 index 00000000..3f3e8160 --- /dev/null +++ b/examples/claudecode-skills-memanto/skill_memory_bridge.py @@ -0,0 +1,62 @@ +"""Lifecycle bridge between developer skills and Memanto.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass + +from memory_backends import BaseMemoryBackend + + +MEMORY_LINE = re.compile( + r"^(Decision|Preference|Quirk|Constraint|Learning):\s*(.+)$", + flags=re.IGNORECASE | re.MULTILINE, +) + + +@dataclass +class SkillRun: + skill_name: str + task: str + file_paths: list[str] + + +class SkillMemoryBridge: + """Adds recall-before and remember-after hooks to skill execution.""" + + def __init__(self, memory: BaseMemoryBackend) -> None: + self.memory = memory + + def before_skill(self, run: SkillRun, *, limit: int = 6) -> str: + query = self._query_for(run) + memories = self.memory.recall(query, limit=limit) + if not memories: + return "MEMANTO ENGINEERING MEMORY\n- No relevant memories found." + lines = ["MEMANTO ENGINEERING MEMORY"] + lines.extend(f"- {memory}" for memory in memories) + return "\n".join(lines) + + def after_skill(self, run: SkillRun, transcript: str) -> list[str]: + stored: list[str] = [] + for label, value in MEMORY_LINE.findall(transcript): + memory = f"{label.title()}: {value.strip()}" + memory_type = self._memory_type(label) + tags = ",".join(["claudecode", "skills", run.skill_name.strip("/")]) + self.memory.remember(memory, memory_type=memory_type, tags=tags) + stored.append(memory) + return stored + + def _query_for(self, run: SkillRun) -> str: + path_text = " ".join(run.file_paths) + return f"{run.skill_name} {run.task} {path_text}" + + def _memory_type(self, label: str) -> str: + normalized = label.lower() + if normalized == "decision": + return "decision" + if normalized == "preference": + return "preference" + if normalized in {"quirk", "constraint"}: + return "context" + return "learning" + From f17789db3fe20b8af1348c025c4c8d238c8fb678 Mon Sep 17 00:00:00 2001 From: attaboy11 Date: Sun, 31 May 2026 09:16:00 +0100 Subject: [PATCH 2/8] Address skills bridge review feedback --- examples/claudecode-skills-memanto/README.md | 14 +-- .../memory_backends.py | 107 ++++++++++++++---- .../requirements.txt | 2 - 3 files changed, 88 insertions(+), 35 deletions(-) diff --git a/examples/claudecode-skills-memanto/README.md b/examples/claudecode-skills-memanto/README.md index ba0c7344..7702ab13 100644 --- a/examples/claudecode-skills-memanto/README.md +++ b/examples/claudecode-skills-memanto/README.md @@ -64,23 +64,22 @@ python run_cross_skill_demo.py --backend memanto --agent-id claudecode-skills-de Wrap skill execution with the bridge: ```python -bridge = SkillMemoryBridge(memory_backend) +from skill_memory_bridge import SkillMemoryBridge, SkillRun -memory_context = bridge.before_skill( +bridge = SkillMemoryBridge(memory_backend) +run = SkillRun( skill_name="/tdd", task="Add tests for invoice webhook idempotency", file_paths=["apps/billing/webhooks/stripe.ts"], ) +memory_context = bridge.before_skill(run) + skill_prompt = f"{memory_context}\n\n{original_skill_prompt}" result = run_skill(skill_prompt) -bridge.after_skill( - skill_name="/tdd", - transcript=result.transcript, - file_paths=result.files_touched, -) +bridge.after_skill(run, result.transcript) ``` The bridge deliberately stores only durable engineering facts: @@ -113,4 +112,3 @@ MEMANTO ENGINEERING MEMORY - Preference: Add replay tests before changing webhook behavior. - Quirk: Billing timestamps are stored as UTC ISO strings. ``` - diff --git a/examples/claudecode-skills-memanto/memory_backends.py b/examples/claudecode-skills-memanto/memory_backends.py index 972eee91..7db5cc42 100644 --- a/examples/claudecode-skills-memanto/memory_backends.py +++ b/examples/claudecode-skills-memanto/memory_backends.py @@ -9,6 +9,9 @@ from dataclasses import asdict, dataclass from datetime import datetime, timezone from pathlib import Path +from typing import Any + +CLI_TIMEOUT_SECONDS = 30 @dataclass @@ -100,7 +103,7 @@ def _load(self) -> list[dict[str, str]]: class MemantoCliBackend(BaseMemoryBackend): - """Backend that talks to the installed Memanto CLI.""" + """Backend that uses the installed Memanto package and CLI session.""" def __init__(self, agent_id: str) -> None: self.agent_id = agent_id @@ -113,30 +116,31 @@ def remember( memory_type: str = "learning", tags: str = "claudecode,skills,memanto", ) -> None: - _run( - [ - "memanto", - "remember", - content, - "--type", - memory_type, - "--source", - self.agent_id, - "--tags", - tags, - ] + self._client().remember( + agent_id=self.agent_id, + memory_type=memory_type, + title=_memory_title(content), + content=content, + confidence=0.8, + tags=_split_tags(tags), + source=self.agent_id, + provenance="explicit_statement", ) def recall(self, query: str, *, limit: int = 6) -> list[str]: - output = _run( - ["memanto", "recall", query, "--limit", str(limit)], - capture=True, + result = self._client().recall( + agent_id=self.agent_id, + query=query, + limit=limit, + tags=_split_tags("claudecode,skills,memanto"), ) - return [ - line.strip(" -") - for line in output.splitlines() - if line.strip() and not line.lstrip().startswith(("MEMANTO", "Agent:")) - ][:limit] + memories = result.get("memories", []) + recalled: list[str] = [] + for memory in memories: + content = str(memory.get("content") or memory.get("title") or "").strip() + if content: + recalled.append(content) + return recalled[:limit] def _ensure_agent(self) -> None: try: @@ -145,32 +149,86 @@ def _ensure_agent(self) -> None: capture_output=True, text=True, check=False, + timeout=CLI_TIMEOUT_SECONDS, ) except OSError as exc: raise _missing_memanto_error() from exc + except subprocess.TimeoutExpired as exc: + raise RuntimeError("Memanto CLI agent creation timed out") from exc if create.returncode == 0: return + detail = _command_detail(create.stdout, create.stderr) + if "already exists" not in detail.lower() and "exists" not in detail.lower(): + raise RuntimeError(f"Memanto CLI agent creation failed: {detail}") _run(["memanto", "agent", "activate", self.agent_id]) + def _client(self) -> Any: + try: + from memanto.cli.client.sdk_client import SdkClient + from memanto.cli.config.manager import ConfigManager + except ImportError as exc: + raise _missing_memanto_error() from exc + + config = ConfigManager() + api_key = config.get_api_key() + if not api_key: + raise RuntimeError( + "MEMANTO is not configured. Run `memanto` to set an API key." + ) + + active_agent_id, active_session_token = config.get_active_session() + if active_agent_id != self.agent_id or not active_session_token: + self._ensure_agent() + active_agent_id, active_session_token = config.get_active_session() + if active_agent_id != self.agent_id or not active_session_token: + raise RuntimeError(f"Memanto agent `{self.agent_id}` is not active.") + + client = SdkClient(api_key) + client.agent_id = self.agent_id + client.session_token = active_session_token + return client + def _terms(text: str) -> set[str]: return set(re.findall(r"[a-z0-9]{3,}", text.lower())) -def _run(cmd: list[str], *, capture: bool = False) -> str: +def _split_tags(tags: str) -> list[str]: + return [tag.strip() for tag in tags.split(",") if tag.strip()] + + +def _memory_title(content: str) -> str: + return content[:47] + "..." if len(content) > 50 else content + + +def _command_detail(stdout: str | bytes | None, stderr: str | bytes | None) -> str: + for stream in (stderr, stdout): + if isinstance(stream, bytes): + stream = stream.decode(errors="replace") + detail = (stream or "").strip() + if detail: + return detail + return "unknown CLI error" + + +def _run(cmd: list[str]) -> str: try: result = subprocess.run( cmd, capture_output=True, text=True, check=True, + timeout=CLI_TIMEOUT_SECONDS, ) except OSError as exc: raise _missing_memanto_error() from exc + except subprocess.TimeoutExpired as exc: + detail = _command_detail(exc.stdout, exc.stderr) + raise RuntimeError(f"Memanto CLI command timed out: {detail}") from exc except subprocess.CalledProcessError as exc: - detail = exc.stderr.strip() or exc.stdout.strip() or "unknown CLI error" + detail = _command_detail(exc.stdout, exc.stderr) raise RuntimeError(f"Memanto CLI command failed: {detail}") from exc - return result.stdout if capture else "" + return result.stdout def _missing_memanto_error() -> RuntimeError: @@ -179,4 +237,3 @@ def _missing_memanto_error() -> RuntimeError: "`memanto` to configure your Moorcheh API key, or use " "`--backend file` for the offline demo." ) - diff --git a/examples/claudecode-skills-memanto/requirements.txt b/examples/claudecode-skills-memanto/requirements.txt index b53b718b..ee19be25 100644 --- a/examples/claudecode-skills-memanto/requirements.txt +++ b/examples/claudecode-skills-memanto/requirements.txt @@ -1,3 +1 @@ memanto>=0.1.0 -python-dotenv>=1.0.0 - From 47f1d45d7d5a0aa3a7b91c23d976dde3376bfa11 Mon Sep 17 00:00:00 2001 From: attaboy11 Date: Sun, 31 May 2026 09:22:12 +0100 Subject: [PATCH 3/8] Document skills memory bridge example --- .../make_demo_gif.py | 4 ++-- .../memory_backends.py | 22 +++++++++++++++++++ .../run_cross_skill_demo.py | 4 ++-- .../skill_memory_bridge.py | 9 ++++++-- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/examples/claudecode-skills-memanto/make_demo_gif.py b/examples/claudecode-skills-memanto/make_demo_gif.py index 64dc7d2f..ea281ec5 100644 --- a/examples/claudecode-skills-memanto/make_demo_gif.py +++ b/examples/claudecode-skills-memanto/make_demo_gif.py @@ -6,7 +6,6 @@ from PIL import Image, ImageDraw, ImageFont - WIDTH = 1040 HEIGHT = 640 BG = (15, 19, 27) @@ -63,6 +62,7 @@ def main() -> None: + """Render the animated walkthrough GIF into the local assets directory.""" assets_dir = Path(__file__).parent / "assets" assets_dir.mkdir(exist_ok=True) output = assets_dir / "demo.gif" @@ -106,6 +106,7 @@ def main() -> None: def _font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: + """Load a readable system font with a portable default fallback.""" candidates = [ "/System/Library/Fonts/SFNS.ttf", "/System/Library/Fonts/Supplemental/Arial.ttf", @@ -120,4 +121,3 @@ def _font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: if __name__ == "__main__": main() - diff --git a/examples/claudecode-skills-memanto/memory_backends.py b/examples/claudecode-skills-memanto/memory_backends.py index 7db5cc42..fb7e6c11 100644 --- a/examples/claudecode-skills-memanto/memory_backends.py +++ b/examples/claudecode-skills-memanto/memory_backends.py @@ -16,6 +16,8 @@ @dataclass class MemoryRecord: + """Serializable memory entry used by the offline JSON backend.""" + content: str memory_type: str = "learning" source: str = "claudecode-skills-demo" @@ -23,6 +25,7 @@ class MemoryRecord: created_at: str = "" def to_json(self) -> dict[str, str]: + """Return the JSON-ready record with a creation timestamp filled in.""" payload = asdict(self) payload["created_at"] = payload["created_at"] or datetime.now( timezone.utc @@ -31,6 +34,8 @@ def to_json(self) -> dict[str, str]: class BaseMemoryBackend: + """Minimal storage protocol consumed by the skill memory bridge.""" + def remember( self, content: str, @@ -38,9 +43,11 @@ def remember( memory_type: str = "learning", tags: str = "claudecode,skills,memanto", ) -> None: + """Persist a durable memory extracted from a completed skill run.""" raise NotImplementedError def recall(self, query: str, *, limit: int = 6) -> list[str]: + """Return relevant stored memories for a new skill run query.""" raise NotImplementedError @@ -48,6 +55,7 @@ class FileMemoryBackend(BaseMemoryBackend): """Local JSON backend for offline reviewer demos.""" def __init__(self, path: Path, *, source: str = "claudecode-skills-demo") -> None: + """Create a file-backed memory store at the supplied path.""" self.path = path self.source = source @@ -58,6 +66,7 @@ def remember( memory_type: str = "learning", tags: str = "claudecode,skills,memanto", ) -> None: + """Append one memory record to the demo JSON file.""" self.path.parent.mkdir(parents=True, exist_ok=True) records = self._load() records.append( @@ -71,6 +80,7 @@ def remember( self.path.write_text(json.dumps(records, indent=2) + "\n", encoding="utf-8") def recall(self, query: str, *, limit: int = 6) -> list[str]: + """Rank stored memories by token overlap with the recall query.""" query_terms = _terms(query) ranked: list[tuple[int, str]] = [] for record in self._load(): @@ -82,6 +92,7 @@ def recall(self, query: str, *, limit: int = 6) -> list[str]: return [content for _, content in ranked[:limit]] def _load(self) -> list[dict[str, str]]: + """Load stored records, tolerating missing or malformed demo files.""" if not self.path.exists(): return [] try: @@ -106,6 +117,7 @@ class MemantoCliBackend(BaseMemoryBackend): """Backend that uses the installed Memanto package and CLI session.""" def __init__(self, agent_id: str) -> None: + """Bind the backend to a Memanto agent and activate its session.""" self.agent_id = agent_id self._ensure_agent() @@ -116,6 +128,7 @@ def remember( memory_type: str = "learning", tags: str = "claudecode,skills,memanto", ) -> None: + """Store a memory through the Memanto SDK with provenance metadata.""" self._client().remember( agent_id=self.agent_id, memory_type=memory_type, @@ -128,6 +141,7 @@ def remember( ) def recall(self, query: str, *, limit: int = 6) -> list[str]: + """Recall relevant Memanto memories using the SDK response payload.""" result = self._client().recall( agent_id=self.agent_id, query=query, @@ -143,6 +157,7 @@ def recall(self, query: str, *, limit: int = 6) -> list[str]: return recalled[:limit] def _ensure_agent(self) -> None: + """Create or activate the configured Memanto demo agent.""" try: create = subprocess.run( ["memanto", "agent", "create", self.agent_id], @@ -163,6 +178,7 @@ def _ensure_agent(self) -> None: _run(["memanto", "agent", "activate", self.agent_id]) def _client(self) -> Any: + """Build a configured SDK client for the active Memanto session.""" try: from memanto.cli.client.sdk_client import SdkClient from memanto.cli.config.manager import ConfigManager @@ -190,18 +206,22 @@ def _client(self) -> Any: def _terms(text: str) -> set[str]: + """Extract lowercase search terms from a free-form string.""" return set(re.findall(r"[a-z0-9]{3,}", text.lower())) def _split_tags(tags: str) -> list[str]: + """Split a comma-separated tag list into non-empty tag names.""" return [tag.strip() for tag in tags.split(",") if tag.strip()] def _memory_title(content: str) -> str: + """Create a compact Memanto title from memory content.""" return content[:47] + "..." if len(content) > 50 else content def _command_detail(stdout: str | bytes | None, stderr: str | bytes | None) -> str: + """Return the most helpful text from a completed or timed-out command.""" for stream in (stderr, stdout): if isinstance(stream, bytes): stream = stream.decode(errors="replace") @@ -212,6 +232,7 @@ def _command_detail(stdout: str | bytes | None, stderr: str | bytes | None) -> s def _run(cmd: list[str]) -> str: + """Run a Memanto CLI command with bounded execution time.""" try: result = subprocess.run( cmd, @@ -232,6 +253,7 @@ def _run(cmd: list[str]) -> str: def _missing_memanto_error() -> RuntimeError: + """Create a consistent error for missing or unconfigured Memanto tooling.""" return RuntimeError( "The `memanto` CLI was not found. Run `pip install memanto` and " "`memanto` to configure your Moorcheh API key, or use " diff --git a/examples/claudecode-skills-memanto/run_cross_skill_demo.py b/examples/claudecode-skills-memanto/run_cross_skill_demo.py index 266ed77a..9b6480ee 100644 --- a/examples/claudecode-skills-memanto/run_cross_skill_demo.py +++ b/examples/claudecode-skills-memanto/run_cross_skill_demo.py @@ -8,7 +8,6 @@ from memory_backends import FileMemoryBackend, MemantoCliBackend from skill_memory_bridge import SkillMemoryBridge, SkillRun - REVIEW_TRANSCRIPT = """ /grill-with-docs reviewed the billing webhook plan. @@ -20,6 +19,7 @@ def parse_args() -> argparse.Namespace: + """Parse backend and reset options for the demo runner.""" parser = argparse.ArgumentParser( description="Run the Claude Code skills + Memanto memory bridge demo.", ) @@ -31,6 +31,7 @@ def parse_args() -> argparse.Namespace: def main() -> None: + """Run two isolated skill sessions that share memory through the bridge.""" args = parse_args() if args.backend == "file" and args.reset: Path(args.memory_file).unlink(missing_ok=True) @@ -70,4 +71,3 @@ def main() -> None: if __name__ == "__main__": main() - diff --git a/examples/claudecode-skills-memanto/skill_memory_bridge.py b/examples/claudecode-skills-memanto/skill_memory_bridge.py index 3f3e8160..1e0358c1 100644 --- a/examples/claudecode-skills-memanto/skill_memory_bridge.py +++ b/examples/claudecode-skills-memanto/skill_memory_bridge.py @@ -7,7 +7,6 @@ from memory_backends import BaseMemoryBackend - MEMORY_LINE = re.compile( r"^(Decision|Preference|Quirk|Constraint|Learning):\s*(.+)$", flags=re.IGNORECASE | re.MULTILINE, @@ -16,6 +15,8 @@ @dataclass class SkillRun: + """Metadata about one isolated developer skill execution.""" + skill_name: str task: str file_paths: list[str] @@ -25,9 +26,11 @@ class SkillMemoryBridge: """Adds recall-before and remember-after hooks to skill execution.""" def __init__(self, memory: BaseMemoryBackend) -> None: + """Create a bridge around any compatible memory backend.""" self.memory = memory def before_skill(self, run: SkillRun, *, limit: int = 6) -> str: + """Format relevant recalled memories for injection into a skill prompt.""" query = self._query_for(run) memories = self.memory.recall(query, limit=limit) if not memories: @@ -37,6 +40,7 @@ def before_skill(self, run: SkillRun, *, limit: int = 6) -> str: return "\n".join(lines) def after_skill(self, run: SkillRun, transcript: str) -> list[str]: + """Extract labeled durable memories from a completed skill transcript.""" stored: list[str] = [] for label, value in MEMORY_LINE.findall(transcript): memory = f"{label.title()}: {value.strip()}" @@ -47,10 +51,12 @@ def after_skill(self, run: SkillRun, transcript: str) -> list[str]: return stored def _query_for(self, run: SkillRun) -> str: + """Build a compact recall query from skill metadata.""" path_text = " ".join(run.file_paths) return f"{run.skill_name} {run.task} {path_text}" def _memory_type(self, label: str) -> str: + """Map a transcript label to Memanto's memory type vocabulary.""" normalized = label.lower() if normalized == "decision": return "decision" @@ -59,4 +65,3 @@ def _memory_type(self, label: str) -> str: if normalized in {"quirk", "constraint"}: return "context" return "learning" - From 752f3bf98e0e6c675c8e80b2356f9890a0b7e530 Mon Sep 17 00:00:00 2001 From: attaboy11 Date: Sun, 31 May 2026 09:23:17 +0100 Subject: [PATCH 4/8] Harden skills demo file memory backend --- .../memory_backends.py | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/examples/claudecode-skills-memanto/memory_backends.py b/examples/claudecode-skills-memanto/memory_backends.py index fb7e6c11..4378136d 100644 --- a/examples/claudecode-skills-memanto/memory_backends.py +++ b/examples/claudecode-skills-memanto/memory_backends.py @@ -3,8 +3,10 @@ from __future__ import annotations import json +import os import re import subprocess +import tempfile import warnings from dataclasses import asdict, dataclass from datetime import datetime, timezone @@ -77,7 +79,7 @@ def remember( tags=tags, ).to_json() ) - self.path.write_text(json.dumps(records, indent=2) + "\n", encoding="utf-8") + _write_records_atomic(self.path, records) def recall(self, query: str, *, limit: int = 6) -> list[str]: """Rank stored memories by token overlap with the recall query.""" @@ -96,7 +98,15 @@ def _load(self) -> list[dict[str, str]]: if not self.path.exists(): return [] try: - return json.loads(self.path.read_text(encoding="utf-8")) + payload = json.loads(self.path.read_text(encoding="utf-8")) + if _is_record_list(payload): + return payload + warnings.warn( + f"Ignoring demo memory file with unexpected shape: {self.path}", + RuntimeWarning, + stacklevel=2, + ) + return [] except json.JSONDecodeError: warnings.warn( f"Ignoring malformed demo memory file: {self.path}", @@ -210,6 +220,43 @@ def _terms(text: str) -> set[str]: return set(re.findall(r"[a-z0-9]{3,}", text.lower())) +def _write_records_atomic(path: Path, records: list[dict[str, str]]) -> None: + """Write memory records through a same-directory temp file replacement.""" + payload = json.dumps(records, indent=2) + "\n" + path.parent.mkdir(parents=True, exist_ok=True) + tmp_name = "" + try: + with tempfile.NamedTemporaryFile( + "w", + dir=path.parent, + encoding="utf-8", + prefix=f".{path.name}.", + suffix=".tmp", + delete=False, + ) as tmp: + tmp_name = tmp.name + tmp.write(payload) + tmp.flush() + os.fsync(tmp.fileno()) + Path(tmp_name).replace(path) + except Exception: + if tmp_name: + Path(tmp_name).unlink(missing_ok=True) + raise + + +def _is_record_list(payload: Any) -> bool: + """Return whether parsed JSON matches list[dict[str, str]].""" + return isinstance(payload, list) and all( + isinstance(item, dict) + and all( + isinstance(key, str) and isinstance(value, str) + for key, value in item.items() + ) + for item in payload + ) + + def _split_tags(tags: str) -> list[str]: """Split a comma-separated tag list into non-empty tag names.""" return [tag.strip() for tag in tags.split(",") if tag.strip()] From 1acabd965fb290c8b5f0031d4776996000febc4e Mon Sep 17 00:00:00 2001 From: attaboy11 Date: Sun, 31 May 2026 20:58:34 +0100 Subject: [PATCH 5/8] Add skills bridge tests --- examples/claudecode-skills-memanto/README.md | 9 +- .../skill_memory_bridge.py | 2 +- .../tests/test_skill_memory_bridge.py | 123 ++++++++++++++++++ 3 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 examples/claudecode-skills-memanto/tests/test_skill_memory_bridge.py diff --git a/examples/claudecode-skills-memanto/README.md b/examples/claudecode-skills-memanto/README.md index 7702ab13..cfe58cc4 100644 --- a/examples/claudecode-skills-memanto/README.md +++ b/examples/claudecode-skills-memanto/README.md @@ -29,7 +29,8 @@ examples/claudecode-skills-memanto/ |-- memory_backends.py |-- requirements.txt |-- run_cross_skill_demo.py -`-- skill_memory_bridge.py +|-- skill_memory_bridge.py +`-- tests/test_skill_memory_bridge.py ``` ## Quick Start @@ -104,6 +105,12 @@ Run the demo: python run_cross_skill_demo.py --backend file --reset ``` +Run the focused offline tests: + +```bash +python -m unittest discover -s tests -v +``` + Expected output includes: ```text diff --git a/examples/claudecode-skills-memanto/skill_memory_bridge.py b/examples/claudecode-skills-memanto/skill_memory_bridge.py index 1e0358c1..c76baf11 100644 --- a/examples/claudecode-skills-memanto/skill_memory_bridge.py +++ b/examples/claudecode-skills-memanto/skill_memory_bridge.py @@ -8,7 +8,7 @@ from memory_backends import BaseMemoryBackend MEMORY_LINE = re.compile( - r"^(Decision|Preference|Quirk|Constraint|Learning):\s*(.+)$", + r"^\s*(Decision|Preference|Quirk|Constraint|Learning):\s*(.+)$", flags=re.IGNORECASE | re.MULTILINE, ) diff --git a/examples/claudecode-skills-memanto/tests/test_skill_memory_bridge.py b/examples/claudecode-skills-memanto/tests/test_skill_memory_bridge.py new file mode 100644 index 00000000..a42aae0c --- /dev/null +++ b/examples/claudecode-skills-memanto/tests/test_skill_memory_bridge.py @@ -0,0 +1,123 @@ +"""Tests for the Claude Code skills + Memanto bridge example.""" + +from __future__ import annotations + +import json +import sys +import tempfile +import unittest +import warnings +from pathlib import Path + +EXAMPLE_DIR = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(EXAMPLE_DIR)) + +from memory_backends import FileMemoryBackend # noqa: E402 +from skill_memory_bridge import SkillMemoryBridge, SkillRun # noqa: E402 + + +class SkillMemoryBridgeTests(unittest.TestCase): + """Exercise the offline reviewer path used by the bounty PR.""" + + def test_bridge_stores_labeled_memories_and_recalls_relevant_context(self) -> None: + with tempfile.TemporaryDirectory() as tmp_dir: + memory = FileMemoryBackend(Path(tmp_dir) / "memory.json") + bridge = SkillMemoryBridge(memory) + review_run = SkillRun( + skill_name="/grill-with-docs", + task="Review billing webhook behavior", + file_paths=["apps/billing/webhooks/stripe.ts"], + ) + + stored = bridge.after_skill( + review_run, + """ + Decision: Keep writes idempotent by Stripe event id. + Preference: Add replay tests before webhook changes. + Quirk: Billing timestamps are UTC ISO strings. + Constraint: Do not persist raw Stripe payloads. + Note: This unlabeled line should not be stored. + """, + ) + + self.assertEqual( + stored, + [ + "Decision: Keep writes idempotent by Stripe event id.", + "Preference: Add replay tests before webhook changes.", + "Quirk: Billing timestamps are UTC ISO strings.", + "Constraint: Do not persist raw Stripe payloads.", + ], + ) + tdd_run = SkillRun( + skill_name="/tdd", + task="Add Stripe webhook replay tests", + file_paths=["apps/billing/webhooks/stripe.test.ts"], + ) + + context = bridge.before_skill(tdd_run) + + self.assertIn("MEMANTO ENGINEERING MEMORY", context) + self.assertIn("Stripe event id", context) + self.assertIn("Add replay tests", context) + self.assertNotIn("unlabeled line", context) + + def test_file_backend_ranks_recall_by_query_overlap(self) -> None: + with tempfile.TemporaryDirectory() as tmp_dir: + memory = FileMemoryBackend(Path(tmp_dir) / "memory.json") + memory.remember("Decision: Stripe webhook replay tests use event ids.") + memory.remember("Learning: Dashboard filters use URL search params.") + memory.remember("Constraint: Stripe payloads are discarded after signature checks.") + + recalled = memory.recall("Stripe webhook replay event tests", limit=2) + + self.assertEqual( + recalled, + [ + "Decision: Stripe webhook replay tests use event ids.", + "Constraint: Stripe payloads are discarded after signature checks.", + ], + ) + + def test_malformed_offline_memory_file_recovers_on_write(self) -> None: + with tempfile.TemporaryDirectory() as tmp_dir: + path = Path(tmp_dir) / "memory.json" + path.write_text("{not-json", encoding="utf-8") + memory = FileMemoryBackend(path) + bridge = SkillMemoryBridge(memory) + run = SkillRun( + skill_name="/handoff", + task="Summarize webhook constraints", + file_paths=["apps/billing/webhooks/stripe.ts"], + ) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + self.assertEqual(memory.recall("webhook"), []) + bridge.after_skill(run, "Learning: Keep webhook fixtures minimal.") + + self.assertTrue(caught) + records = json.loads(path.read_text(encoding="utf-8")) + self.assertEqual( + [record["content"] for record in records], + ["Learning: Keep webhook fixtures minimal."], + ) + + def test_unexpected_offline_memory_shape_recovers_on_write(self) -> None: + with tempfile.TemporaryDirectory() as tmp_dir: + path = Path(tmp_dir) / "memory.json" + path.write_text('{"content": "not a list"}', encoding="utf-8") + memory = FileMemoryBackend(path) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + memory.remember("Decision: Use a same-directory temp file.") + + self.assertTrue(caught) + records = json.loads(path.read_text(encoding="utf-8")) + self.assertEqual(records[0]["content"], "Decision: Use a same-directory temp file.") + self.assertEqual(records[0]["memory_type"], "learning") + + +if __name__ == "__main__": + unittest.main() From 08b23f61c2b3206a7e7233f777f6d1a43ab71b7d Mon Sep 17 00:00:00 2001 From: attaboy11 Date: Wed, 3 Jun 2026 19:01:39 +0100 Subject: [PATCH 6/8] Make skills memory bridge reusable --- examples/claudecode-skills-memanto/README.md | 31 +++++++- .../skill_memory_bridge.py | 73 +++++++++++++++++-- .../tests/test_skill_memory_bridge.py | 60 ++++++++++++++- 3 files changed, 155 insertions(+), 9 deletions(-) diff --git a/examples/claudecode-skills-memanto/README.md b/examples/claudecode-skills-memanto/README.md index cfe58cc4..04ee6a72 100644 --- a/examples/claudecode-skills-memanto/README.md +++ b/examples/claudecode-skills-memanto/README.md @@ -6,6 +6,7 @@ The bridge has two lifecycle hooks: - `before_skill(...)`: recall relevant engineering memory and return a concise context block that can be appended to a skill prompt. - `after_skill(...)`: extract durable project decisions, coding preferences, and codebase quirks from a completed skill transcript, then store them in Memanto. +- `run_with_memory(...)`: wrap any existing skill runner callable with both hooks, so teams can drop the bridge around their current `/tdd`, `/handoff`, or custom skill executor without rewriting those skills. ![Cross-skill memory demo](assets/demo.gif) @@ -62,7 +63,7 @@ python run_cross_skill_demo.py --backend memanto --agent-id claudecode-skills-de ## Integration Pattern -Wrap skill execution with the bridge: +Wrap skill execution with the bridge directly: ```python from skill_memory_bridge import SkillMemoryBridge, SkillRun @@ -83,6 +84,34 @@ result = run_skill(skill_prompt) bridge.after_skill(run, result.transcript) ``` +Or use the executor-agnostic wrapper when you already have a function that runs a skill: + +```python +def run_skill(prompt: str) -> str: + # Call your existing Claude Code, shell, or local skill runner here. + return "Learning: Invoice export tests should preserve customer locale." + +result = bridge.run_with_memory( + run, + "Create a handoff note for the invoice export branch.", + run_skill, +) + +print(result.prompt) +print(result.stored_memories) +``` + +`SkillRun.metadata` can carry project, framework, branch, tenant, or issue identifiers into the recall query without changing the bridge internals: + +```python +run = SkillRun( + skill_name="/tdd", + task="Add route tests", + file_paths=["apps/mobile/app/(tabs)/index.tsx"], + metadata={"framework": "expo-router", "project": "mobile-app"}, +) +``` + The bridge deliberately stores only durable engineering facts: - `Decision: Keep Stripe webhook handlers idempotent by event id.` diff --git a/examples/claudecode-skills-memanto/skill_memory_bridge.py b/examples/claudecode-skills-memanto/skill_memory_bridge.py index c76baf11..2e41b1d2 100644 --- a/examples/claudecode-skills-memanto/skill_memory_bridge.py +++ b/examples/claudecode-skills-memanto/skill_memory_bridge.py @@ -3,7 +3,8 @@ from __future__ import annotations import re -from dataclasses import dataclass +from collections.abc import Callable +from dataclasses import dataclass, field from memory_backends import BaseMemoryBackend @@ -20,32 +21,82 @@ class SkillRun: skill_name: str task: str file_paths: list[str] + metadata: dict[str, str] = field(default_factory=dict) + + +@dataclass +class SkillExecution: + """Result returned when the bridge wraps an arbitrary skill runner.""" + + prompt: str + transcript: str + stored_memories: list[str] class SkillMemoryBridge: - """Adds recall-before and remember-after hooks to skill execution.""" + """Drop-in recall-before and remember-after hooks for skill execution.""" - def __init__(self, memory: BaseMemoryBackend) -> None: + def __init__( + self, + memory: BaseMemoryBackend, + *, + header: str = "MEMANTO ENGINEERING MEMORY", + base_tags: tuple[str, ...] = ("claudecode", "skills"), + ) -> None: """Create a bridge around any compatible memory backend.""" self.memory = memory + self.header = header + self.base_tags = base_tags def before_skill(self, run: SkillRun, *, limit: int = 6) -> str: """Format relevant recalled memories for injection into a skill prompt.""" query = self._query_for(run) memories = self.memory.recall(query, limit=limit) if not memories: - return "MEMANTO ENGINEERING MEMORY\n- No relevant memories found." - lines = ["MEMANTO ENGINEERING MEMORY"] + return f"{self.header}\n- No relevant memories found." + lines = [self.header] lines.extend(f"- {memory}" for memory in memories) return "\n".join(lines) + def prompt_with_memory( + self, + run: SkillRun, + original_prompt: str, + *, + limit: int = 6, + ) -> str: + """Return a skill prompt prefixed with recalled engineering memory.""" + memory_context = self.before_skill(run, limit=limit) + prompt = original_prompt.strip() + if not prompt: + return memory_context + return f"{memory_context}\n\n{prompt}" + + def run_with_memory( + self, + run: SkillRun, + original_prompt: str, + executor: Callable[[str], str], + *, + limit: int = 6, + ) -> SkillExecution: + """Wrap any skill runner callable with Memanto memory hooks.""" + prompt = self.prompt_with_memory(run, original_prompt, limit=limit) + transcript = executor(prompt) + stored_memories = self.after_skill(run, transcript) + return SkillExecution( + prompt=prompt, + transcript=transcript, + stored_memories=stored_memories, + ) + def after_skill(self, run: SkillRun, transcript: str) -> list[str]: """Extract labeled durable memories from a completed skill transcript.""" stored: list[str] = [] for label, value in MEMORY_LINE.findall(transcript): memory = f"{label.title()}: {value.strip()}" memory_type = self._memory_type(label) - tags = ",".join(["claudecode", "skills", run.skill_name.strip("/")]) + tags = ",".join(self._tags_for(run)) self.memory.remember(memory, memory_type=memory_type, tags=tags) stored.append(memory) return stored @@ -53,7 +104,15 @@ def after_skill(self, run: SkillRun, transcript: str) -> list[str]: def _query_for(self, run: SkillRun) -> str: """Build a compact recall query from skill metadata.""" path_text = " ".join(run.file_paths) - return f"{run.skill_name} {run.task} {path_text}" + metadata_text = " ".join(f"{key}:{value}" for key, value in run.metadata.items()) + return f"{run.skill_name} {run.task} {path_text} {metadata_text}" + + def _tags_for(self, run: SkillRun) -> tuple[str, ...]: + """Build stable tags for memories emitted by one skill run.""" + skill_tag = run.skill_name.strip("/") + if skill_tag: + return (*self.base_tags, skill_tag) + return self.base_tags def _memory_type(self, label: str) -> str: """Map a transcript label to Memanto's memory type vocabulary.""" diff --git a/examples/claudecode-skills-memanto/tests/test_skill_memory_bridge.py b/examples/claudecode-skills-memanto/tests/test_skill_memory_bridge.py index a42aae0c..2641f0f4 100644 --- a/examples/claudecode-skills-memanto/tests/test_skill_memory_bridge.py +++ b/examples/claudecode-skills-memanto/tests/test_skill_memory_bridge.py @@ -13,7 +13,11 @@ sys.path.insert(0, str(EXAMPLE_DIR)) from memory_backends import FileMemoryBackend # noqa: E402 -from skill_memory_bridge import SkillMemoryBridge, SkillRun # noqa: E402 +from skill_memory_bridge import ( # noqa: E402 + SkillExecution, + SkillMemoryBridge, + SkillRun, +) class SkillMemoryBridgeTests(unittest.TestCase): @@ -79,6 +83,60 @@ def test_file_backend_ranks_recall_by_query_overlap(self) -> None: ], ) + def test_bridge_wraps_any_skill_executor_with_memory_hooks(self) -> None: + with tempfile.TemporaryDirectory() as tmp_dir: + memory = FileMemoryBackend(Path(tmp_dir) / "memory.json") + memory.remember( + "Decision: Invoice exports must preserve customer locale settings." + ) + bridge = SkillMemoryBridge(memory) + run = SkillRun( + skill_name="/handoff", + task="Summarize invoice export implementation", + file_paths=["apps/billing/invoices/export.ts"], + metadata={"project": "billing"}, + ) + prompts_seen: list[str] = [] + + def fake_executor(prompt: str) -> str: + prompts_seen.append(prompt) + return "Learning: Invoice exports need a locale regression test." + + result = bridge.run_with_memory( + run, + "Create a handoff note for the invoice export branch.", + fake_executor, + ) + + self.assertIsInstance(result, SkillExecution) + self.assertEqual(prompts_seen, [result.prompt]) + self.assertIn("MEMANTO ENGINEERING MEMORY", result.prompt) + self.assertIn("preserve customer locale", result.prompt) + self.assertIn("Create a handoff note", result.prompt) + self.assertEqual( + result.stored_memories, + ["Learning: Invoice exports need a locale regression test."], + ) + + records = json.loads((Path(tmp_dir) / "memory.json").read_text()) + self.assertEqual(records[-1]["tags"], "claudecode,skills,handoff") + + def test_metadata_contributes_to_recall_query(self) -> None: + with tempfile.TemporaryDirectory() as tmp_dir: + memory = FileMemoryBackend(Path(tmp_dir) / "memory.json") + memory.remember("Decision: Mobile builds use expo-router defaults.") + bridge = SkillMemoryBridge(memory) + run = SkillRun( + skill_name="/tdd", + task="Add route tests", + file_paths=[], + metadata={"framework": "expo-router"}, + ) + + context = bridge.before_skill(run) + + self.assertIn("expo-router defaults", context) + def test_malformed_offline_memory_file_recovers_on_write(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: path = Path(tmp_dir) / "memory.json" From dfece303274d9fc422832c76bb0ed0241a84bbc5 Mon Sep 17 00:00:00 2001 From: attaboy11 Date: Wed, 3 Jun 2026 19:24:06 +0100 Subject: [PATCH 7/8] Harden skills bridge memory tags --- .../skill_memory_bridge.py | 7 ++++++- .../tests/test_skill_memory_bridge.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/examples/claudecode-skills-memanto/skill_memory_bridge.py b/examples/claudecode-skills-memanto/skill_memory_bridge.py index 2e41b1d2..7ea48cc6 100644 --- a/examples/claudecode-skills-memanto/skill_memory_bridge.py +++ b/examples/claudecode-skills-memanto/skill_memory_bridge.py @@ -109,11 +109,16 @@ def _query_for(self, run: SkillRun) -> str: def _tags_for(self, run: SkillRun) -> tuple[str, ...]: """Build stable tags for memories emitted by one skill run.""" - skill_tag = run.skill_name.strip("/") + skill_tag = self._sanitize_tag(run.skill_name) if skill_tag: return (*self.base_tags, skill_tag) return self.base_tags + def _sanitize_tag(self, value: str) -> str: + """Normalize user-facing skill names into comma-safe memory tags.""" + normalized = re.sub(r"[\s,/]+", "-", value.strip()) + return normalized.strip("-") + def _memory_type(self, label: str) -> str: """Map a transcript label to Memanto's memory type vocabulary.""" normalized = label.lower() diff --git a/examples/claudecode-skills-memanto/tests/test_skill_memory_bridge.py b/examples/claudecode-skills-memanto/tests/test_skill_memory_bridge.py index 2641f0f4..e13188c0 100644 --- a/examples/claudecode-skills-memanto/tests/test_skill_memory_bridge.py +++ b/examples/claudecode-skills-memanto/tests/test_skill_memory_bridge.py @@ -137,6 +137,21 @@ def test_metadata_contributes_to_recall_query(self) -> None: self.assertIn("expo-router defaults", context) + def test_skill_name_is_sanitized_before_tag_storage(self) -> None: + with tempfile.TemporaryDirectory() as tmp_dir: + memory = FileMemoryBackend(Path(tmp_dir) / "memory.json") + bridge = SkillMemoryBridge(memory) + run = SkillRun( + skill_name="/custom skill,runner/", + task="Summarize memory bridge tag handling", + file_paths=[], + ) + + bridge.after_skill(run, "Learning: Tags should stay comma-safe.") + + records = json.loads((Path(tmp_dir) / "memory.json").read_text()) + self.assertEqual(records[0]["tags"], "claudecode,skills,custom-skill-runner") + def test_malformed_offline_memory_file_recovers_on_write(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: path = Path(tmp_dir) / "memory.json" From 652e68f1973de15b63bc49ff733c6af0217c510b Mon Sep 17 00:00:00 2001 From: attaboy11 Date: Wed, 3 Jun 2026 19:25:33 +0100 Subject: [PATCH 8/8] Sanitize skills memory tags --- .../skill_memory_bridge.py | 3 ++- .../tests/test_skill_memory_bridge.py | 13 +++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/examples/claudecode-skills-memanto/skill_memory_bridge.py b/examples/claudecode-skills-memanto/skill_memory_bridge.py index 7ea48cc6..a25e0dbf 100644 --- a/examples/claudecode-skills-memanto/skill_memory_bridge.py +++ b/examples/claudecode-skills-memanto/skill_memory_bridge.py @@ -12,6 +12,7 @@ r"^\s*(Decision|Preference|Quirk|Constraint|Learning):\s*(.+)$", flags=re.IGNORECASE | re.MULTILINE, ) +TAG_SEPARATOR = re.compile(r"[^a-z0-9]+") @dataclass @@ -116,7 +117,7 @@ def _tags_for(self, run: SkillRun) -> tuple[str, ...]: def _sanitize_tag(self, value: str) -> str: """Normalize user-facing skill names into comma-safe memory tags.""" - normalized = re.sub(r"[\s,/]+", "-", value.strip()) + normalized = TAG_SEPARATOR.sub("-", value.strip().lower()) return normalized.strip("-") def _memory_type(self, label: str) -> str: diff --git a/examples/claudecode-skills-memanto/tests/test_skill_memory_bridge.py b/examples/claudecode-skills-memanto/tests/test_skill_memory_bridge.py index e13188c0..347b86ea 100644 --- a/examples/claudecode-skills-memanto/tests/test_skill_memory_bridge.py +++ b/examples/claudecode-skills-memanto/tests/test_skill_memory_bridge.py @@ -142,15 +142,24 @@ def test_skill_name_is_sanitized_before_tag_storage(self) -> None: memory = FileMemoryBackend(Path(tmp_dir) / "memory.json") bridge = SkillMemoryBridge(memory) run = SkillRun( - skill_name="/custom skill,runner/", + skill_name="/Custom skill,runner/v2!!", task="Summarize memory bridge tag handling", file_paths=[], ) bridge.after_skill(run, "Learning: Tags should stay comma-safe.") + bridge.after_skill( + SkillRun( + skill_name=" ///,,, ", + task="Summarize empty skill tag handling", + file_paths=[], + ), + "Decision: Empty skill tags fall back to base tags.", + ) records = json.loads((Path(tmp_dir) / "memory.json").read_text()) - self.assertEqual(records[0]["tags"], "claudecode,skills,custom-skill-runner") + self.assertEqual(records[0]["tags"], "claudecode,skills,custom-skill-runner-v2") + self.assertEqual(records[1]["tags"], "claudecode,skills") def test_malformed_offline_memory_file_recovers_on_write(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: