The question every line answered by name
Until October 2026 the family lived on one Mac, and that Mac's name was the answer to every "where": in code constants, launchd plists, the CORS list, playbooks, skills, the vault. Measured on 2026-10-21, before anything moved: the port ledger held 121 claims, 118 of them on that one Mac, with no roles and no host table; the kit's generated tables were baked into 71 vendored copies across 43 repositories; the data fan-outs were guarded by a computer name. Then Dad decided the family would live on a second Mac while code stays on the first, and asked for a move that any later move could repeat:
원장 하나 수정하고 데이터 싱크하는 수준이어야 한다는 거야. 한번 할 때 제대로 해.
("It has to come down to editing one ledger and syncing the data. When we do it, do it properly." Dad, 2026-10-21.)
One value is the identity
Now the answer is a role. cwkFirelink's port ledger carries a roles table with exactly two entries. roles.pippa names the Pippa machine: every service member, the live data under ~/cwk-dbs, the vault's ground truth, Pippa's crons and data fan-outs, every single-instance writer, the HTTPS twins the phones dial. roles.workbench names where code is written: the ground truth for code fan-outs, Coop's workers, builds and simulators, Dad's dev servers, Cinder beside Photoshop. A machine is an identity only (fleet label, MagicDNS name, tailnet address), and every claim in the ledger names a role (@pippa), one machine (Ember's engine, pinned to its GPU) or every machine (an app each Mac runs as itself). A move changes one value. The data roots do not move: ~/cwk-dbs and ~/Obsidian are the same paths on every Mac, and the role decides which Mac's copy is live.
Every Python member reads the roles through its vendored kit_family_endpoints.py (excerpt below). family_roles() reads this Mac's checkout of the ledger at the moment of asking, cached by the file's mtime and size, so a flip of roles.pippa, or its revert in a rollback, reaches a running service without a restart. A ledger that cannot be read answers with the table baked at the last kit sync, and the call never raises. Phones ask GET /api/family/roles for the same answer.
Writers ask the role, never a hostname
Two Macs must never both run a single-instance writer: Pippa's crons, the Telegram bot, the Grok exchange, Coop's writer, the publishers, the data fan-outs, the vault pair. Each asks the kit before it acts. is_pippa_machine() compares this Mac's fleet label with the one the ledger gives the role, reading the canonical ledger and never a host profile, so Pippa's Ark (lesson 10) never becomes the family's data writer. single_writer_here() answers yes on the Pippa machine, or on the Ark, where a held writer runs only once Dad turns it on. An outward writer on a Mac that is on the origin lease also holds once that Mac has gone 30 minutes without confirming the ledger at origin, because the family may have moved without this Mac hearing it; chat keeps answering. A Mac joins the lease by following origin on its own timer, which records each confirmed follow. A Mac with no record, such as the workbench where origin is written, is on no lease and never holds for this reason. How the heartbeat, the Telegram bot and the backup line live under that gate is heart track lesson 9, 'One Heart on One Machine'.
A move is a pointer plus a data sync
The data half is scripts/pippa-machine.py, run on the new Mac, B. It rolls A's stores into a staging area while A serves, at background priority and resumable, and never writes over B's live paths. A live SQLite store travels as an image taken through SQLite's own backup API and checked on B by sha256 and integrity_check, never as raw files with loose -wal and -shm beside them. B's own data is excluded by rule, and anything a promote replaces is set aside whole first. Two kinds of secret travel apart from the data, and neither rule is about where a copy sits. A rotating token, such as the connectors' refresh tokens, stays out of the rolling sync and is carried once, after A has stopped, so only one Mac ever refreshes it; A keeps its copy, with nothing running there to use it. Keychain items go through family-keychain-carry.py as the switch's step 1b, before A stops, so from then on they sit on both Macs. That is safe because their values do not change on use (the OAuth logins, whose tokens rotate, are left out on purpose and made on B itself). The carry is create-only: a different value already on B stops the move until Dad decides.
The running half is scripts/pippa-switch.py on the workbench. The switch is 21 steps (preflight; A's writers down; every moving unit stopped and the vault proven quiet; the final delta; promote; the ledger commit and its kit wave; B's units; the twins; verify; the recovery copies ready; A down), each recorded in switch-state.json with what it changed. A cut run resumes where it stopped, and the ledger edit is never made twice. The rollback runs in an order where each barrier stops everything after it: B's units down, the vault proven quiet, B's writes carried back, the ledger reverted and every follower proven to hold the revert, the launchers and web clients rebuilt, and only then A's units. A failed barrier enables and starts nothing on A, keeps the record and exits 1, and a rerun continues from it. It does not leave A as it was: once the barrier for B's writes has passed, those writes are already promoted onto A's data, so a later stop at the ledger, the launchers or the web clients finds A holding B's data with its units still down.
Two syncs, because code and the vault are different things
Code's ground truth is the workbench: it commits and pushes, and every Mac the ledger places a member on follows origin by fast-forward only, never stashing, resetting or forcing. The vault's ground truth is the Pippa machine, and Dad ruled its shape:
대원칙은 '피파 있는 곳에 볼트 있다'야.
("The great principle: where Pippa is, the vault is.") Exactly one two-way pair runs, between the Pippa machine and the workbench, because those are the two places the vault is edited. It runs on Unison with an archive of the last agreed state, so it can tell a deletion from a new file, which a pair of rsyncs cannot. Every other Mac receives the vault one way from the Pippa machine. When the two roles sit on one Mac there is no pair.
What a dead machine may lose
A healthy round trip loses nothing: the switch back carries everything B wrote. Only a Pippa machine that is dead and cannot be read may lose anything, and Dad set the bound on 2026-10-21: at most ten minutes of conversations and Coop. While the workbench is another Mac it pulls those every five minutes, and every other moving store hourly (lesson 3). A recovery states its window store by store; a window past the bound, or one it cannot prove, stops for Dad's decision. Silence is not proof of death: a stopped scheduler stops the copies while the machine may still be writing, so the loss is measured to Dad's word that B is off, never to the last time B answered.
The first move, and the gaps no review could see
On 2026-10-22 the switch ran for real, after a rehearsal round trip the same morning. Pippa stopped answering at 10:20 KST and answered from the new Pippa machine at 10:44; the switch finished at 11:01. It stopped three times, and each stop was fixed in place and resumed, never rolled back. Then Dad's real use found what the post-migration review had passed: the login keychain locked to ssh workers; a launchd interpreter without its Network Volumes grant, so a published video never reached the NAS; connector sign-ins registered to return to a loopback address no other device could reach; an /etc/hosts record sending the workbench's name to an address no Mac holds; a remote worker whose runtime snapshot still dialled the old address. None of them lives in a repository. Dad, the night after:
완벽하게 마이그레이션 원샷한다는 건 환상이야. 특히 우리처럼 시스템 난이도가 높을수록.
("A perfect one-shot migration is a fantasy, the more so the harder the system is, like ours." 2026-10-23.) So the plan now opens with an inventory of state bound to a machine outside the code: privacy grants per process kind, keychain items and who may open them, URLs registered at providers, mounts, names, addresses installed in clients. Each is probed the way its real caller goes, never by a health endpoint, and every new kind a move turns up is added there.
The plan is the pipeline; the scripts are its tools
The scripts are not the move. docs/PIPPA-MACHINE-MIGRATION.md is: a runbook in which each step says why it exists, what to check before trusting its tool, what proves it worked, how it is undone, and the signs that it no longer fits the Macs in front of you. Dad's ruling, 2026-10-21:
스크립트는 머리가 없어. outdated 면 오히려 파이프라인을 망칠 수 있다는 뜻이야. 피파가 보고 판단하는 플랜이어야 해.
("Scripts have no head. Outdated, they can wreck the pipeline instead. It has to be a plan Pippa reads and judges.") The plan's last step is revising itself: every gap a move found becomes a step, and a move is not done until the document reflects it. The first move closed on 2026-10-25 with version 7.0, after every open table had been probed again by its real caller. The next move starts at its section 9.0 and reads the first move only as a record. Writing all of this by role, never by name, is its own rule (rules track, lesson 7).