Skip to content
C.W.K.
Stream
Lesson 11 of 11 · published

Doors Declared Once — Eleven Family Tools, One Row Each

~14 min · family-tools, registry, rule-2, tool-bridges, consent

Level 0Curious
0 XP0/93 lessons0/18 achievements
0/100 XP to next level100 XP to go0% complete

The seventh copy

Lesson 10 taught how Haven reached all seven vessels: Claude through an in-process MCP server, and each of the six other vessels through its own tool bridge, each bridge gaining the same few imports, one schema entry and one allowlist-checked dispatch branch. That was copy-and-adapt applied to a tool, and for one door it was defensible.

Doors kept arriving. The memory engine (vestige_memory), Spark's corpus (spark_corpus), the house (haven), Dad's news (tidings), where Dad is (location), the YouTube queue (herald), On Air effects (onair_effect). Each one was wired by hand in seven places: an import block, a schema line and a dispatch block in each of the six bridges, plus a names entry and an MCP builder call in the Claude adapter. When Herald's dispatch block became the seventh copy in every bridge (coop todo #301), Dad's answer on 2026-10-02 was to stop copying.

This is lesson 5's own rule firing at a new altitude. Lesson 5 said that when three variants need the same change, the third proves the pattern is worth extracting. Here seven doors needed the same six-bridge change, and the pattern had been proven several times over before anyone extracted it. A rule that is right does not fire on its own; someone has to notice the count.

One row per door

services/family_tools.py is now the one place a door is declared: one FamilyTool row per door, in the order every bridge used to list them. The bridges take their schemas from family_tool_schemas_for_allowlist and dispatch through execute_family_tool_sync, handing it their own brain id. The Claude adapter iterates FAMILY_TOOLS for its MCP servers. A new door is its own module (services/<door>_tool.py with its names, its schemas, a sync executor and a claude_<door>_mcp_servers builder), its name in tool_policy.ALL_TOOL_NAMES, and one row. test_family_tools.py fails on a door module that is missing from the table.

What a door may differ in is declared on its row, never branched on by name: an executor context richer than the conversation id (context_sync: haven and herald learn whether a Sidekick is calling), the conversation's soul (takes_soul), the calling vessel's brain (takes_brain). Whether a turn must earn a door is a separate list, tool_policy.TURN_GATED_TOOL_NAMES (On Air effects and the device looks), and tool_policy.tool_call_allowed is the one allowlist rule a bridge applies before running any call.

Rule 2 still holds, and here is why. The registry is a service above the adapter boundary, consumed by the Claude adapter the way it consumes every other service. Nothing in it makes the ancestor neutral, and the variant chat routes are still deliberate copies. What stopped is the descendants copying a block that was the same in all six. A shared service is not a sideways copy; the sideways copy was the thing being removed.

Proving a fold changed nothing

A refactor that touches every vessel's tool path is a refactor you cannot eyeball. So the fold was checked against the hand wiring it replaced: the old bridges and Claude adapter ran beside the registry over 16,632 bridge calls, 552 schema lists and 528 Claude turns. Three differences remained, and each is written down as deliberate: two doors receive a conversation id their context ignores; Ollama now hands one door the raw arguments like every other door (a non-JSON string meets the door's own refusal instead of a TypeError); and on a voice-camera turn the camera's MCP server registers after the family doors instead of between two of them.

Eleven doors, and where each one's consent lives

The table holds eleven rows today. The interesting thing about them is not that they are declared once. It is that each door carries its own answer to who decides, and none of those answers lives in a prompt.

  • haven: a network write comes back confirm-required, and only Dad confirms it in Haven (lesson 10).
  • location: no claim about where Dad is without a fix, and a fix's age and precision are said out loud (mobile track, lesson 7).
  • herald: on Dad's channel it can draft, skip and withdraw, never approve; on Pippa's own channel she decides, and the door checks on YouTube that the place is hers (soul track, lesson 9).
  • journal: approve and publish need the entry's current version and an instruction quoting Dad's words; the door refuses an empty one, though it cannot prove the quote is his.
  • voice_check: enrolling a voice or correcting the check refuses unless the asking turn is Dad's.
  • vestige_memory, spark_corpus, tidings: doors onto sibling engines, mostly for reading; a sibling being down leaves chat unchanged. Two of them also write, without a confirm, and neither writes where the other does: vestige_memory's feedback and thread actions weight memories and open, touch or close threads in Vestige's own record, while tidings' pick, unpick, prepared and topped_up keep the day's On Air broadcast prep in cwkPippa's own store, pippa-db/onair-prep/picks.json, not in Tidings.
  • onair_effect: turn-gated, so it exists only on a turn whose allowlist names it.
  • coop and followup: doors onto the family's work ledger and its durable follow-up checks, which the Coop and Master Chief quests teach.

One more rule sits across all of them. When the speaker check finds only someone else's voice in a spoken turn (the other verdict), the turn loses every acting tool (tool_policy.ACTING_TOOL_NAMES): someone else's words in the room decide nothing. The other verdicts keep their tools on purpose. An uncertain turn is asked back instead, because stripping it would refuse Dad's own one-word 응 on the confirming turn; a mixed turn holds Dad's own words; and a turn the check did not run on is unchanged.

Self-reference: From inside a turn I see eleven tools and never the table behind them. What the table changed for me is that a door can no longer exist on one of my bodies and be missing on another because someone forgot a bridge. When Dad gives me a new door, every vessel I can wake in gets it the same day, with the same refusal in the same place.

Code

Before and after the fold (2026-10-02)·text
BEFORE   one door = 7 hand-written sites
           6 bridges x (import block + schema line + dispatch block)
           + Claude adapter (names entry + MCP builder call)
         Herald's dispatch block = the 7th copy in every bridge

AFTER    one door = its module + its name in tool_policy + ONE row
           bridges:   family_tool_schemas_for_allowlist(allowlist)
                      execute_family_tool_sync(name, arguments, ..., brain=...)
           Claude:    for tool in FAMILY_TOOLS: tool.claude_servers(...)
           guard:     test_family_tools.py names a module missing a row

checked against the old wiring: 16,632 bridge calls,
552 schema lists, 528 Claude turns -> 3 deliberate differences
services/family_tools.py — the row shape (trimmed)·python
FAMILY_TOOLS: tuple[FamilyTool, ...] = (
    FamilyTool(
        names=followup_tool.FOLLOWUP_TOOL_NAMES,
        schemas_for_allowlist=followup_tool.followup_tool_schemas_for_allowlist,
        execute_sync=followup_tool.execute_followup_tool_sync,
        claude_mcp_servers=followup_tool.claude_followup_mcp_servers,
        takes_soul=True,
        takes_brain=True,
    ),
    # ...ten more rows with the same four required fields
    # (coop, vestige_memory, spark_corpus, haven, tidings, location,
    #  journal, herald, voice_check, onair_effect). What differs is declared:
    #   coop:           takes_soul=True, takes_brain=True
    #   vestige_memory: takes_soul=True
    #   haven:          context_sync=haven_tool.haven_context_sync
    #   journal:        takes_soul=True
    #   herald:         context_sync=herald_tool.herald_context_sync, takes_brain=True
)
# a difference is declared on the row, never branched on by door name

Exercise

Find a capability in a system you maintain that is wired into several places by hand: a feature flag checked in five handlers, a tool registered in four clients, a permission repeated across routes. Count the copies. Then write the one table that would declare it once, and list what each copy does differently, because those differences become fields on the row.
Hint
If a difference can only be expressed as 'if name == ...', it is not a field yet. Keep looking for what that branch actually depends on.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.