A running checklist for working through the open mobile backlog in order, one PR per item, each merged only after CI is green. Ticked when the PR merges; marked blocked with a link to the write-up when an item needs something outside the repository.
Follow-ups found during the run
Each item: a test that fails without the fix, bun run test, bun run typecheck, bun run verify:packages and Pickier locally, then CI.
Log
fix(mobile): share() is fire-and-forget, so four of its native outcomes can never reach the page #203 : split in two. fix(ios): let share() settle the promise its native side answers #208 makes iOS share() return its promise and adds a static check for the whole bug class, which found five more (fix(ios): five page calls drop the answer the native side sends them #207 ). fix(android): resolve share() with whether the person picked an app #210 resolves Android share() with whether an app was picked and adds the suite's first real cancellation path: the harness dismisses the real share menu. Android's ungated enable* flags are filed as fix(android): enable* flags shape the manifest and craft.capabilities, but the bridge never checks them #209 .
fix(ios): let share() settle the promise its native side answers #208 and fix(android): resolve share() with whether the person picked an app #210 merged. Both carried two red iOS jobs that also fail intermittently on main: the shim leg's first simctl launch --console-pty after a cold boot takes ~200s (reproduced locally on a fresh simulator; a plain launch takes 3s), and the Zig slice fixture sometimes loses the Siri deletion reply (i=38) or the simulated coordinate. Fixing the gate before the next merge.
Gate fix fix(mobile-e2e): launch iOS apps without a pty, so a cold simulator cannot eat the budget #212 : both iOS harnesses launch without --console-pty (≈200s on a cold simulator, reproduced) and the slice's count agrees with its wait loop. The other slice flake is a real never-settling promise, filed as fix(ios): removeSiriShortcut can wait for ever on a completion the framework never fires #211 .
fix(ios): settle location promises when Core Location has no callback #197 : 4fe86f0 had already fixed it in Swift, but no test ran it on a device. test(mobile-e2e): prove iOS location settles, and that Zig served it #213 turns geolocation on in the iOS probe, grants location and sets a coordinate from the host, and requires the reported coordinate back. A targeted mutation of 4fe86f0 fails permissions.location.granted on both legs. Zig now logs hand-backs so the runtime leg can prove Zig served the calls.
fix(mobile-e2e): launch iOS apps without a pty, so a cold simulator cannot eat the budget #212 , test(mobile-e2e): prove iOS location settles, and that Zig served it #213 and test(mobile-e2e): prove Android reports approximate location as granted #214 merged, all fully green. fix(android): report granted location permission through the mobile bridge #192 turned out to be fixed by bb5c1b5 and 780e952 but never run on a device. test(mobile-e2e): prove Android reports approximate location as granted #214 's emulator run grants approximate location only, and a CI mutation that requires precise location makes it fail with denied on both legs (run 35111184463).
fix(ios): deliver custom-scheme deep links on cold start #198 : reproduced with an XCUITest cold start. The launch link was dispatched 1ms before onLink could exist. fix(ios): hand a cold-start link to the page that subscribes for it #216 replays early links to the first subscriber, with getInitialURL claiming the launch link so it is not delivered twice, and adds a real cold-start test to both iOS legs. Android parity is fix(android): hand a link that arrives before onLink to the first subscriber, as iOS now does #215 .
perf(android): the shipped libcraft.so carries debug info, ~5.5 MB per ABI #204 : zig objcopy cannot copy ELF to ELF, so perf(android): ship libcraft.so without its DWARF, and keep the DWARF for symbols #217 strips with the NDK's llvm-objcopy, keeps the DWARF in android-symbols/, and the Android runner refuses a library that still carries any.
Nothing in CI compiles the Kotlin templates, and #156 shipped three type errors because of it #163 : already done by 6f3a4d7 (android-builder → test:templates). It caught fix(android): resolve share() with whether the person picked an app #210 's duplicate PendingIntent import today. Closed with that evidence.
fix(ios): add required WCSessionDelegate methods to generated Watch app #193 –fix(ios): configure APNs production entitlement for release archives #196 : all four were fixed in the generator on 14 September (5d03387 , 6bd84f4 , 611be12 ), and none of those fixes was checked where it failed. test(ios): install the Watch-enabled app and read the APNs environment it signs #218 builds the Watch-enabled app with the Watch app embedded and installs it on an iOS simulator; the macOS runner already has a watchOS runtime. It also reads the aps-environment Xcode actually signs for Debug and Release. Reverting each fix makes the gate fail with that issue's own message. fix(ios): configure APNs production entitlement for release archives #196 is a one-file change, so it rides in the same PR as the Watch work to respect the 5–8 files per commit rule.
perf(android): ship libcraft.so without its DWARF, and keep the DWARF for symbols #217 and test(ios): install the Watch-enabled app and read the APNs environment it signs #218 merged, fully green. The original backlog is done. Continuing with the four issues filed along the way, in the order above: the two iOS ones are self-contained, fix(android): hand a link that arrives before onLink to the first subscriber, as iOS now does #215 ports an iOS contract, and fix(android): enable* flags shape the manifest and craft.capabilities, but the bridge never checks them #209 changes Android defaults so it goes last.
fix(ios): five page calls drop the answer the native side sends them #207 : fix(ios): hand the page the answers native was already sending #222 , fully green. Adding the callbackId alone would have hung haptic and speech on every call that worked: native only ever rejected them, and Zig declared them .reply = .none. Both runtimes now resolve. The conformance test checks both directions, and a Bun test runs the real page script. The craft.haptics.* feedback helpers treat "haptics left off" as nothing to play, as Android and the web do. Filed fix(android): haptic, vibrate, startListening and stopListening answer nothing, where iOS now does #219 (Android parity), fix(location): craft.watchPosition and clearWatch mean different things on each platform, and v1 can leave a dead watch #220 (the flat watch API) and fix(ios): openURL, setBadge and setFlashlight never settle on a malformed argument in Swift #221 (Swift cases that never settle on a bad argument).
fix(ios): removeSiriShortcut can wait for ever on a completion the framework never fires #211 : fix(ios): settle a Siri shortcut removal whose completion never comes #227 , fully green. Both runtimes now answer TIMEOUT after 15 s. Zig's completion blocks moved out of the reply slots, so a late completion cannot answer the next call. Across three CI runs the simulator never called the deletion completion at all, while locally it arrives in about 40 ms. So the slice fixture asserts that the call settles, and a timeout shows as a CI warning rather than a failure. Filed fix(ios): Zig actions that wait on a framework completion have no deadline, and cannot safely get one yet #223 , fix(ios): Swift-only actions that wait on a framework callback have no deadline #224 , fix(ios): ios_async's main-queue hop checks only that a slot is in use, not which lease #225 and fix(ios): an answer to a call made before a page reload can settle a different call after it #226 .
fix(android): hand a link that arrives before onLink to the first subscriber, as iOS now does #215 : fix(android): hand the launch link to the page that subscribes for it #232 merged, fully green (including the Kotlin compile and the Android emulator cold starts). The Kotlin compile and emulator are verified only in CI. It also fixes a double delivery to craftReady handlers on both platforms. Filed fix(android): injecting the bridge is not idempotent, and a stale injection callback can mark the wrong page ready #228 , fix(android): a recreated activity restores the wrong initial URL, and Recents re-fires the launch shortcut #229 , test(android): compare each JNI descriptor with its Kotlin external fun, not only the name #230 and fix(android): CraftNative.deliver drops a script silently when no deliverer is installed #231 .
Paused here at the user's request, until the weekend. fix(android): enable* flags shape the manifest and craft.capabilities, but the bridge never checks them #209 is not started.
A running checklist for working through the open mobile backlog in order, one PR per item, each merged only after CI is green. Ticked when the PR merges; marked blocked with a link to the write-up when an item needs something outside the repository.
share()never reports back to the page — iOS in fix(ios): let share() settle the promise its native side answers #208, Android in fix(android): resolve share() with whether the person picked an app #210libcraft.so— perf(android): ship libcraft.so without its DWARF, and keep the DWARF for symbols #217Follow-ups found during the run
removeSiriShortcutcan wait for ever on a completion the framework never fires — fix(ios): settle a Siri shortcut removal whose completion never comes #227onLinkto the first subscriber — fix(android): hand the launch link to the page that subscribes for it #232enable*flags are never checked by the bridge — fix(android): enforce the capability flags the bridge already reports #233Each item: a test that fails without the fix,
bun run test,bun run typecheck,bun run verify:packagesand Pickier locally, then CI.Log
share()return its promise and adds a static check for the whole bug class, which found five more (fix(ios): five page calls drop the answer the native side sends them #207). fix(android): resolve share() with whether the person picked an app #210 resolves Androidshare()with whether an app was picked and adds the suite's first real cancellation path: the harness dismisses the real share menu. Android's ungatedenable*flags are filed as fix(android): enable* flags shape the manifest and craft.capabilities, but the bridge never checks them #209.main: the shim leg's firstsimctl launch --console-ptyafter a cold boot takes ~200s (reproduced locally on a fresh simulator; a plain launch takes 3s), and the Zig slice fixture sometimes loses the Siri deletion reply (i=38) or the simulated coordinate. Fixing the gate before the next merge.--console-pty(≈200s on a cold simulator, reproduced) and the slice'scountagrees with its wait loop. The other slice flake is a real never-settling promise, filed as fix(ios): removeSiriShortcut can wait for ever on a completion the framework never fires #211.permissions.location.grantedon both legs. Zig now logs hand-backs so the runtime leg can prove Zig served the calls.deniedon both legs (run 35111184463).onLinkcould exist. fix(ios): hand a cold-start link to the page that subscribes for it #216 replays early links to the first subscriber, withgetInitialURLclaiming the launch link so it is not delivered twice, and adds a real cold-start test to both iOS legs. Android parity is fix(android): hand a link that arrives before onLink to the first subscriber, as iOS now does #215.zig objcopycannot copy ELF to ELF, so perf(android): ship libcraft.so without its DWARF, and keep the DWARF for symbols #217 strips with the NDK'sllvm-objcopy, keeps the DWARF inandroid-symbols/, and the Android runner refuses a library that still carries any.android-builder→test:templates). It caught fix(android): resolve share() with whether the person picked an app #210's duplicatePendingIntentimport today. Closed with that evidence.aps-environmentXcode actually signs for Debug and Release. Reverting each fix makes the gate fail with that issue's own message. fix(ios): configure APNs production entitlement for release archives #196 is a one-file change, so it rides in the same PR as the Watch work to respect the 5–8 files per commit rule.hapticand speech on every call that worked: native only ever rejected them, and Zig declared them.reply = .none. Both runtimes now resolve. The conformance test checks both directions, and a Bun test runs the real page script. Thecraft.haptics.*feedback helpers treat "haptics left off" as nothing to play, as Android and the web do. Filed fix(android): haptic, vibrate, startListening and stopListening answer nothing, where iOS now does #219 (Android parity), fix(location): craft.watchPosition and clearWatch mean different things on each platform, and v1 can leave a dead watch #220 (the flat watch API) and fix(ios): openURL, setBadge and setFlashlight never settle on a malformed argument in Swift #221 (Swift cases that never settle on a bad argument).TIMEOUTafter 15 s. Zig's completion blocks moved out of the reply slots, so a late completion cannot answer the next call. Across three CI runs the simulator never called the deletion completion at all, while locally it arrives in about 40 ms. So the slice fixture asserts that the call settles, and a timeout shows as a CI warning rather than a failure. Filed fix(ios): Zig actions that wait on a framework completion have no deadline, and cannot safely get one yet #223, fix(ios): Swift-only actions that wait on a framework callback have no deadline #224, fix(ios): ios_async's main-queue hop checks only that a slot is in use, not which lease #225 and fix(ios): an answer to a call made before a page reload can settle a different call after it #226.craftReadyhandlers on both platforms. Filed fix(android): injecting the bridge is not idempotent, and a stale injection callback can mark the wrong page ready #228, fix(android): a recreated activity restores the wrong initial URL, and Recents re-fires the launch shortcut #229, test(android): compare each JNI descriptor with its Kotlin external fun, not only the name #230 and fix(android): CraftNative.deliver drops a script silently when no deliverer is installed #231.