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
+39 -39
View File
@@ -15,7 +15,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from scribe.services import retrieval_surfaces as rs
from grimoire.services import retrieval_surfaces as rs
from tests.helpers import fake_note, fake_rule, writepath_cfg
# The MCP tool layer reads its caller from a ContextVar the HTTP transport sets
@@ -82,7 +82,7 @@ def _arm_patches(pc, hits, recorder, prior_art=None, cfg=None, rule_search=None,
async def _run_arm(hits, recorder, prior_art=None, retrieval_log=None, **kwargs):
from scribe.services import plugin_context as pc
from grimoire.services import plugin_context as pc
with ExitStack() as stack:
for ctx in _arm_patches(pc, hits, recorder, prior_art,
retrieval_log=retrieval_log):
@@ -148,7 +148,7 @@ async def test_the_arm_searches_on_its_OWN_bar_not_the_code_one():
The two values are deliberately different here so the assertion can tell
them apart.
"""
from scribe.services import plugin_context as pc
from grimoire.services import plugin_context as pc
search = AsyncMock(return_value=[])
with ExitStack() as stack:
@@ -217,12 +217,12 @@ async def test_mcp_get_rule_records_an_agent_pull():
get_rule(N)", so this is the exact action a landed hint produces."""
rec = MagicMock()
rule = fake_rule(id=156, title="A wait with no deadline is a bug")
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.get_rule",
with patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.get_rule",
AsyncMock(return_value=rule)), \
patch("scribe.mcp.tools.rulebooks.rulebooks_svc.rule_detail",
patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.rule_detail",
AsyncMock(return_value={"id": 156})), \
patch("scribe.mcp.tools.rulebooks.record_rule_pulled", rec):
from scribe.mcp.tools.rulebooks import get_rule
patch("grimoire.mcp.tools.rulebooks.record_rule_pulled", rec):
from grimoire.mcp.tools.rulebooks import get_rule
await get_rule(rule_id=156)
assert rec.call_args.kwargs["rule_id"] == 156
@@ -234,10 +234,10 @@ async def test_a_rule_that_cannot_be_read_is_not_a_pull():
"""Recorded after the access check. A refused read is not a pull, and
counting it would credit the arm for a hint nobody could open."""
rec = MagicMock()
with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.get_rule",
with patch("grimoire.mcp.tools.rulebooks.rulebooks_svc.get_rule",
AsyncMock(return_value=None)), \
patch("scribe.mcp.tools.rulebooks.record_rule_pulled", rec):
from scribe.mcp.tools.rulebooks import get_rule
patch("grimoire.mcp.tools.rulebooks.record_rule_pulled", rec):
from grimoire.mcp.tools.rulebooks import get_rule
with pytest.raises(ValueError):
await get_rule(rule_id=156)
@@ -260,8 +260,8 @@ def test_every_rule_detail_door_records_a_pull():
note). What it can still prove is that the handler names the recorder —
which is the thing that gets forgotten when a door is added.
"""
rest = _source_of("src/scribe/routes/rulebooks.py")
mcp = _source_of("src/scribe/mcp/tools/rulebooks.py")
rest = _source_of("src/grimoire/routes/rulebooks.py")
mcp = _source_of("src/grimoire/mcp/tools/rulebooks.py")
assert 'source="rest_rule"' in rest, (
"the REST rule-detail route does not record a pull"
)
@@ -280,8 +280,8 @@ def test_the_bulk_loaders_are_not_counted_as_pulls():
front of an agent, so "surely those are pulls too" is the reading someone
arrives at without the argument.
"""
for path in ("src/scribe/mcp/tools/rulebooks.py",
"src/scribe/mcp/tools/projects.py"):
for path in ("src/grimoire/mcp/tools/rulebooks.py",
"src/grimoire/mcp/tools/projects.py"):
src = _source_of(path)
for door in ("list_always_on_rules", "enter_project"):
if f"async def {door}" not in src:
@@ -312,7 +312,7 @@ def test_rules_payload_records_both_the_family_and_project_halves():
remembers goes un-emitted with nothing forcing its surface to be accounted
for.
"""
from scribe.services import rulebooks as svc
from grimoire.services import rulebooks as svc
rec = MagicMock()
with patch.object(svc, "record_rule_surfaced", rec):
@@ -335,7 +335,7 @@ def test_rules_payload_records_both_the_family_and_project_halves():
def test_brief_rules_payload_lists_titles_and_records_only_what_it_shows():
"""The handshake's form: project rules by id and title, nothing else. A
global rule it doesn't show must not count as surfaced."""
from scribe.services import rulebooks as svc
from grimoire.services import rulebooks as svc
rec = MagicMock()
with patch.object(svc, "record_rule_surfaced", rec):
@@ -361,7 +361,7 @@ def test_every_rules_payload_caller_names_itself():
import re
seen = set()
for path in Path("src/scribe").rglob("*.py"):
for path in Path("src/grimoire").rglob("*.py"):
for m in re.finditer(r"rules_payload\([^)]*source=\"([a-z_]+)\"", path.read_text()):
seen.add(m.group(1))
assert seen == {
@@ -398,7 +398,7 @@ def _tool_patches(pc, hits, recorder, cfg=None, retrieval_log=None):
async def _run_tool_arm(hits, recorder, command="curl -s https://git.example/api/v1/runs",
tool="Bash", retrieval_log=None, **kwargs):
from scribe.services import plugin_context as pc
from grimoire.services import plugin_context as pc
with ExitStack() as stack:
for ctx in _tool_patches(pc, hits, recorder, retrieval_log=retrieval_log):
stack.enter_context(ctx)
@@ -432,7 +432,7 @@ def _prompt_patches(pc, hits, recorder, retrieval_log=None):
async def _run_prompt_arm(hits, recorder, prompt="please merge to main",
retrieval_log=None, **kwargs):
from scribe.services import plugin_context as pc
from grimoire.services import plugin_context as pc
with ExitStack() as stack:
for ctx in _prompt_patches(pc, hits, recorder, retrieval_log=retrieval_log):
stack.enter_context(ctx)
@@ -455,7 +455,7 @@ async def test_the_prompt_arm_retrieves_against_what_the_operator_SAID():
id=2, title="`main` — never without explicit request",
when_to_apply="opening or merging a dev→main pull request",
))])
from scribe.services import plugin_context as pc
from grimoire.services import plugin_context as pc
with ExitStack() as stack:
stack.enter_context(patch.object(rs, "get_setting", AsyncMock(return_value="0.6")))
stack.enter_context(patch.object(pc, "semantic_search_rules", search))
@@ -489,7 +489,7 @@ async def test_the_prompt_arm_says_nothing_when_asked_nothing():
retrieval_logs that no operator action produced."""
search = AsyncMock(return_value=[])
log = MagicMock()
from scribe.services import plugin_context as pc
from grimoire.services import plugin_context as pc
with ExitStack() as stack:
stack.enter_context(patch.object(rs, "get_setting", AsyncMock(return_value="0.6")))
stack.enter_context(patch.object(pc, "semantic_search_rules", search))
@@ -547,7 +547,7 @@ async def test_turning_off_the_notes_menu_does_not_turn_off_rules():
structural fact rather than a simulation of the setting. A future refactor
that reaches for that config here fails, whatever it then does with it.
"""
from scribe.services import plugin_context as pc
from grimoire.services import plugin_context as pc
notes_cfg = AsyncMock(return_value={
"enabled": False, "threshold": 0.55, "top_k": 3,
@@ -591,7 +591,7 @@ async def test_the_tool_arm_is_a_ranked_source():
"""It CHOSE what it showed, so a pull can settle whether the choice was any
good — unlike a preload, which chose nothing. If this drifts into the
ambient class the arm becomes unjudgeable."""
from scribe.services.rule_usage import is_ambient
from grimoire.services.rule_usage import is_ambient
assert not is_ambient("pre_tool_rule")
@@ -636,7 +636,7 @@ async def test_a_rule_the_session_already_holds_is_referenced_not_re_offered():
async def test_an_empty_command_asks_the_ranker_nothing():
"""Every Bash call reaches this. A blank payload must cost no embedding
query at all, not merely return nothing after paying for one."""
from scribe.services import plugin_context as pc
from grimoire.services import plugin_context as pc
search = AsyncMock(return_value=[])
rec = MagicMock()
@@ -654,7 +654,7 @@ async def test_an_empty_command_asks_the_ranker_nothing():
async def test_the_tool_arm_fails_open():
"""A recall aid may never break the operator's action. A ranker that raises
must cost the hint, not the command."""
from scribe.services import plugin_context as pc
from grimoire.services import plugin_context as pc
with ExitStack() as stack:
stack.enter_context(patch.object(pc, "get_writepath_config",
@@ -668,7 +668,7 @@ async def test_the_tool_arm_fails_open():
async def test_a_long_command_is_bounded_before_it_reaches_the_ranker():
"""A heredoc or a pasted script would push the verb and its target — the
part a rule is about — out of the embedding window."""
from scribe.services import plugin_context as pc
from grimoire.services import plugin_context as pc
search = AsyncMock(return_value=[])
with ExitStack() as stack:
@@ -727,7 +727,7 @@ def test_the_hook_and_the_route_agree_on_every_parameter_name():
hook = Path("plugin/hooks/scribe_tool_rules.sh").read_text()
defs = Path("plugin/hooks/scribe_defs.sh").read_text()
route = Path("src/scribe/routes/plugin.py").read_text()
route = Path("src/grimoire/routes/plugin.py").read_text()
handler = route.split("async def pre_tool_rules")[1].split("\n@plugin_bp")[0]
sent = set(re.findall(r"[?&]([a-z_]+)=", hook))
@@ -863,7 +863,7 @@ async def test_a_command_the_arm_never_searched_writes_no_row_at_all():
A row here would report a call that never happened and drag the clear-rate
down with phantom declines — the mirror of the defect, from the other side.
"""
from scribe.services import plugin_context as pc
from grimoire.services import plugin_context as pc
log = MagicMock()
with ExitStack() as stack:
@@ -885,9 +885,9 @@ def test_no_rule_arm_logs_its_call_behind_a_results_guard():
alone stays behind `if rule_ids:`. And no arm writes a rule-source row of
its own any more — a fourth copy would be the defect's way back in.
"""
from scribe.services import retrieval_pipeline as rp
from grimoire.services import retrieval_pipeline as rp
src = Path("src/scribe/services/retrieval_pipeline.py").read_text()
src = Path("src/grimoire/services/retrieval_pipeline.py").read_text()
tree = ast.parse(src)
fns = {n.name: n for n in ast.walk(tree) if isinstance(n, ast.AsyncFunctionDef)}
@@ -936,7 +936,7 @@ def test_no_rule_arm_logs_its_call_behind_a_results_guard():
# 4. No arm keeps a private copy: outside the pipeline, nothing writes a
# call row under a rule arm's source.
pc_src = Path("src/scribe/services/plugin_context.py").read_text()
pc_src = Path("src/grimoire/services/plugin_context.py").read_text()
for arm in rp.RULE_ARMS:
assert f'source="{arm.source}"' not in pc_src, (
f"plugin_context writes a {arm.source} row of its own again — the "
@@ -1053,7 +1053,7 @@ def test_the_three_hit_fixture_sits_inside_the_rule_band():
retighten these scores, and says so here rather than through four
confusing failures elsewhere.
"""
from scribe.services import plugin_context as pc
from grimoire.services import plugin_context as pc
spread = _THREE_HITS[0][0] - _THREE_HITS[-1][0]
assert spread < pc._RULEHINT_BAND, (
@@ -1410,7 +1410,7 @@ def _search_by_kind(general, preference):
async def _run_slot(general, preference, recorder=None, retrieval_log=None, **kwargs):
from scribe.services import plugin_context as pc
from grimoire.services import plugin_context as pc
rec = recorder or MagicMock()
with ExitStack() as stack:
stack.enter_context(patch.object(rs, "get_setting", AsyncMock(return_value="0.6")))
@@ -1474,7 +1474,7 @@ async def test_the_slot_query_can_only_answer_with_a_preference():
place on score — a slot silently spent on the wrong kind is worse than no
slot at all.
"""
from scribe.services import plugin_context as pc
from grimoire.services import plugin_context as pc
search = _search_by_kind(_RULES_FILLING_THE_LIMIT, _PREF_HIT)
with ExitStack() as stack:
stack.enter_context(patch.object(rs, "get_setting", AsyncMock(return_value="0.6")))
@@ -1609,7 +1609,7 @@ async def test_a_preference_on_the_ledger_keeps_the_slot_and_is_not_recounted():
@pytest.mark.asyncio
async def test_each_act_arm_searches_at_its_own_bar():
"""The split, where it actually takes effect."""
from scribe.services import plugin_context as pc
from grimoire.services import plugin_context as pc
cfg = writepath_cfg(threshold=0.6, top_k=3, rule_threshold=0.77, tool_rule_threshold=0.61)
@@ -1637,7 +1637,7 @@ async def test_an_act_arm_reports_the_bar_it_actually_searched_at():
as having cleared a bar it never faced — and the reader's conclusion would
be to move the bar that was already right.
"""
from scribe.services import plugin_context as pc
from grimoire.services import plugin_context as pc
cfg = writepath_cfg(threshold=0.6, top_k=3, rule_threshold=0.77, tool_rule_threshold=0.61)
@@ -1673,7 +1673,7 @@ def test_every_hook_rule_search_says_which_project_it_is_for():
# The hook arms search through the pipeline's one
# ranked-search stage, checked below — so a direct search appearing
# here again is a copy of the arm coming back, and fails the count.
"src/scribe/services/plugin_context.py": 0,
"src/grimoire/services/plugin_context.py": 0,
}
for path, expected in sources.items():
calls = [
@@ -1704,7 +1704,7 @@ def test_every_hook_rule_search_says_which_project_it_is_for():
# arms, their slots, and the via-lesson arm's lesson search), so the
# searches are counted per function: a rule search copied anywhere else
# is a new function in this map, and fails it.
pipeline = ast.parse(Path("src/scribe/services/retrieval_pipeline.py").read_text())
pipeline = ast.parse(Path("src/grimoire/services/retrieval_pipeline.py").read_text())
by_function = {}
for fn in ast.walk(pipeline):
if isinstance(fn, ast.AsyncFunctionDef):
@@ -1741,7 +1741,7 @@ def test_every_hook_rule_search_says_which_project_it_is_for():
async def test_the_act_arms_scope_their_search_to_the_bound_project(bound, scope):
"""A bound session searches its own project; an unbound one (0) searches
global rules only, which the search spells as `project_id=None`."""
from scribe.services import plugin_context as pc
from grimoire.services import plugin_context as pc
cfg = writepath_cfg(threshold=0.6, top_k=3, rule_threshold=0.6, tool_rule_threshold=0.6)
tool_search = AsyncMock(return_value=[])