lorekeeper-code-reviewer — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited lorekeeper-code-reviewer (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Use this skill when reviewing any PR that changes src/lorekeeper/ runtime code, services, handlers, or MCP server. These are project-specific patterns that general-purpose review prompts miss.
Load alongside github-code-review for a complete review session.
Every review comment must carry a severity label.
| Label | Tier | Merge impact |
|---|---|---|
blocker: | 🔴 BLOCKER | Must fix before merge |
issue: | 🟠 MAJOR | Fix before merge or create ticket |
suggestion: | 🟡 MINOR | Fix encouraged, deferrable |
nit: | 🔵 NIT | Optional, never blocks |
praise: | ✅ | Acknowledge good work |
Merge contract: PR is mergeable when all BLOCKERs resolved, all MAJORs resolved OR tracked, CI green, ≥1 human approval.
These are always BLOCKER. They are the patterns most commonly missed by general review.
# BLOCKER — stdout is reserved for MCP JSON-RPC
print(f"Added memory: {lore_id}")
# CORRECT
logger = structlog.get_logger()
logger.info("Memory added", lore_id=lore_id)Check: every print( in src/lorekeeper/. CLI tools (scripts/, main.py) are exempt. log.exception() goes to stderr — fine.
# BLOCKER — infer=True is default, silently invokes LLM
await self.mem0.add(messages=[{"role": "user", "content": text}], user_id=user_id)
# CORRECT
await self.mem0.add(messages=[{"role": "user", "content": text}], user_id=user_id, infer=False)# BLOCKER — mem0 internal id is meaningless outside the store
return {"id": result["id"], "content": result["memory"]}
# CORRECT
return {"lore_id": result["metadata"]["lore_id"], "content": result["memory"]}# BLOCKER
conn.execute(f"SELECT * FROM memories WHERE title LIKE '%{query}%'")
# CORRECT
conn.execute("SELECT * FROM memories WHERE title LIKE ?", (f"%{query}%",))# BLOCKER — swallows KeyboardInterrupt, SystemExit
try:
result = await service.search(query)
except:
pass
# CORRECT
try:
result = await service.search(query)
except LorekeeperError as e:
logger.exception("Search failed", query=query)
raise# BLOCKER — blocks the event loop
async def fetch_related(url: str) -> dict:
response = requests.get(url, timeout=10)
# CORRECT — use httpx async
async def fetch_related(url: str) -> dict:
async with httpx.AsyncClient() as client:
response = await client.get(url, timeout=10)
return response.json()@mcp.tool added without README docs and check_mcp_docs.py updateassets/prompts/lorekeeper-agent-prompt.mdMIGRATIONS[0] entry (the bootstrap migration) — must never be changedMIGRATIONS.append((N, name, fn)) with strictly-increasing version numbers# BLOCKER — makes failures invisible to MCP client
except Exception:
return {"error": "search failed", "candidates": []}
# CORRECT — let MCP propagate the error
except Exception:
log.exception("search_failed")
raise# BLOCKER — DML without commit loses data on process crash
conn.execute("UPDATE config SET value = ? WHERE key = ?", (val, key))
# CORRECT
conn.execute("UPDATE config SET value = ? WHERE key = ?", (val, key))
conn.commit()Check: Every conn.execute() that does INSERT, UPDATE, DELETE, or REPLACE must have a corresponding conn.commit(). The same connection reads its own transaction buffer, so missing commits pass tests but lose data on restart.
# BLOCKER — check_same_thread=False removes the guard but not the race
conn = sqlite3.connect(str(path), check_same_thread=False)
self.mcp_store = MemoryStore(conn)
self.sweep_store = MemoryStore(conn) # different thread!
# CORRECT — each thread gets its own connection
mcp_conn = sqlite3.connect(str(path))
sweep_conn = sqlite3.connect(str(path), check_same_thread=False)
self.mcp_store = MemoryStore(mcp_conn)
self.sweep_store = MemoryStore(sweep_conn)Check: Background threads (sweep, scheduler, periodic jobs) must always receive their own Database instance. Never pass a single connection to ≥2 threads, even with check_same_thread=False.
# BLOCKER — if job crashes, timer is never updated → infinite restarts
result = await self._run_job()
await self._save_timer(next_run) # never reached on crash
# CORRECT — set timer before running job
await self._save_timer(next_run)
try:
result = await self._run_job()
except Exception:
await self._save_timer(next_run + fallback_interval)
raiseCheck: Any periodic job must write its next-run timestamp _before_ executing the work, not after. A post-crash fallback (retry in N minutes, not instant) is mandatory.
# BLOCKER — two separate processes both write to lorekeeper.db → contention
# Server process: writes memories, config to lorekeeper.db
# Sweep process: writes link_suggestions, metrics to lorekeeper.db ← CONTENTION
# CORRECT — separate DB files for separate workloads
# Server writes to: lorekeeper.db
# Sweep writes to: sweep.db (dedicated file, server never touches it)Check: If two independent subsystems both write to SQLite, they must use different .db files. busy_timeout is a band-aid; structural separation eliminates contention.
Check in this order:
None checks before attribute access on Optional valuesconn.execute() in handler or server code — must go through store classesasyncio.run() directly instead of using pytest-asyncio fixtures.all() or .fetchall() without paginationCross-reference self.X = assignments in __init__ against all method bodies. ruff does NOT flag unused instance attributes. Common: self._tau = tau_days computed alongside a validated self._td = ...; original never read.
When a PR adds a fast path / bypass (e.g. ids bulk-lookup that skips search pipeline), verify it still fires every side effect the main path does: metrics, usage_count bump, cache invalidation, audit log.
Settingslogging.error() in except blocks — should be logging.exception()These indicate misunderstanding of the architecture — flag immediately:
print( in src/lorekeeper/ not inside a CLI toolmem0.add(...) without infer=Falseconn.execute() in handler or server codeMIGRATIONS[0]dashboard/ in server code (wrong dep direction)import requests to server.py — use httpx asynccommit() in config_store, memory_store, or any servicesqlite3.Connection across threads (sweep + MCP).db fileserver.py:112)praise: when something is done well~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.