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
+33 -33
View File
@@ -13,16 +13,16 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from scribe.mcp.tools.milestones import get_milestone
from scribe.mcp.tools.tasks import (
from grimoire.mcp.tools.milestones import get_milestone
from grimoire.mcp.tools.tasks import (
elide, get_task, list_tasks, work_log_payload,
)
from scribe.services.notes import brief_row
from grimoire.services.notes import brief_row
# Bound at IMPORT time, which is before any fixture runs — so these names
# keep pointing at the real implementations even though conftest's autouse
# _no_task_log_arm replaces the module attributes for every test. Reaching
# them as `task_logs.logs_for_task` would get the stub and test nothing.
from scribe.services.task_logs import (
from grimoire.services.task_logs import (
count_logs_for_task,
log_counts_for_tasks,
logs_for_task,
@@ -54,11 +54,11 @@ def fake_log(log_id: int = 1, content: str = "did the thing", **attrs):
def _live_arm(logs=(), total=0, counts=None):
"""Re-patch conftest's autouse stub so the arm under test is live."""
return (
patch("scribe.services.task_logs.logs_for_task",
patch("grimoire.services.task_logs.logs_for_task",
AsyncMock(return_value=list(logs))),
patch("scribe.services.task_logs.count_logs_for_task",
patch("grimoire.services.task_logs.count_logs_for_task",
AsyncMock(return_value=total)),
patch("scribe.services.task_logs.log_counts_for_tasks",
patch("grimoire.services.task_logs.log_counts_for_tasks",
AsyncMock(return_value=counts or {})),
)
@@ -168,7 +168,7 @@ async def test_get_task_returns_the_work_log():
fake = fake_task(id=4208, title="stale body", parent_id=None)
p1, p2, p3 = _live_arm(logs=[fake_log(content="partially shipped in 04775c3")],
total=1)
with patch("scribe.mcp.tools.tasks.notes_svc.get_note_for_user",
with patch("grimoire.mcp.tools.tasks.notes_svc.get_note_for_user",
AsyncMock(return_value=(fake, "owner"))), p1, p2, p3:
out = await get_task(task_id=4208)
assert "work_log" in out
@@ -180,7 +180,7 @@ async def test_get_task_returns_the_work_log():
async def test_get_task_says_a_task_has_no_log_rather_than_omitting_the_key():
fake = fake_task(id=1, parent_id=None)
p1, p2, p3 = _live_arm(logs=[], total=0)
with patch("scribe.mcp.tools.tasks.notes_svc.get_note_for_user",
with patch("grimoire.mcp.tools.tasks.notes_svc.get_note_for_user",
AsyncMock(return_value=(fake, "owner"))), p1, p2, p3:
out = await get_task(task_id=1)
assert out["work_log"] == {"total": 0, "entries": []}
@@ -191,7 +191,7 @@ async def test_get_task_counts_every_entry_not_just_the_window():
fake = fake_task(id=1, parent_id=None)
window = [fake_log(i) for i in (9, 8, 7)]
p1, p2, p3 = _live_arm(logs=window, total=9)
with patch("scribe.mcp.tools.tasks.notes_svc.get_note_for_user",
with patch("grimoire.mcp.tools.tasks.notes_svc.get_note_for_user",
AsyncMock(return_value=(fake, "owner"))), p1, p2, p3:
out = await get_task(task_id=1)
assert out["work_log"]["total"] == 9
@@ -207,10 +207,10 @@ async def test_get_task_default_window_is_three_entries():
limit_seen["limit"] = limit
return []
with patch("scribe.mcp.tools.tasks.notes_svc.get_note_for_user",
with patch("grimoire.mcp.tools.tasks.notes_svc.get_note_for_user",
AsyncMock(return_value=(fake, "owner"))), \
patch("scribe.services.task_logs.logs_for_task", _capture_limit), \
patch("scribe.services.task_logs.count_logs_for_task",
patch("grimoire.services.task_logs.logs_for_task", _capture_limit), \
patch("grimoire.services.task_logs.count_logs_for_task",
AsyncMock(return_value=0)):
await get_task(task_id=1)
assert limit_seen["limit"] == 3
@@ -223,9 +223,9 @@ async def test_log_limit_zero_asks_for_every_entry_and_skips_the_count_query():
fake = fake_task(id=1, parent_id=None)
counter = AsyncMock(return_value=99)
p1, _, p3 = _live_arm(logs=[fake_log(1), fake_log(2)])
with patch("scribe.mcp.tools.tasks.notes_svc.get_note_for_user",
with patch("grimoire.mcp.tools.tasks.notes_svc.get_note_for_user",
AsyncMock(return_value=(fake, "owner"))), \
p1, p3, patch("scribe.services.task_logs.count_logs_for_task", counter):
p1, p3, patch("grimoire.services.task_logs.count_logs_for_task", counter):
out = await get_task(task_id=1, log_limit=0)
assert out["work_log"]["total"] == 2
assert counter.await_count == 0
@@ -249,7 +249,7 @@ async def test_get_task_docstring_tells_a_reader_the_log_outranks_the_body():
async def test_list_tasks_rows_carry_a_zero_filled_log_count():
rows = [fake_task(id=1, milestone_id=None), fake_task(id=2, milestone_id=None)]
_, _, p3 = _live_arm(counts={1: 4})
with patch("scribe.mcp.tools.tasks.notes_svc.list_notes",
with patch("grimoire.mcp.tools.tasks.notes_svc.list_notes",
AsyncMock(return_value=(rows, 2))), p3:
out = await list_tasks()
assert out["tasks"][0]["log_count"] == 4
@@ -262,9 +262,9 @@ async def test_list_tasks_rows_carry_a_zero_filled_log_count():
async def test_list_tasks_counts_the_page_in_one_query():
rows = [fake_task(id=i, milestone_id=None) for i in range(1, 6)]
counter = AsyncMock(return_value={})
with patch("scribe.mcp.tools.tasks.notes_svc.list_notes",
with patch("grimoire.mcp.tools.tasks.notes_svc.list_notes",
AsyncMock(return_value=(rows, 5))), \
patch("scribe.services.task_logs.log_counts_for_tasks", counter):
patch("grimoire.services.task_logs.log_counts_for_tasks", counter):
await list_tasks()
assert counter.await_count == 1
assert sorted(counter.await_args.args[1]) == [1, 2, 3, 4, 5]
@@ -279,17 +279,17 @@ async def test_get_milestone_steps_carry_a_log_count():
steps = [fake_task(id=3735, milestone_id=385),
fake_task(id=3736, milestone_id=385)]
counter = AsyncMock(return_value={3735: 2})
with patch("scribe.mcp.tools.milestones.milestones_svc.get_milestone",
with patch("grimoire.mcp.tools.milestones.milestones_svc.get_milestone",
AsyncMock(return_value=milestone)), \
patch("scribe.mcp.tools.milestones.milestones_svc.get_milestone_progress",
patch("grimoire.mcp.tools.milestones.milestones_svc.get_milestone_progress",
AsyncMock(return_value={})), \
patch("scribe.mcp.tools.milestones.notes_svc.list_notes",
patch("grimoire.mcp.tools.milestones.notes_svc.list_notes",
AsyncMock(return_value=(steps, 2))), \
patch("scribe.mcp.tools.milestones.rulebooks_svc.get_applicable_rules",
patch("grimoire.mcp.tools.milestones.rulebooks_svc.get_applicable_rules",
AsyncMock(return_value=[])), \
patch("scribe.mcp.tools.milestones.rulebooks_svc.rules_payload",
patch("grimoire.mcp.tools.milestones.rulebooks_svc.rules_payload",
MagicMock(return_value={})), \
patch("scribe.services.task_logs.log_counts_for_tasks", counter):
patch("grimoire.services.task_logs.log_counts_for_tasks", counter):
out = await get_milestone(milestone_id=385)
assert out["steps"][0]["log_count"] == 2
assert out["steps"][1]["log_count"] == 0
@@ -315,9 +315,9 @@ async def test_logs_for_task_is_not_filtered_by_who_wrote_the_entry():
session.execute = AsyncMock(return_value=MagicMock(
scalars=MagicMock(return_value=MagicMock(all=MagicMock(return_value=[])))
))
with patch("scribe.services.task_logs.can_read_note",
with patch("grimoire.services.task_logs.can_read_note",
AsyncMock(return_value=True)), \
patch("scribe.services.task_logs.async_session",
patch("grimoire.services.task_logs.async_session",
MagicMock(return_value=session)):
await logs_for_task(7, 42)
# The WHERE clause specifically — `user_id` is a selected COLUMN of the
@@ -333,9 +333,9 @@ async def test_logs_for_task_refuses_a_task_the_caller_cannot_read():
"""Unscoped would be the mirror-image hole: access control is about
routing the question through the access layer, in both directions."""
opened = MagicMock()
with patch("scribe.services.task_logs.can_read_note",
with patch("grimoire.services.task_logs.can_read_note",
AsyncMock(return_value=False)), \
patch("scribe.services.task_logs.async_session", opened):
patch("grimoire.services.task_logs.async_session", opened):
out = await logs_for_task(7, 42)
assert out == []
assert opened.call_count == 0
@@ -344,9 +344,9 @@ async def test_logs_for_task_refuses_a_task_the_caller_cannot_read():
@pytest.mark.asyncio
async def test_count_for_an_unreadable_task_is_zero_not_a_leak():
opened = MagicMock()
with patch("scribe.services.task_logs.can_read_note",
with patch("grimoire.services.task_logs.can_read_note",
AsyncMock(return_value=False)), \
patch("scribe.services.task_logs.async_session", opened):
patch("grimoire.services.task_logs.async_session", opened):
assert await count_logs_for_task(7, 42) == 0
assert opened.call_count == 0
@@ -359,7 +359,7 @@ async def test_log_counts_for_tasks_scopes_by_readability_in_the_same_query():
session.execute = AsyncMock(return_value=MagicMock(
all=MagicMock(return_value=[(1, 3)])
))
with patch("scribe.services.task_logs.async_session",
with patch("grimoire.services.task_logs.async_session",
MagicMock(return_value=session)):
out = await log_counts_for_tasks(7, [1, 2])
assert out == {1: 3}
@@ -369,9 +369,9 @@ async def test_log_counts_for_tasks_scopes_by_readability_in_the_same_query():
@pytest.mark.asyncio
async def test_no_ids_asks_the_database_nothing():
from scribe.services import task_logs as svc
from grimoire.services import task_logs as svc
opened = MagicMock()
with patch("scribe.services.task_logs.async_session", opened):
with patch("grimoire.services.task_logs.async_session", opened):
assert await log_counts_for_tasks(7, []) == {}
assert opened.call_count == 0