feat(family): family canon reaches the session - entry readout, retrieval reach, skill, report cue (milestone 463 step 6, #4992)
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Failing after 1m53s
CI & Build / Python tests (push) Successful in 2m39s
CI & Build / Build & push image (push) Skipped

- enter_project carries a `family` key, but only when the project has
  something to answer: counts of unassessed, owed and to-recheck answers,
  each with the list_family_adoptions call that lists it. It shows on every
  entry, never by platform touch: entry is when work is chosen, and an
  unanswered idea is otherwise invisible.
- Retrieval: a widened project search (include_global_kinds) now also
  reaches the canon ideas on the project's platforms. It also reaches their
  references in the project's languages, or all of them when none matches.
  An off-platform project gets none, and the plain project filter (the
  duplicate gate) is unchanged.
- Closing a task returns `family_owed`, the owed answers filed while it was
  open, and the report cue asks for them to be named.
- New plugin skill family-canon (moment work.record) covers when to
  evaluate a promotion, answering in order, what counts as a reason, the
  precedent reflex and the conflict order. _INSTRUCTIONS, create_note,
  create_snippet, classify_shapes and reporting-back point at it.
  The plugin version is minted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 13:27:39 -04:00
co-authored by Claude Opus 5.5
parent 5f41dbd283
commit 07e21c7ff9
19 changed files with 637 additions and 19 deletions
+41
View File
@@ -1,4 +1,5 @@
"""Tests for fable_*_project tools."""
import contextlib
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@@ -48,6 +49,16 @@ def _no_coverage():
yield
@pytest.fixture(autouse=True)
def _no_family():
"""enter_project reads the family-canon readout (milestone 463 step 6) —
no database here, so the common case is stubbed: nothing to answer. The
populated key is asserted in its own test below."""
with patch("scribe.mcp.tools.projects.family_adoption_svc.family_readout",
AsyncMock(return_value=None)) as mock:
yield mock
@pytest.fixture(autouse=True)
def _no_background_seed():
"""enter_project now fire-and-forgets a coverage self-seed (#2802). These
@@ -483,3 +494,33 @@ def test_inception_routes_and_tool_are_registered():
# Rules left inception with subscriptions (milestone 414).
assert "subscribe_rulebooks" not in tool.parameters.get("properties", {})
@pytest.mark.asyncio
async def test_enter_project_carries_the_family_line_only_when_it_asks_something(_no_family):
"""The `family` key is attached when the project has family canon to
answer, and absent otherwise — a key that usually says null trains
readers to skip it (#2483)."""
line = {"line": "family canon on this project's platforms: 2 unassessed",
"unassessed": 2, "owed": 0, "needs_recheck": 0,
"calls": {"unassessed": 'list_family_adoptions(project_id=5, status="unassessed")'}}
async def enter():
with contextlib.ExitStack() as stack:
for cm in _enter_project_stubs(fake_project(id=5)):
stack.enter_context(cm)
return await enter_project(project_id=5)
assert "family" not in await enter()
_no_family.return_value = line
assert (await enter())["family"] == line
@pytest.mark.asyncio
async def test_a_failing_family_readout_never_fails_the_handshake(_no_family):
_no_family.side_effect = RuntimeError("database down")
with contextlib.ExitStack() as stack:
for cm in _enter_project_stubs(fake_project(id=5)):
stack.enter_context(cm)
out = await enter_project(project_id=5)
assert "family" not in out and out["project"]["id"] == 5