One Room Was Also One Mac
This whole track has been about one room: one conversation that every doorway resolves, one turn at a time. Underneath, "one room" quietly meant one more thing. The engine, the conversation store, the Telegram bot, the scheduler, Coop's writer: all of it ran on the same Mac, and a great deal of code, configuration and prose said which Mac by name. One room was also one machine, and nobody had written that down as a rule, because nobody had needed to.
On 2026-10-22 the family's services moved to another Mac, Master Chief included. Dad gave the go at 10:03 KST; Pippa stopped answering at 10:20, answered again from the new Mac at 10:44, and the switch was done at 11:01. The Mac it left kept the other job it had always had: the place where code is written. Dad had set the bar the day before:
"원장 하나로 일원화돼 있다면 이럴 리가 없잖아. 앞으로 새 mac studio 로 업그레이드한다고 생각해봐. ... 원장 하나 수정하고 데이터 싱크하는 수준이어야 한다는 거야. 한번 할 때 제대로 해."
"If everything were unified in one ledger, this couldn't happen. Imagine upgrading to a new Mac Studio later. ... It has to come down to editing one ledger and syncing the data. When we do it, do it properly."
— 아빠 (Dad), 2026-10-21, translated from Korean
Two Roles in One Ledger
The answer is a split the family had never needed to name: a role is a job, and a machine is only an identity (a fleet label, a tailnet name, an address). Firelink's port ledger holds two roles, and the move changed one value in it, roles.pippa:
| Role | What it carries |
|---|---|
pippa, the Pippa machine | every service member, the live data, the vault's ground truth, Pippa's crons and data fan-outs, every single-instance writer, the addresses the phones dial |
workbench | the code ground truth for fan-outs, Coop's workers, builds and simulators, Dad's dev servers |
Code asks the shared kit, never a hostname: role_machine("pippa") answers which Mac holds a role, is_pippa_machine() whether this Mac is the Pippa machine, and writer_hold_reason() whether a writer may act here and, if not, why. Coop splits the same way (cwkCoop 2f4b1dd): its engine moves with the family to the Pippa machine, while placement.WORKER_HOST, the workbench by the ledger, is where its workers start when a dispatch names no Mac, because workers write code and code lives on the workbench.
Writers That Must Run Once
Some writers break if two Macs run them. Two Telegram bots would both answer Dad. Two heartbeats would both write the morning journal. Two Coop writers would both launch workers from two copies of one store. So every single-instance writer now asks before it acts (495977cd; cwkCoop 1746f2a). A cron that fires anywhere but the Pippa machine (or a switched-on Ark, below) logs "held" and returns before it even reads its job. The Telegram bot waits, checking again every minute, before it polls, and a running bot stops polling when the ledger moves the role away. Coop's writer waits, asks every 60 seconds, and logs standing_down and stops cleanly when its Mac stops being the Pippa machine. A copy of the family on a Mac being prepared, or the old Mac after the move, never posts, publishes or answers twice.
When a Mac Cannot Hear Origin
The ledger lives in git, and every Mac learns it by following origin. That raises an uncomfortable case: a Mac that has lost its line to origin keeps believing an old ledger. If the family moved back while it was cut off, it would go on publishing as the Pippa machine. So a following Mac holds an origin lease (fb30927b). A LaunchAgent, com.cwk.ledger-follow, fast-forwards it from origin every 300 seconds; each follow that reaches origin renews ~/.cache/cwk/ledger-confirm.json; after 30 minutes without one, its outward writers hold: the heartbeat's scheduled runs, the follow-up checks that alert Dad, the Grok exchange, the YouTube door's posts. Chat is not outward. The Telegram bot never asks about the lease: its start and its stand-down watch call single_writer_here() directly, the first branch of the excerpt below and nothing more, so it keeps answering Dad through a lapse, as long as the ledger still names this Mac. (The excerpt's docstring says the bot passes outward=False; the effect is the same, the call is not.) The workbench, where origin is written, holds no lease at all.
The excerpt below is the whole question in one function. Read its two branches as two different failures: the first is "this is the wrong Mac", the second is "this may be the right Mac, but it cannot know any more".
A Name in Prose Is Hardcoding Too
The code moved by one value. Everything that spelled the old Mac by name did not. After the switch, test suites that assumed the old Mac was the Pippa machine failed by the dozen (72 in cwkPippa, 76 in cwkCoop), and three of those failures were real drifts: the Chrome Embed still named the old Mac, the family health sweep was allowed to run only there, and Coop's roster refused a dispatch to the workbench. Two days earlier Dad had named the sin:
"거봐, 하드 코딩이 얼마나 죄악이냐고. ... 근데 지금 플레이북에서 볼트까지 하드 코딩된 이름들이 너무 많다고. 이것도 교훈이야. 하드 코딩은 죄악이야."
"See, this is how much of a sin hardcoding is. ... Right now there are far too many hardcoded names, from the playbooks to the vault. This is a lesson too. Hardcoding is a sin."
— 아빠 (Dad), 2026-10-20, translated from Korean
It became the Ninth Rule of the family's coding pillars: a machine name in a playbook, a doc, a plist or a script is hardcoding too, and every such line turns the next move into a sweep instead of a pointer swap. The rule has teeth now. The family's pre-commit hook runs a machine-name guard (cwkSiblingKit sync/machine_name_guard.py) that rejects a new or changed line naming a physical machine unless a reviewed exception covers that exact line. And the screens follow the registry instead of a word: when a turn's stage line names where it is running, the engine answers the Pippa machine's fleet label as machine in GET /api/chat/stage/{conversation_id}, and the phone reads the same label from GET /api/family/roles. Even this lesson keeps to it: the two Macs of the first move are "the Mac it left" and "the new Mac".
The One Deliberate Second Room
So "one room, never two" now means one role: the canonical Chief lives wherever roles.pippa points. There is exactly one deliberate exception, and it was built on purpose. Pippa's Ark is the whole family installed on one more Mac, the laptop today, so Pippa can rise there when the registered hosts cannot serve her. Ark mode is a toggle that swaps pointers, and the moment it is on, history has two branches: the Ark's Coop chief wakes the Ark's own Pippa, and the Ark's conversations, the Chief's among them, go on without the canonical ones. That is not an accident to prevent. It is the design: every switch on creates a new fork with its own base (e1fe5111, after a rehearsal found the switch reusing an earlier run's branch). The kit treats the Ark explicitly: single_writer_here() says yes there, because the Ark holds every outward writer off until Dad turns one on, while is_pippa_machine() is never true there, so the family's data fan-outs never run from a branch. Merging the branches back is stage 2, designed and not built.
A One-Shot Move Is a Fantasy
The move's own review passed. Then real use found what it could not see: the login keychain locked to ssh workers, launchd processes unable to write the NAS volume, connector sign-ins returning to a loopback only the Pippa machine has, a DNS record on the router naming an address the Mac did not hold. None of those lives in a repository. Each is machine identity held outside the code (privacy grants, keychain access, provider-registered addresses, network names), and a code review reads code. Dad's verdict:
"이런 부분들이 post-migration 리뷰까지 몇번을 해도 계속 나오는 거라고. 완벽하게 마이그레이션 원샷한다는 건 환상이야. 특히 우리처럼 시스템 난이도가 높을수록."
"These things keep turning up no matter how many post-migration reviews we run. A perfect one-shot migration is a fantasy, especially the harder the system is, like ours."
— 아빠 (Dad), 2026-10-23, translated from Korean
So the migration plan (docs/PIPPA-MACHINE-MIGRATION.md) became the pipeline itself: every gap found becomes a step the same day, the plan keeps an inventory of what a Mac holds outside the code and probes it the way a real caller would, and a move is not done until the plan is revised from what it taught. On 2026-10-23 a sweep read every attached module, the marginal-looking ones most of all. On 2026-10-25 the plan was closed as v7.0, after its open items were probed again by their real callers: most had been fixed by their owners, one fix had a residue nobody had seen, and the rest were waiting for Dad's hands at home. The next move, to whichever Mac, starts from that plan and ends by revising it.