CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m2s
CI & Build / Python tests (push) Failing after 1m26s
CI & Build / Build & push image (push) Skipped
Step 3 of the Scribe -> Grimoire rename. - Plugin `grimoire` in marketplace `grimoire-plugin`, MCP server key `grimoire`, so tools reach a session as mcp__plugin_grimoire_grimoire__*. - hooks/scribe_* -> hooks/grimoire_*, their shell functions and the TMPDIR ledgers with them; SCRIBE_URL / SCRIBE_TOKEN / SCRIBE_HOOK_DIR / SCRIBE_LEDGER_DIRS -> GRIMOIRE_*; the project marker is `.grimoire`. - skills/using-scribe -> using-grimoire, /grimoire:sync, stored processes arrive as grimoire-proc-<slug>; moment_actions names them the same way. - Migration 0124 rewrites stored skill names (rule_moments, rule_moment_judgments, moment_mappings) from scribe:/scribe-proc- to grimoire:/grimoire-proc-, so a rule mounted on a skill keeps firing. - Settings' install commands and MCP-only defaults, the docs and the tests follow. - scripts/check_plugin.py drops the smoke event for a report-check hook that has no file in plugin/hooks. Kept on purpose: the secret-sealing label, and the names that let backups written as Scribe restore and be counted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
4.4 KiB
Python
100 lines
4.4 KiB
Python
"""How family canon reaches a session, without a
|
|
database: the line enter_project carries, the skill and its triggers, and the
|
|
pointers every other surface gives to it. Retrieval reach, the readout's
|
|
counts and the owed answers against Postgres are in
|
|
tests/test_integration_family_reach.py.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pathlib
|
|
import re
|
|
|
|
from grimoire.services import moment_actions
|
|
from grimoire.services.family_adoption import family_line
|
|
from tests.helpers import skill_text, tool_doc
|
|
|
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _cell(status="unassessed", *, in_scope=True, needs_recheck=False):
|
|
return {"status": status, "in_scope": in_scope, "needs_recheck": needs_recheck}
|
|
|
|
|
|
# --- the enter_project line ----------------------------------------------------------
|
|
|
|
def test_nothing_to_answer_is_no_line():
|
|
assert family_line(5, []) is None
|
|
assert family_line(5, [_cell("adopted"), _cell("exempt")]) is None
|
|
|
|
|
|
def test_the_line_counts_each_ask_and_names_the_call_that_lists_it():
|
|
line = family_line(5, [
|
|
_cell(), _cell(), _cell("owed"), _cell("adopted", needs_recheck=True),
|
|
])
|
|
assert (line["unassessed"], line["owed"], line["needs_recheck"]) == (2, 1, 1)
|
|
assert line["line"].endswith("2 unassessed · 1 owed · 1 to recheck")
|
|
assert line["calls"] == {
|
|
"unassessed": 'list_family_adoptions(project_id=5, status="unassessed")',
|
|
"owed": 'list_family_adoptions(project_id=5, status="owed")',
|
|
"needs_recheck": "list_family_adoptions(project_id=5, needs_recheck=true)",
|
|
}
|
|
assert "family-canon" in line["answer_with"]
|
|
|
|
|
|
def test_a_count_of_zero_names_no_call():
|
|
line = family_line(5, [_cell("owed")])
|
|
assert set(line["calls"]) == {"owed"} and "unassessed" not in line["line"]
|
|
|
|
|
|
def test_an_answer_kept_as_history_off_the_platform_is_not_asked_again():
|
|
"""A row whose project left the idea's platforms stays as history; an
|
|
unassessed one there is not something this project is asked for."""
|
|
assert family_line(5, [_cell(in_scope=False)]) is None
|
|
|
|
|
|
# --- the skill ----------------------------------------------------------------------
|
|
|
|
def _frontmatter(name: str) -> str:
|
|
text = (ROOT / "plugin" / "skills" / name / "SKILL.md").read_text()
|
|
return re.match(r"\A---\n(.*?)\n---\n", text, re.S).group(1)
|
|
|
|
|
|
def test_the_family_canon_skill_ships_and_triggers_on_what_the_server_sends():
|
|
"""Its description is what makes a session load it, so it names every
|
|
key the server hands back about family canon."""
|
|
front = _frontmatter("family-canon")
|
|
assert "name: family-canon" in front
|
|
for trigger in ("family_hint", "`family`", "family_owed", "enter_project"):
|
|
assert trigger in front, trigger
|
|
assert moment_actions.BUNDLED_SKILL_MOMENTS["family-canon"] == ("work.record",)
|
|
|
|
|
|
def test_the_skill_carries_the_reflexes_and_defers_the_lists_to_the_tools():
|
|
text = " ".join(skill_text("family-canon").split())
|
|
for phrase in ("one criterion with no support vetoes it",
|
|
"in the order `assess_family_adoption` lists them",
|
|
"every ground above it must be said not to apply",
|
|
"What counts as a reason", "The precedent reflex"):
|
|
assert phrase in text, phrase
|
|
|
|
|
|
def test_the_plugin_names_the_skill_it_ships():
|
|
manifest = (ROOT / "plugin" / ".claude-plugin" / "plugin.json").read_text()
|
|
static = (ROOT / "plugin" / "hooks" / "grimoire_static_context.md").read_text()
|
|
assert "family-canon" in manifest and "family-canon" in " ".join(static.split())
|
|
|
|
|
|
# --- the pointers ---------------------------------------------------------------------
|
|
|
|
def test_every_surface_that_meets_family_canon_points_at_the_skill():
|
|
server = (ROOT / "src" / "grimoire" / "mcp" / "server.py").read_text()
|
|
index = re.search(r'_INSTRUCTIONS = """(.*?)"""', server, re.S).group(1)
|
|
assert "family-canon" in index and "`family`" in index
|
|
for module, tool in (("notes", "create_note"), ("snippets", "create_snippet"),
|
|
("shapes", "classify_shapes")):
|
|
assert "family-canon" in tool_doc(f"grimoire.mcp.tools.{module}", tool), tool
|
|
assert "family_hint" in tool_doc("grimoire.mcp.tools.notes", "create_note")
|
|
assert "`family`" in tool_doc("grimoire.mcp.tools.projects", "enter_project")
|
|
assert "family_owed" in tool_doc("grimoire.mcp.tools.tasks", "update_task")
|
|
assert "family_owed" in skill_text("reporting-back")
|