rename(547): the server is grimoire — package, image, DB defaults, backups
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 44s

Step 2 of the Scribe -> Grimoire rename.

- src/scribe -> src/grimoire, and every import, patch target, module path,
  hypercorn entry point and `python -m grimoire.restore` with it; pyproject,
  uv.lock and the frontend package are named grimoire.
- Image git.fabledsword.com/bvandeusen/fabledgrimoire; CI's Postgres service
  runs as grimoire / grimoire_test.
- Compose files default the DB user, database and (prod) volume to grimoire,
  each overridable from .env, so an install created as Scribe keeps its data
  by setting POSTGRES_USER, POSTGRES_DB and PGDATA_VOLUME.
- Backups are written as grimoire-<ts>.tar with format grimoire-backup-tar.
  Archives named scribe-* with the scribe-backup-tar manifest still restore
  and are still counted at the destination (folder and S3), with tests.
- Exports, the MCP auth realm, the ASGI scope key and the persistent logger
  carry the new name.

Kept on purpose: the secret-sealing key label stays "scribe.secrets.v1",
because every stored secret was derived under it.

Plugin identity (plugin name, hooks, env vars, .scribe marker, skills) is
step 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 10:50:45 -04:00
co-authored by Claude Opus 5.5
parent e99f1169f7
commit 0f5ef0be63
447 changed files with 3611 additions and 3558 deletions
+24 -24
View File
@@ -5,8 +5,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from scribe.mcp._context import _user_id_ctx
from scribe.mcp.tools.search import search
from grimoire.mcp._context import _user_id_ctx
from grimoire.mcp.tools.search import search
from tests.helpers import fake_note
@@ -29,7 +29,7 @@ async def test_fable_search_returns_repackaged_results():
_user_id_ctx.set(7)
fake = fake_note(id=1, title="kafka rebalance", is_task=False, body="HPA details", tags=["ops"])
with patch(
"scribe.mcp.tools.search.semantic_search_notes",
"grimoire.mcp.tools.search.semantic_search_notes",
AsyncMock(return_value=[(0.93, fake)]),
):
out = await search(q="kafka")
@@ -65,7 +65,7 @@ async def test_search_shows_the_passage_that_matched_not_the_opening():
}
return [(0.5, fake)]
with patch("scribe.mcp.tools.search.semantic_search_notes", _search):
with patch("grimoire.mcp.tools.search.semantic_search_notes", _search):
out = await search(q="answer")
r = out["results"][0]
assert r["excerpt"] == "THE ANSWER IS 04775c3."
@@ -83,7 +83,7 @@ async def test_a_result_says_whether_its_excerpt_is_the_match_or_the_opening():
_user_id_ctx.set(7)
fake = fake_note(id=1, title="t", body="short body")
with patch(
"scribe.mcp.tools.search.semantic_search_notes",
"grimoire.mcp.tools.search.semantic_search_notes",
AsyncMock(return_value=[(0.5, fake)]),
):
out = await search(q="x")
@@ -98,7 +98,7 @@ async def test_a_long_excerpt_keeps_both_ends_and_says_how_much_went():
body = "OPENING. " + ("m" * 3000) + " CLOSING."
fake = fake_note(id=1, title="t", body=body)
with patch(
"scribe.mcp.tools.search.semantic_search_notes",
"grimoire.mcp.tools.search.semantic_search_notes",
AsyncMock(return_value=[(0.5, fake)]),
):
out = await search(q="x")
@@ -115,7 +115,7 @@ async def test_a_short_record_arrives_whole_and_unmarked():
_user_id_ctx.set(7)
fake = fake_note(id=1, title="t", body="all of it")
with patch(
"scribe.mcp.tools.search.semantic_search_notes",
"grimoire.mcp.tools.search.semantic_search_notes",
AsyncMock(return_value=[(0.5, fake)]),
):
out = await search(q="x")
@@ -134,7 +134,7 @@ async def test_fable_search_content_type_filters_at_service_layer():
that already asks this way."""
_user_id_ctx.set(7)
mock_search = AsyncMock(return_value=[])
with patch("scribe.mcp.tools.search.semantic_search_notes", mock_search):
with patch("grimoire.mcp.tools.search.semantic_search_notes", mock_search):
await search(q="x", content_type="task")
assert mock_search.call_args.kwargs["is_task"] is True
assert mock_search.call_args.kwargs.get("task_kind") is None
@@ -173,7 +173,7 @@ async def test_each_specific_kind_reaches_the_engine_filter_it_names(
for a snippet must narrow to snippets, not merely to non-tasks."""
_user_id_ctx.set(7)
mock_search = AsyncMock(return_value=[])
with patch("scribe.mcp.tools.search.semantic_search_notes", mock_search):
with patch("grimoire.mcp.tools.search.semantic_search_notes", mock_search):
await search(q="x", content_type=content_type)
kwargs = mock_search.call_args.kwargs
for key, value in expected.items():
@@ -183,7 +183,7 @@ async def test_each_specific_kind_reaches_the_engine_filter_it_names(
def test_the_vocabulary_is_derived_from_the_facet_table_not_recopied():
"""Adding a kind to `_FACETS` is one edit, at this door too. A hand-kept
list here lets the agent's search offer fewer kinds than the web's."""
from scribe.services.knowledge import FACET_TYPES, content_type_filters
from grimoire.services.knowledge import FACET_TYPES, content_type_filters
for facet in FACET_TYPES:
filters = content_type_filters(facet) # raises if a kind is unreachable
@@ -203,7 +203,7 @@ async def test_an_unknown_content_type_is_refused_rather_than_returning_nothing(
instead of falling through to a filter that matches no row."""
_user_id_ctx.set(7)
mock_search = AsyncMock(return_value=[])
with patch("scribe.mcp.tools.search.semantic_search_notes", mock_search):
with patch("grimoire.mcp.tools.search.semantic_search_notes", mock_search):
with pytest.raises(ValueError) as err:
await search(q="x", content_type="snippets") # plural typo
mock_search.assert_not_awaited()
@@ -216,8 +216,8 @@ async def test_an_unknown_content_type_is_refused_rather_than_returning_nothing(
async def test_the_docstring_names_every_kind_the_tool_accepts():
"""The docstring IS the agent-facing contract — a filter an agent
has not been told about is unreachable however well it is wired."""
from scribe.mcp.tools.search import search as search_tool
from scribe.services.knowledge import FACET_TYPES
from grimoire.mcp.tools.search import search as search_tool
from grimoire.services.knowledge import FACET_TYPES
doc = search_tool.__doc__ or ""
for facet in FACET_TYPES:
@@ -242,7 +242,7 @@ async def test_an_explicit_search_reaches_a_lesson_from_any_project():
"""
_user_id_ctx.set(7)
mock_search = AsyncMock(return_value=[])
with patch("scribe.mcp.tools.search.semantic_search_notes", mock_search):
with patch("grimoire.mcp.tools.search.semantic_search_notes", mock_search):
await search(q="x", project_id=3)
assert mock_search.call_args.kwargs["include_global_kinds"] is True
@@ -252,7 +252,7 @@ async def test_an_explicit_search_reaches_a_lesson_from_any_project():
async def test_fable_search_limit_is_clamped():
_user_id_ctx.set(7)
mock_search = AsyncMock(return_value=[])
with patch("scribe.mcp.tools.search.semantic_search_notes", mock_search):
with patch("grimoire.mcp.tools.search.semantic_search_notes", mock_search):
await search(q="x", limit=999)
assert mock_search.call_args.kwargs["limit"] == 50
@@ -273,7 +273,7 @@ async def test_rule_search_scopes_to_the_project_it_is_given(project_id, scope):
is asking the whole rulebook — unlike a hook, which speaks unasked."""
_user_id_ctx.set(7)
found = AsyncMock(return_value=[])
with patch("scribe.mcp.tools.search.semantic_search_rules", found):
with patch("grimoire.mcp.tools.search.semantic_search_rules", found):
await search(q="release tagging", content_type="rule", project_id=project_id)
kwargs = found.await_args.kwargs
assert {k: kwargs[k] for k in scope} == scope
@@ -281,7 +281,7 @@ async def test_rule_search_scopes_to_the_project_it_is_given(project_id, scope):
def test_a_milestone_is_embedded_by_what_it_is_for_then_its_plan():
from scribe.services.embeddings import milestone_document
from grimoire.services.embeddings import milestone_document
assert milestone_document("M3", "metadata providers", "## Goal\nx") == (
"M3 — metadata providers", "metadata providers\n\n## Goal\nx")
@@ -313,9 +313,9 @@ async def test_milestone_search_is_its_own_shape_and_scopes_to_the_project():
}
return [(0.81, ms)]
with patch("scribe.mcp.tools.search.semantic_search_milestones",
with patch("grimoire.mcp.tools.search.semantic_search_milestones",
_milestone_search), \
patch("scribe.services.milestones.get_project_milestone_summary", summary):
patch("grimoire.services.milestones.get_project_milestone_summary", summary):
out = await search(q="book metadata", content_type="milestone", project_id=30)
assert seen["project_id"] == 30
assert out["results"] == [{
@@ -356,7 +356,7 @@ async def test_system_search_is_its_own_shape_and_carries_the_matched_passage():
}
return [(0.81, fake)]
with patch("scribe.mcp.tools.search.semantic_search_systems", _system_search):
with patch("grimoire.mcp.tools.search.semantic_search_systems", _system_search):
out = await search(q="where does retrieval telemetry go?",
content_type="system", project_id=2)
@@ -383,12 +383,12 @@ async def test_system_search_scopes_to_the_project_it_was_given():
answer to "where does this belong here?"."""
_user_id_ctx.set(7)
mock = AsyncMock(return_value=[])
with patch("scribe.mcp.tools.search.semantic_search_systems", mock):
with patch("grimoire.mcp.tools.search.semantic_search_systems", mock):
await search(q="x", content_type="system", project_id=30)
assert mock.call_args.kwargs["project_id"] == 30
mock.reset_mock()
with patch("scribe.mcp.tools.search.semantic_search_systems", mock):
with patch("grimoire.mcp.tools.search.semantic_search_systems", mock):
await search(q="x", content_type="system")
assert mock.call_args.kwargs["project_id"] is None
@@ -400,8 +400,8 @@ async def test_a_system_search_does_not_go_through_the_note_search():
_user_id_ctx.set(7)
notes = AsyncMock(return_value=[])
with (
patch("scribe.mcp.tools.search.semantic_search_notes", notes),
patch("scribe.mcp.tools.search.semantic_search_systems", AsyncMock(return_value=[])),
patch("grimoire.mcp.tools.search.semantic_search_notes", notes),
patch("grimoire.mcp.tools.search.semantic_search_systems", AsyncMock(return_value=[])),
):
await search(q="x", content_type="system")
notes.assert_not_awaited()