openSettings, downloadFile, sendToWatch. None was blocked by anything structural. Each was held back by a condition its own module had written down — and each of those conditions has since been met by work landed in later rounds.
This came out of an audit of all 38 remaining actions in which every "blocked" verdict was handed to a second agent whose instructions were to refute it. 13 of 15 were overturned. My own read last round — "the rest is mostly Swift-only or blocked" — was substantially wrong, and the wrongness was the expensive kind: it would have stopped work that was available.
The three, and what unblocked each
openSettings — bridge_mobile_permissions said claiming it meant "first extending ios_async with a bool-literal reply variant", because its reply is a bare boolean while ios_async.boolBlock answers the strings "granted"/"denied" — and "denied" is truthy, so if (await craft.permissions.openSettings()) takes the success branch on failure.
The hazard was real; the conclusion was wrong. A module-owned global block delivering through deliverJson carries any literal — the shape bridge_mobile_auth and bridge_mobile_siri established afterwards. What the note was actually objecting to remains true and is still avoided: the repo's one hand-rolled stack block (bridge_mobile_system.zig:391) is safe only because openURL: chooses to copy an escaping handler, which is the callee's choice and not a guarantee. Every block added here is global, so Block_copy is identity and the question does not arise.
downloadFile — recorded as "out of scope for this round" and nothing more. It needs void (^)(NSURL *, NSURLResponse *, NSError *), which no pooled block covers. Two deliberate divergences:
- The filename is validated.
saveFilealready refuses/and NUL becauseappendingPathComponenton"../Library/Preferences/x.plist"escapes the container. Swift'sdownloadFileruns the sameappendingPathComponentwith no check, so that escape is available through the download path today. Guarding one entrance and not the other is not a guard. - The error text becomes a code, since the protocol has no message field.
sendToWatch — bridge_mobile_watch stated the condition exactly: "becomes migratable the moment ios_async gains a deliverError(ticket, BridgeError)". It has one, added in round 8.
That mattered more than it sounds: the error half is the common branch here, not the exotic one — unpaired simulator, watch app not installed, not reachable, reply timed out — and the bundled CraftWatchApp.swift.template implements only the no-reply variant, so against the shipped companion the error handler is what always fires.
The same note also predicted the fix implemented here: the reply goes through +[NSJSONSerialization isValidJSONObject:] first. Swift hands it straight to dataWithJSONObject:, which raises NSInvalidArgumentException for the NSDate/NSData values WCSession explicitly permits — and a raise is not catchable from Zig or from Swift's catch. A watch replying ["at": Date()] terminates the app today.
Deliberately not included: registerBackgroundTask
The audit recommended it as a fourth, "MIGRATABLE — Swift makes no BGTaskScheduler call at all". That is true and misses the point. Swift inserts into a Set nothing reads, so migrating it means either reproducing a fabricated success or diverging into a refusal — and bridge_mobile_bgtasks already reasoned that through and wrote the decision down. A real registration must happen before launch finishes and cannot be made from a dispatch. Taking it for +1 ratchet would be the overclaiming this project exists to avoid.
Stale docs are the actual deliverable
Three module headers and five tests asserted the old decisions, including two named for the refusal they pinned ("openSettings is left to the Swift shim on purpose", "sendToWatch is left to the Swift shim, not claimed and not refused"). All rewritten rather than deleted — reasoning kept, conclusions inverted — so the next audit doesn't re-derive the old map. A written-down refusal that has quietly become false is worse than no note at all.
Verification
zig build test green (153/153), zig fmt --check, build-ios and build-ios-simulator clean, pickier . 0 errors.
31 fixture assertions pass on a simulator, three new:
ok: URL fetched and moved into the app container (i=48 seen 1x)
ok: watch refusal delivered through the async error path (i=52 seen 1x)
ok: openSettings replied a real boolean, not "granted"/"denied" (i=54 seen 1x)openSettingsassertstypeof payload === 'boolean'— the exact trap, and only the module's own block satisfies it. Posted last, because it navigates to Settings; I checked empirically that the reply lands before the webview backgrounds.downloadFilefetches adata:URL, so it needs no network, and asserts the path landed underDocuments/.sendToWatchcomes backNOT_FOUNDfrom a linked WatchConnectivity —WCSessionitself reporting no reachable watch, not a missing class.
One self-inflicted failure worth recording: the download assertion first failed on a JS suffix comparison of mine ('slice-dl.txt'.slice(-13) returns all 12 characters, so it could never equal the 13 including the /). The Zig was correct on the first run.