The fact that gets skipped
Every other client in this household ships by copying a file. The web UI is a build directory served by the engine; the Mac apps are signed bundles copied into place. iOS is the one platform where the last step is not yours — Apple sits between the build and the device, and that changes what "done" means.
The family rule is one line: TestFlight is delivery. A cable install is a build check. A cable build expires in a week, needs the cable and an unlocked device, and reaches exactly one phone. It is how you find out whether the thing runs. It is not how anyone receives it.
This is written down because it was learned the expensive way, in a sibling app, twelve days after the household already had a working upload pipeline. A session finished the work, side-installed the build on Dad's locked phone, and logged the real delivery path as a future task. Dad had to point at it.
Four facts, and only the last one is delivery
The contract names them so a status update cannot collapse them into one:
The archive was exported and uploaded — your script says so. Apple finished processing — the store console says so, and it can take a while. The internal group has the build — a separate assignment, a separate screen. It is installed on the device — the device says so, or the tester row does.
Four separate truths. A report that says "shipped" when only the first is true is not lying on purpose; it is compressing a four-step process the way people compress one-step ones. So the rule is to say which one is true.
Gates that refuse, instead of mail that arrives later
Most of the upload script is refusals, and every refusal has the same origin story: Apple would otherwise accept the upload and send an email about it hours later.
The order is the design: everything cheap refuses before anything expensive runs, and all of it before the build number moves. First an App Store Connect key — or, without one, a refusal to upload while the screen is locked, because a locked screen kills the keychain session the archive needs. Then a dedicated simulator, so a test run never lands on a shared one. Then the app's inputs committed, so the build number names what is actually built; the shared kit's sources clean, because the archive compiles them too; the packaged renderer present; the icon the right size and opaque. Only then the suite, and only after it passes does the build number move.
After the archive lands, HEAD is checked again — a sibling session may have committed while the build ran — and then the archive itself is inspected: the app's icon name declared, the embedded watch app's too, the watch's compiled asset catalog present, the complication's extension actually there.
That last group is the interesting one. They are assertions about the output, not the input — the build succeeded and the artifact is still wrong. A green compile is not evidence about a bundle's contents.
The project file is generated, and the tree must be clean
Two smaller choices that matter more than they look.
The Xcode project is generated from a plain text source and then committed. Hand-editing the project in the IDE is how a team identifier or a signing setting ends up in a file nobody reads in review, and the next machine builds something subtly different. The text file is the source; the project is an artifact that happens to be tracked.
And the clean-tree gate is not hygiene — it is about what an archive actually compiles. It compiles the working tree, not the last commit. A dirty file, including one a sibling session left in a shared checkout, silently becomes part of the build you are about to ship and can never be reproduced from the commit you stamped it with.
What is already paid for
Nothing here is invented per app. One developer team carries the whole family, named in each project's text source rather than clicked into an IDE. A new app needs one store record and nothing else.
And since 2026-09-14 the pipeline itself is one program. Until that day the family carried eight hand-grown copies, and they differed by more lines than the smallest of them contained. The differences were not design choices. They were guards one app had learned and the others had not caught up to: two archives swallowed the build tool's status behind a filtered pipe, one had no clean-tree check and no HEAD re-check, one never moved its build number, only two refused a locked screen. Each was a guard learned once and copied short. Now every guard runs for every app in one order. The program is vendored into each repo and held byte-identical by a drift test, and each app keeps only its own facts beside it — names, paths, a few hooks — so a release never depends on a sibling checkout being present on the machine that builds it.
That is the same instinct the vessels track has been teaching about brains and the frontend track about sibling apps, pointed at a build pipeline: the canonical thing lives in one place, and every consumer carries a copy that is proven identical rather than a reimplementation that is hoped to be.