lorekeeper-dev — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited lorekeeper-dev (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.
Practices and conventions for developing the Lorekeeper MCP server.
Two vector store backends (switch via LORE_VECTOR_STORE):
LanceDBEngine in lancedb_engine.py. Uses sentence-transformers directly, no Mem0 for vectors.LORE_VECTOR_STORE=chroma) — Mem0-backed ChromaDBEngine in chromadb_engine.py. Single-process only.score, confidence, soft_deleted, usage_count), all MemoryLink rows, BM25 index source. Shared by both backends.Engine factory: engine_factory.py → build_engine(). Orchestrator: MemoryService in orchestrator.py (was Orchestrator in earlier versions).
Canonical identity: lore_id UUID lives in the vector store's metadata. All app logic uses lore_id.
combined = 0.45·semantic + 0.30·keyword + 0.15·(score/10) + 0.10·log_usage_normWeights are env-configurable (LORE_W_*). Dedup threshold: 0.6·semantic + 0.4·keyword >= 0.85.
Chroma distance vs. similarity (critical): Mem0 v2 score_and_rank receives Chroma cosine _distances_ but treats them as similarities — items with distance ~0.0 get filtered out; unrelated items (distance ~1.0) score 1.0 and block all inserts. Fix: bypass mem0 pipeline — embed directly with SentenceTransformer, query the collection directly, return score = 1.0 - distance.
LanceDB: Always returns cosine distance (lower=better). normalize_score() converts to similarity: 1.0 - distance. Probe is a no-op.
`infer=False` on every `mem0.add()` call — text stored verbatim, no LLM extraction.
stdout reserved for MCP protocol — all logging to stderr via structlog.
uv run pytest # run tests
uv run pytest tests/ -x -q # fail-fast
uv run ruff check src tests # lint (Python)
uv run ruff check src tests --fix # auto-fix lint
uv run mypy src # type check (run before push; not in pre-commit hook — too slow)
npx @biomejs/biome check src/lorekeeper/dashboard/static/js/ # lint (JS)
npx @biomejs/biome check ... --write # auto-fix JS lint
uv run lorekeeper # start serverInstall once per clone via bash scripts/setup.sh. It runs:
ruff check src tests — Python lintbiome check — JS lintuv run pytest tests/ -q — test suiteSee docs/linter-decisions.md for the full rationale on rule selection.
scripts/setup.shSmart multi-agent setup. Run from the repo root:
./scripts/setup.shWhat it does:
~/.claude), Cursor (~/.cursor)LORE_DATA_DIR + LOREKEEPER_SETUP_VERSION env vars## Lorekeeper section into each agent's prompt file — version-stamped, only updates when source version changesassets/skills/ (copied, flat) and .hermes/skills/ (symlinked, with category subdirs) into each agent's skills dirVerification after running setup.sh:
lorekeeper: under mcp_servers with both LORE_DATA_DIR and LOREKEEPER_SETUP_VERSIONsoul.md (or profile soul.md) has a ## Lorekeeper section with version comment matching assets/prompts/lorekeeper-agent-prompt.md~/.hermes/skills/software-development/lorekeeper-dev symlink exists and points into repo~/.hermes/skills/ contains all user skills from assets/skills/ (as copies, not symlinks)v{M.m.m} (with v prefix) — script uses string equality for idempotencyPrompt source of truth: assets/prompts/lorekeeper-agent-prompt.md — edit this to change the Lorekeeper section injected into all agents.
Re-run after editing any skill, updating the prompt file, or adding a new agent install.
test_search_unrelated_query_scores_below_thresholdFormat: <type>/LKPR-N-short-description
Types: feature/, fix/, hotfix/, refactor/, chore/, docs/
Examples:
feature/LKPR-7-lore-init-onboarding
fix/LKPR-19-fk-constraints-link-store
chore/LKPR-13-sleep-cycle-consolidation
refactor/LKPR-4-context-budgetingRules:
mainFull convention → load the commit-convention skill.Format: [LKPR-N] type: short imperative title
| Tag | When |
|---|---|
[LKPR-N] | Work tied to a specific ticket |
[LKPR-0] | Housekeeping — chore, backlog edits, status changes, skill updates |
Types: feat, fix, docs, refactor, test, chore, perf
Examples:
[LKPR-6] feat: add iterative search with relevance cutoff
[LKPR-19] fix: enable FK constraints via PRAGMA foreign_keys=ON
[LKPR-0] chore: add LKPR-21 entity-resolution backlog ticketRules:
chore/backlog branch for backlog management, opens a PR → auto-approved → merged. No more direct pushes for anyone. # Dev
git config --local user.name "Dev"
git config --local user.email "[email protected]"
# Diana
git config --local user.name "Diana"
git config --local user.email "[email protected]"Full details → load the backlog-management skill. Brief summary below:Tickets live in backlogs/ as LKPR-N-slug.md. Status & priority tracked via GitHub Issue labels (LKPR-24 hybrid model).
During sprint (no git needed):
gh issue edit LKPR-30 --add-label "S:In-progress" --remove-label "S:Ready"
gh issue edit LKPR-30 --add-label "S:Review" --remove-label "S:In-progress"
gh issue edit LKPR-30 --add-label "S:Done" --remove-label "S:Review"Weekly sync (PM): markdown status: fields updated to match labels, committed on chore/backlog, opened as PR.
All active tickets now have corresponding GitHub Issues (LKPR-24 migration). Every non-done ticket was imported with status and priority labels. Command to find your ticket:
gh issue list --label "LKPR-N" --repo Jessinra/Lorekeeper # by number (label not needed — filter by title)
gh issue list --label "S:Ready" --repo Jessinra/Lorekeeper # ready to build
gh issue list --label "S:Proposal" --repo Jessinra/Lorekeeper # proposalsTickets live in backlogs/ as LKPR-N-slug.md. Completed → backlogs/done/. Numbering: sequential (highest+1), never fill gaps.
Picking up a ticket:
./scripts/lorekeeper-backlog.sh backlog for what's readystatus to in-progressSubmitting work: 3. Self-review: full test suite (uv run pytest) + lint (uv run ruff check src tests) + mypy (uv run mypy src) 4. Move ticket to status: review 5. Push branch + open PR via gh pr create (load github-pr-workflow skill for details) 6. Ping Jason on Telegram to review and merge
| Rule | Limit |
|---|---|
| File length | 200–400 lines typical, 800 max — split if larger |
| Function length | 50 lines max — extract helpers if longer |
| Nesting depth | 4 levels max — use early returns to flatten |
| Mutation | Avoid — prefer returning new objects; no in-place mutation of args |
These aren't style preferences — they're enforced during PM review. PRs violating the 800-line or 4-nesting limits will be sent back.
`scripts/` naming: Python scripts use snake_case.py (e.g. gh_reconcile.py, check_issues.py); shell scripts use kebab-case.sh (e.g. check-branch.sh, next-ticket-number.sh). One-shot/throwaway migrations don't belong in scripts/ long-term — delete them once the migration has run everywhere.
Every fix or feature must have:
The rule: every fix needs a test that would have caught the bug. Every feature needs tests covering the happy path + at least one edge case.
What to test:
Test naming pattern:
def test_<unit>_<scenario>_<expected_outcome>():
# e.g.
def test_search_unrelated_query_scores_below_threshold():
def test_lore_insert_duplicate_blocked_at_high_similarity():
def test_memory_engine_empty_store_returns_empty_list():Discipline:
uv run pytest locally before every push — no exceptionsgh) — Efficient OperationSee the github-app-bot-auth skill for full auth setup, token rotation, and troubleshooting.This repo uses jessinra-megumi-dev[bot] for all gh operations. The bot auth token auto-refreshes every 45 min via cron (no manual intervention needed).
Quick reference for common operations:
# Check auth
gh auth status
# PR from current branch (standard flow)
git push -u origin HEAD
gh pr create --base main --title "[LKPR-N] type: title" --body "## Summary\n\nCloses LKPR-N" --reviewer @copilot
# View PR details
gh pr view 12
gh pr diff 12
gh pr checks 12
# Inline review comments (not shown by pr view)
gh api repos/Jessinra/Lorekeeper/pulls/12/comments --jq '.[] | {path, body, line}'
# Merge
gh pr merge 12 --squash --delete-branch
# General API
gh api repos/Jessinra/Lorekeeper/pulls --jq '.[] | {number, title, state}'The token is short-lived (1hr) but auto-refreshed. If gh returns 401, run:
python3 ~/.hermes/scripts/gh-token-refresh.pyBefore opening a PR, run through this:
Correctness
Tests
uv run pytest?Code Quality
breakpoint() left inuv run ruff check src testsDocumentation
Git
[LKPR-N] type: title format (housekeeping = [LKPR-0])?<type>/LKPR-N-slug?status: review, resolved_date, root cause written?subprocess with timeout=N? → wrapped in try/except TimeoutExpired: proc.kill(); proc.wait()?subprocess.Popen with stdout=PIPE or stderr=PIPE on a long-lived process? → redirect to DEVNULL/tempfile insteadpytest hook (pytest_collection_modifyitems, etc.)? → correct signature from pytest docs?addopts in pyproject.toml? → verify uv run pytest tests/e2e/ -m e2e still collects testsorigin and PR opened via gh pr create --reviewer @copilot?Apply these when reviewing PRs or self-reviewing before pushing. Flag issues at 3 levels: 🔴 Blocker (security/correctness), 🟡 Should-fix (maintainability/performance), 🟢 Nit (style).
get_user, fetch_order), classes are nouns (MemoryService)is_valid, has_permission, can_retryurl, id, db OK; usrNm ❌)# TODO(LKPR-N): descriptionexcept: passmypy).format() or %enumerate() over range(len(...))zip() for parallel iterationwith) for file/DB/network resources — never manual .close()pathlib.Path over os.path string manipulationdef f(x=[]) ❌ → def f(x=None) ✅except: — catch specific exceptionseval(), exec(), or pickle.loads() on untrusted inputsubprocess calls use list args, never shell=True with user inputconst by default, let only when reassignment needed — never var=== always — no == loose equality?. and nullish coalescing ?? used correctlyasync/await over raw .then() chainsPromise.all() for parallel async — not sequential await in a loop.catch()-handledinnerHTML with unsanitized user content (XSS risk 🔴)eval() or new Function() with dynamic stringsArray.at(-1) over arr[arr.length - 1]pip or npm packages checked via pip audit / npm auditWhen a PR adds a new cross-cutting constraint (e.g. namespace scoping, auth checks, rate limits):
get_*, all_*, update_*, delete_*, insert_* call in scope. Apply the constraint to each, not just the "main" read path.None), non-shared means [ns, 'shared']. Verify both branches, don't collapse them to the same value.get step: get() returns None for out-of-scope IDs → reject before writing.When injecting values into YAML, JSON, shell, or any structured config format:
json.dumps(value) for YAML scalars, shlex.quote() for shell. Never bare f-string inject.:, #, ", \, newlines? Prove it doesn't corrupt the config.For every piece of frontend state that reflects backend data:
<select> options, set el.value and check if the prior value still exists.PRAGMA index_info() / PRAGMA table_info(), not string-matching sqlite_master.sql. DDL whitespace is not normalized across SQLite versions.SELECT * or unbounded findAll()print() / console.log())Things Akane (PM) will check on every review:
status: done. If you found something interesting during the fix, write it down.The bar isn't perfection — it's transparency and traceability. If something was hard, document it. If you made a judgment call, note it in the ticket or commit body.
After every set of changes:
lorekeeper-dev-self-review skill and run the Reflexion cycle (Actor → Evaluator → Reflector → repeat, max 3 iterations). Self-score each criterion 1-10. PASS requires overall ≥ 8. Do not skip this step.[LKPR-N] type: title formatorigin (GitHub): git push origin <branch>github-pr-workflow skill. In short: gh pr create --base main --title "[LKPR-N] type: title" --body "..."
gh pr edit <PR_NUMBER> --add-reviewer @copilot # separate step; may fail if login invalidImplementation plans live in docs/plans/YYYY-MM-DD_HHMMSS-<slug>.md — not .hermes/plans/. This is the project-specific override of the global plan skill default.
Skills for Lorekeeper users/clients live in assets/skills/ in this repo — one folder per skill, matching the standard skill-name/SKILL.md structure. These are distributed to users alongside the server.
Skills for the dev agent (engineering practices, internal tooling) live in .hermes/skills/ in this repo. Run scripts/setup.sh to symlink them into ~/.hermes/skills/ so they're loadable via skill_view. Do NOT copy them to assets/skills/.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.