Skip to content

Watch apps: one entry point, a phone-watch channel, and a simulator that can run the pair - #5487

Open
shai-almog wants to merge 295 commits into
masterfrom
watch-apps-product
Open

Watch apps: one entry point, a phone-watch channel, and a simulator that can run the pair#5487
shai-almog wants to merge 295 commits into
masterfrom
watch-apps-product

Conversation

@shai-almog

@shai-almog shai-almog commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

isWatch() existed without a product on top of it. This turns the watch render
slice into a watch app: one setting builds it on both platforms, the two apps can
talk, and you can develop the pair on your desktop.

Bugs this fixes

Wearables are new and nothing depends on them, so these are fixed rather than
preserved:

  • A cloud build never produced a watch app. codename1.watchMain became a
    build argument only on the local path; the server lifts only codename1.arg.*
    keys out of the uploaded settings file, so the daemon asked for watchMain and
    got nothing.
  • The documented companion default never embedded the watch app.
    watchNative.embedCompanion defaulted to false, so the "Embed Watch Content"
    phase was actively removed even in companion mode.
  • watchMain reached only iOS. Wear OS was enabled by an unrelated
    android.wear hint, so a project had to declare the same intent twice.
  • The simulator had no watch form factor. JavaSEPort never overrode
    isWatch(), so the guide's advice to iterate on a watch layout locally was
    untrue.
  • A Wear app could not scroll. onGenericMotionEvent read only the mouse
    axes; rotary input arrives on SOURCE_ROTARY_ENCODER / AXIS_SCROLL.
  • A round Wear face clipped its own corners. No display cutout is reported,
    so the safe area came back zero.

What is new

One setting. codename1.watchMain is the entire opt-in on both platforms.
Nine build hints are deleted; bundle id, deployment target, team id and display
name are derived. codename1.watchStandalone is the only other setting — the one
thing not inferable from the project. Net new hints: zero.

com.codename1.wearable — the phone↔watch channel, same API on Apple Watch
and Wear OS, modelled on com.codename1.car (portable API, SPI bridge, inert
when there is nothing on the other end). It exposes the three transports the
platforms actually give us, because picking the wrong one is the usual reason a
watch app "never gets the update": sendMessage for a live answer, putData for
state that survives sleep and relaunch, transferFile for bulk. Callbacks arrive
on the EDT and are queued across a cold start — the platform starts an app purely
to hand it a payload. Backed by WCSession on Apple and the Wearable Data Layer
on Android, both gated by API scan so apps that never talk to a watch link
nothing.

A simulator that runs the pair. Four generated watch skins (Apple Watch 41/45,
Wear round, Wear square), isWatch() and the "watch" override layer, and a
Watch menu that launches watchMain in a second process wired to the first — so
sendMessage and putData genuinely round-trip on the desktop. Two processes,
not two windows: Display is a singleton and sharing it would hide the bugs that
only appear once the pair is real.

Complications as surfaces families. A complication is a WidgetKit widget in
an accessory family, so WATCH_CIRCULAR/RECTANGULAR/INLINE/CORNER join
WidgetSize rather than getting an API of their own.

The guide, rewritten around the two-app model, with the data-sharing decision
table as its centre.

Not yet done, and stated as such

  • The watchOS widget extension target and the Wear complication/tile services
    that render the watch families are not generated yet. The guide says so.
  • The watch target compiles its OWN ParparVM translation, rooted at
    codename1.watchMain, and boots a watch stub. Verified end to end: the watch
    target compiles, links and renders on the watchOS simulator (166 pass / 3 fail
    / 2 skip). Tree-shaking is real -- the phone tree carries 24 generated SVG
    classes, the watch tree none -- and that is also the one known regression:
    those classes are reached reflectively, so SVGStatic,
    SVGAnimatedScreenshotTest and LottieAnimatedScreenshotTest fall back to a
    placeholder render. The watch pass needs the same reflective roots the phone
    pass keeps. A project whose watch entry point IS the phone main keeps a single
    translation and is unaffected.
  • Companion mode on Android does not yet emit a second wear APK; standalone works.

Verification

CI is the gate. Everything below is what was additionally checked locally before
pushing, and the numbers move as the branch does.

  • codenameone-maven-plugin: 466 pass, 1 skipped
  • core-unittests: 4754 pass, with SpotBugs, PMD and Checkstyle at zero
  • core, JavaSE, iOS and Android ports build; the injected Android bridge sources
    type-check against a stub harness (nothing in CI compiles them) and the
    WatchConnectivity native passes clang -fsyntax-only for iphoneos/arm64
  • docs gates green: snippet validation (659 blocks), Asciidoctor
    --failure-level WARN, Vale, capitalization
  • build-ios-watch does run in CI -- it is a job in Test iOS UI build
    scripts
    , on macos-15 with DEVELOPER_DIR pointed at Xcode 26, and this PR
    touches paths that trigger it. It is the real gate for anything that alters
    the generated Xcode project. It is not run on the authoring machine, and
    during the rapid review cycle several of its runs were cancelled by
    supersession, so check it against the CURRENT head rather than an older run.

Server-side half: codenameone/BuildDaemon#watch-apps-product

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings July 29, 2026 11:21
chatgpt-codex-connector[bot]

This comment was marked as resolved.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

This comment was marked as resolved.

Copilot AI review requested due to automatic review settings July 29, 2026 11:36
@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

chatgpt-codex-connector[bot]

This comment was marked as resolved.

This comment was marked as resolved.

Copilot AI review requested due to automatic review settings July 29, 2026 11:44
chatgpt-codex-connector[bot]

This comment was marked as resolved.

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@shai-almog

shai-almog commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 12 screenshots: 12 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

Copilot AI review requested due to automatic review settings July 29, 2026 12:07
chatgpt-codex-connector[bot]

This comment was marked as resolved.

This comment was marked as resolved.

@shai-almog

shai-almog commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 151 screenshots: 151 matched.

Native Android coverage

  • 📊 Line coverage: 8.09% (7868/97198 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.07% (41636/515701), branch 2.88% (1402/48723), complexity 3.18% (1663/52272), method 4.90% (1355/27642), class 9.97% (367/3680)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 8.09% (7868/97198 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.07% (41636/515701), branch 2.88% (1402/48723), complexity 3.18% (1663/52272), method 4.90% (1355/27642), class 9.97% (367/3680)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend scalar fallback (no native SIMD)
SIMD int-add (64K x300) java 271ms / native 155ms = 1.7x speedup
SIMD float-mul (64K x300) java 154ms / native 117ms = 1.3x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path gated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode 94.000 ms
Base64 CN1 decode 90.000 ms
Base64 native encode 376.000 ms
Base64 encode ratio (CN1/native) 0.250x (75.0% faster)
Base64 native decode 291.000 ms
Base64 decode ratio (CN1/native) 0.309x (69.1% faster)
Image encode benchmark status skipped (SIMD unsupported)

@shai-almog

shai-almog commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 181 screenshots: 181 matched.
✅ JavaScript-port screenshot tests passed.

Copilot AI review requested due to automatic review settings July 29, 2026 12:47
chatgpt-codex-connector[bot]

This comment was marked as resolved.

This comment was marked as resolved.

Copilot AI review requested due to automatic review settings July 29, 2026 12:56
chatgpt-codex-connector[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f10016b1e4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tools/watch-skins/GenerateWatchSkins.java Outdated
shai-almog and others added 4 commits August 13, 2026 19:15
…e in

Moving those models to logical points left ppi at the physical Retina 326.
JavaSEPort turns it into pixelMilliRatio = ppi / 25.4, which every
millimetre-based size is converted through, so the simulator made each one twice
the size the device gives it: IOSImplementation.getDeviceDensity() answers
DENSITY_MEDIUM for a display narrower than 500, and the watch now reports 198.
163 is that density. The Wear models are still in pixels and keep 326.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Probing usr/lib/swift/<M>.swiftmodule and usr/include/<M> found the Swift
overlays -- Darwin, Dispatch and ObjectiveC each have one, which is why the
three modules that got tested passed -- and missed nearly everything else.
Apple declares its C modules in shared maps, and of the 78 the watchOS SDK's
usr/include/module.modulemap names, 66 matched no path at all: SQLite3, zlib,
MachO, notify, os_object, TargetConditionals among them. A staged source
importing any of those was called unattributed, and one unattributed import
switches strict package filtering off wholesale, mirroring every phone package
product into the watch target.

The maps are now parsed, along with the toolchain's own swiftmodule directories,
and memoised: about 2800 names in roughly a second, once per build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…inst

theGeneratedPredicatesDecideCorrectly failed on build-linux-jdk8: these plugin
tests run on Linux too, where there is no xcrun to ask for an SDK path. The
guard I wrote for that -- `|| sdk_names.empty?` -- cannot fire, because the
helper seeds the Swift standard names before it reads anything and so never
returns an empty map. On Linux it therefore asked whether a host with no Apple
SDK knows about SQLite3, and failed.

Guarded on whether an SDK path was actually found, with the lookup itself
rescued: a missing xcrun raises rather than returning empty. Verified both ways
-- against the real SDKs, and against an xcrun that exits non-zero, which takes
the skip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ly be said

The notice naming kinds that will appear on no platform was guarded on
widgetExtensionBuilder existing, and parseSurfacesManifest clears
surfacesExtensionEnabled before that builder is created whenever every declared
kind is watch-only. The one case the notice exists for is therefore the one case
it could not reach: the build discarded every declared surface and reported only
that the iOS lowering had been skipped.

It is emitted from parseSurfacesManifest now, naming the kinds. The block at the
extension-generation site keeps the mixed case -- watch-only kinds alongside iOS
ones, where the extension is built and quietly carries none of them -- and its
comment no longer claims to cover both, which was how this hid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f45a720626

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

shai-almog and others added 4 commits August 13, 2026 21:05
putSecondaryEntryPointArguments read only the unprefixed project setting, while
the cloud mirror leaves an existing codename1.arg.* value alone -- so
-Dcodename1.arg.watchMain=... reached the daemon and was ignored locally, and
one invocation produced two different products. An absent project setting also
disabled the watch target outright, whatever the command line asked for. That is
worse than an override that works nowhere, because nothing in the local build
says the flag was dropped.

The overlaid argument comes first now, the project setting behind it, which is
the order the mirror already resolves them in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`== 1` and `!= 0` say exactly what the bare macro says, and the atom matcher
accepted only the bare form -- so `#if TARGET_OS_WATCH == 1` was left undecided,
its #else survived, and a phone-only package imported there was mirrored into
the watch target.

The mirror of the `== 0` reading already in cn1_watch_excludes_watch: that side
learned two rounds ago that a zero comparison is a negation, and this side was
not taught the same thing about a one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Requiring both a device and a simulator slice rejected a valid watchOS-only
archive: the framework phase then skipped it and the watch target failed on
undefined symbols, over a simulator slice nothing in that build was going to
load. The same conjunction was in the bundle check, over
CFBundleSupportedPlatforms and SupportedPlatformVariant, and is corrected with
it -- it is the same rule about the same artifacts, and only the archive half
was reported.

EITHER slice here, deliberately, rather than a specific one. This generator
hands the developer an Xcode project and does not know which destination they
will build for. The cloud builder does -- it selects the SDK and the destination
itself -- so it asks for exactly the matching slice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A reply on a review thread is not visible to whatever reads the diff next, so
two rejections that were argued in comment threads are written where the
decision lives.

The watch target is created :application, not :watch2_app.
com.apple.product-type.application.watchapp2 is the LEGACY WatchKit App and is
valid only paired with a watchkit2-extension carrying the code. This generates
the modern single-target app that replaced it -- WKApplication, SwiftUI @main
with @WKApplicationDelegateAdaptor, watchOS 10 floor, one target -- and Xcode's
own watchOS App.xctemplate declares com.apple.product-type.application with no
mention of watchapp2. The only watchapp2 in that template directory is the
legacy iOS container.

The watch translation is passed no separate reflective-keep-roots channel
because the stub IS that channel: it gets the same svgRegistryInstall and route
and annotation install snippets the phone stub does, so SVGRegistry.installGlobal
runs there too and the generated classes it names are hard references the
translator follows. The watch suite builds a distinct watchMain and SVGStatic
and SVGAnimated match their goldens every run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a563336dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tools/watch-skins/GenerateWatchSkins.java Outdated
shai-almog and others added 5 commits August 14, 2026 07:34
…lishes

CN1WatchHost derives one geometric inset from the corner radius and applies it
to all four sides. The skins took the vertical inset as 6% of the display height
instead, so the Apple models advertised 13 and 15 points against the host's 4
and 5 -- three times the inset, on the axis a watch layout is tightest on. A
layout honouring the safe area reflowed in the simulator and fitted on the
device, which is the simulator lying in the direction that wastes the most time.
Nothing on a rounded-rectangle face intrudes vertically beyond the corner arc,
so the extra modelled nothing.

A circular face keeps its own rule: inscribing a rectangle in a circle really
does cost about 15% on both axes.

Correcting the record as well as the code -- an earlier commit claimed the
regenerated 45mm skin "advertises the same five-point inset the watch does",
which was true of the horizontal axis and stated as though it were the whole
rectangle. The new test mirrors the host's formula rather than hard-coding 4 and
5, so the two sides cannot drift apart again without failing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It did not do what I said it did. The justification was that filtering
NativeLookup.register out of the watch stub stops a phone-only native's
Objective-C being compiled for watchOS, where a UIKit import is a build failure.
ByteCodeTranslator.execute copies every non-class file on the classpath into its
output verbatim -- `if (name.endsWith(".class")) Parser.parse(f); else copy(f)`
-- so that .m is staged and compiled for whichever target lists it whether or
not any Java stub names it. The filter never prevented that failure. A native
that cannot build for watchOS is guarded with TARGET_OS_WATCH in its own source,
which is how the port handles its own in 71 files, and the developer guide now
says so.

So the walk went, and with it: reachableClasses, classReferences,
installedRegistryRoots, reachesHealth, resolveHealthUsagePerRoot,
healthListenersReachableFrom, nativeRegistrationsReachableFrom, the four
per-root health fields, the sensorWriteThroughCallers attribution and the
ClassScanner.scanningClass hook added to feed it. Roughly 700 lines across the
two builders and their tests.

What replaces it is what was already there: the app-wide API scan decides
whether the app uses HealthKit, both targets get the same answer, and
watchNative.health overrides it when the scan is wrong. That is one boolean and
an existing hint instead of a bytecode walk.

The cost is honest and small: a watch that does not use health carries the
listener registry unless the project says watchNative.health=false, and the
watch binary is slightly larger than a per-root filter would leave it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…or them

WATCH_OPTIONAL_FRAMEWORKS already names every framework the port links that
watchOS does not have -- it is what ParparVM weak-links through
-Doptional.frameworks, and the port's sources are #ifdef'd around them. That is
the conditional-system-library arrangement every other platform here uses, and
the frameworks phase now follows the same declaration instead of probing SDK
directories for each entry.

The probe was the wrong shape, not merely mis-tuned. It asked whether a
framework exists in the watch SDKs, which has no single right answer: requiring
both dropped a device-only framework such as BackgroundTasks from a device
archive, and requiring only the active one makes the generated project depend on
which destination produced it. A declaration has neither problem, is reviewable
in one place, and cannot disagree with the weak-link list because it IS the
weak-link list.

The SDK lookup that remains answers a different question -- whether an import in
staged Swift names something the SDK vends or a package product -- and it now
covers frameworks too, which is where the framework directories went.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The declaration names what the PORT links, so it can say whether the watch has
CarPlay or OpenGLES and nothing at all about a dylib the developer dropped into
the project. Routing both through it accepted any raw library whose basename
happened not to be on the list, and an iOS-only one was then weak-linked into
the watch target -- weak linkage does not save a platform mismatch at link time.

So the raw-library branch now draws the distinction the .framework branch
already drew: from the SDK, the declaration decides; vendored, the binary does.
A .dylib is Mach-O and answers the same slice test the static archives use. A
.tbd is text that names its platforms, and unreadable is answered no, as the
plist and archive checks answer it -- a library left out costs a link error
naming it, one wrongly linked costs a mismatch deep in a build with no reason to
involve it.

Checked against the real stubs: watchOS libz.tbd reads as watch, the iPhoneOS
one does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing is both iOS and watchOS, so `#if os(watchOS) && os(iOS)` is false on
every platform and that arm never compiles anywhere. Treating the presence of
os(watchOS) as evidence the arm might compile on the watch kept an unreachable
arm alive and mirrored the iOS-only package it imports into the watch target.

The dual of the disjunction rule added earlier: there every operand had to
exclude the watch for the whole to exclude it, here any one operand does. Split
at the top level and asked recursively, so an operand that is itself a
disjunction gets the whole test, and `os(watchOS) && FEATURE` is still not
excluded -- a feature flag is unknown, not false.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 721b548915

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

shai-almog and others added 3 commits August 14, 2026 10:22
Two findings, one in each half of the watch build.

**The local node is not a peer.** Every module the build emits declares the same
`cn1_wearable` capability, so this device satisfies the capability query it
issues, and FILTER_ALL is an unfiltered set. Keeping our own id made isPaired()
and isCompanionAppInstalled() answer true on a device with no counterpart at
all, purely on the strength of seeing ourselves.

Filtered where the cache is WRITTEN -- all four paths -- rather than where each
question is asked, so there is one rule instead of one per caller. The transfer
quorum already stripped the local node after the fact; that line goes, because
filtering there left every other reader of the same cache still seeing us.

The async refresh asks getLocalNode() alongside the capability query instead of
resolving it in the callback: that listener runs on the main thread unless given
an executor, and a blocking identity query there is an ANR -- the trap
bondedNodeIds() already routes latency-sensitive callers around. Both tasks have
completed by the time the combined listener runs, so neither read blocks.

**A parenthesized disjunction is punctuation, not nesting.** In
`#if (os(iOS) || os(macOS)) && FEATURE` the top-level `||` split yields one
operand, and returning "cannot tell" there settled the condition before the
conjunction rule could observe that the group is false on watchOS -- retaining
an arm the compiler drops and mirroring its iOS/macOS-only package into the
watch target. The splitter now strips parentheses that wrap the whole
expression, balance-checked so `(a) && (b)` keeps its `&&`, and a single
top-level operand falls through instead of answering.

Falling through makes the rest of the function reachable for conditions it never
saw before, and every scan below reads a positive platform mention as exclusion.
`#if !(os(iOS))` is TRUE on watchOS -- that arm is the one the watch compiles --
so a negated group is now explicitly undecidable, which keeps it. That was
already wrong before this change; it just could not be reached.

27 predicate assertions, including both over-exclusion guards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…amed

`ld: framework 'SystemConfiguration' not found` on the unsigned device archive.
watchOS has no SystemConfiguration -- and no AudioToolbox and no QuickLook --
yet ByteCodeTranslator puts all three in every generated project's link phase,
and WATCH_OPTIONAL_FRAMEWORKS did not name them, so they survived into the watch
target.

The source half of the arrangement was already right: every one of these is
imported under `#if !TARGET_OS_WATCH` in the port. That is why it went
unnoticed -- the watch target compiled cleanly and only failed at the link, so
nothing short of an archive got far enough to see it.

Weak-linking cannot cover an absent framework: `ATTRIBUTES = (Weak, )` still
resolves against the SDK, so a framework the SDK does not have has to be left
out of the phase entirely. Under-declaring is a link error; over-declaring one
watchOS does have is merely tidy. The two are worth telling apart, so the
constant now says which of its entries are which and how to check:

  ls "$(xcrun --sdk watchos --show-sdk-path)/System/Library/Frameworks" | grep X

I audited the translator's whole base list that way rather than adding back the
single name the linker happened to print first; AudioToolbox and QuickLook came
out of that audit, not out of the log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
decode() calls Integer.parseInt on the four bytes after a '%', so a stored path
containing something like `%zzzz` threw NumberFormatException. In replaySpooled
that call sat OUTSIDE the try, so the throw went past all three completion
callbacks: the key stayed in SPOOL_IN_FLIGHT for the life of the process,
spoolBusy() stayed true, and every later reply-bearing request was silently
downgraded to a plain spooled message. It also aborted the rest of that drain
pass, so records queued behind the bad one were never looked at either.

Fixed at both levels, because either alone leaves the other exposed.

The try now covers the WHOLE body of replaySpooled rather than the delivery
alone. Every step in there parses bytes some earlier build wrote -- the framing
search, the substrings, the percent decode, the Base64 -- and guarding only the
ones already seen to fail is what let the next one strand a claim. The raw path
is kept before decoding so the handler can still name the record when decoding
is what failed, and both delivery calls queue rather than run listeners inline,
so a throw from one has not already spent `delivered` or `dropped`.

decode() itself no longer throws: a '%' not followed by four hex digits is
literal text. That is the same hazard at two more call sites -- both enumerate
peer-published item paths, where one corrupt byte took out the whole
enumeration -- and wrapping each caller would have left the third to be found
later. encode() escapes everything outside [A-Za-z0-9_-] and always writes four
hex digits, so nothing it produces reaches the lenient branch; verified by
round-tripping the encoder and by feeding the decoder every malformed shape I
could construct (bare %, short %0, non-hex %zzzz, doubled %%, trailing %).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 62e51c9d56

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

shai-almog and others added 2 commits August 14, 2026 13:13
Rejecting a P1 that says an untrusted local app can bind the exported
WearableListenerService and hand us a forged message or deletion. It cannot, and
our own comments are what said otherwise -- so the fix is to correct them rather
than to add a check.

Verified against play-services-wearable 18.2.0, not from memory:

  WearableListenerService.onBind is FINAL. There is no override point, which is
  also why the suggested remedy -- authenticate the binder caller -- cannot be
  written. It is final because the library does it for us.

  The binder it returns (com.google.android.gms.wearable.zzag) starts every
  dispatch with Binder.getCallingUid() and, for any uid not already accepted,
  calls UidVerifier.isGooglePlayServicesUid. That requires the calling uid to own
  the com.google.android.gms package AND that package to pass
  GoogleSignatureVerifier.isGooglePublicSignedPackage. A uid that fails logs
  "Caller is not GooglePlayServices; caller UID: n" and the dispatch returns
  having invoked nothing.

  WearableListenerService declares no onStartCommand, so the started-intent route
  delivers nothing either, and we override neither method.

A local app can therefore bind or start this service and still never reach
onMessageReceived, onDataChanged or ensureAppRunning.

What the node checks actually do is bound which PEER an authentic Play services
delivery may claim to be from -- a weaker and different question, and the one
dataItemExists answers properly for data changes. Saying they were the barrier
against a local attacker overstated both the threat and the mitigation, and is
what generated this finding; it would have generated it again.

The argument lives in the class javadoc, in the manifest comment that exports
the service, and next to isKnownNode, because those are the three places someone
asks the question.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… parser

Three things, one of them mine to answer for.

**NativeLookup has no business in our own code.** registeredInterfaceName()
parsed `NativeLookup.register(...)` lines out of the generated stub. It had no
callers -- it was left behind when the reachability walk was deleted -- and it
should never have existed: NativeLookup is the user-level native-interface
mechanism, not something the framework reasons about. Gone, along with the
orphaned javadoc the same deletion stranded above writeHealthBindings and the
dead Executor.scanningClass hook that walk was the only consumer of.

**`ld: framework 'Vision' not found`, the third round of this.** The watch
target mirrors the app target's frameworks phase and drops what
WATCH_OPTIONAL_FRAMEWORKS names. Last time I audited ByteCodeTranslator's base
list and said so; that was the wrong set. IPhoneBuilder's API scan adds more --
Vision, CoreImage, CoreNFC, CoreTelephony, JavascriptCore, AdSupport -- and each
only reaches the watch link in a project that uses the feature, so they arrive
one CI round at a time. All six are absent from both watch SDKs, checked, and
all six are now declared. CN1Vision.m was already `#if !TARGET_OS_WATCH`; as
before, only the link list lagged.

A deny list is silent about what nobody classified, so it will keep doing this.
WATCH_LINKABLE_FRAMEWORKS now names the ones the watch may link, and a test
asserts the two lists PARTITION every quoted `*.framework` literal IPhoneBuilder
can emit. Adding a framework without classifying it fails that test on the
commit that adds it instead of in an iOS job forty minutes later. Verified by
removing Vision and watching it fail with the framework named. It cannot see
ios.add_libs or the other module's base list, which is stated where it matters.

Matching is now case-insensitive: IPhoneBuilder writes "JavascriptCore" with a
lowercase s, which resolves only because macOS filesystems are, and an exact
match would have missed it. Both SDKROOT branches -- frameworks and raw
libraries -- were changed; only fixing the first would have left the dylib arm
reading the old way.

**#if TARGET_OS_WATCH == 0 excludes the watch** (BuildDaemon#164 r3781548389).
`== 0` and `!= 1` are the unary `!` written out, and the same reading already
existed for the other platforms, where it means the opposite -- there a false
test is the arm the watch DOES compile. Only the unary spelling was recognized
here, so a branch that cannot compile on watchOS was kept.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f0599e0857

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

shai-almog and others added 2 commits August 14, 2026 19:35
watchUsesHealth's javadoc still described the per-root walk that was deleted --
"when the watch has its own watchMain it shakes from its own root and the
phone's usage says nothing". The body says the opposite and has since the
deletion. A reviewer read the doc and filed the walk's removal as a bug.

The doc now states the decision that is implemented: app-wide, with
watchNative.health overriding either way. The concern it used to describe is
real and still answered -- a non-health watch app in a project whose phone uses
HealthKit sets watchNative.health=false rather than having it guessed -- and
the reason inference lost is recorded: the walk did not work, and guessing wrong
the other way omits the entitlement from a watch app that does use HealthKit,
which fails at runtime instead of at signing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…me out

ensureAppRunning() calls startActivity() from a service, which Android 10 and
later refuse. The comment there was right that nothing is LOST -- messages and
removals are spooled before it is attempted -- but wrong by omission about
requests: a cold process has no Display and no registered listener, so
spoolOrDeliverRequest degraded every reply-bearing request to a plain message
and the sender sat out the full 30-second timeout for an answer that provably
was not coming. Reachable-but-not-running is the normal state of a phone app
whose watch just asked it something, so this was the common case, not the edge.

The receiver now says so. A new /cn1/replyerr/<token> path carries a reason back
to the asking node, and the requester turns it into the error arm of
deliverReply -- the same arm the timeout would eventually have taken, minus the
thirty seconds. The request is still spooled and still replays as a plain
message on the next launch, which is what it has become by then.

Sent BEFORE the spool write, so a process killed mid-write still frees the
sender, and best-effort: if it cannot be sent the deadline is still there, which
is exactly today's behaviour.

Only ever travels between two devices running this bridge -- the Data Layer
pairs Android with Wear, and the Apple side answers through WatchConnectivity's
own reply handler -- so no iOS or JavaSE change is implied. A peer on an older
build does not match the path, ignores the message and waits out its timeout as
before, so this degrades rather than breaks.

What it is NOT: a way to run the app headlessly. Registering the app's listeners
means running app code, and there is no permitted background path to that
without a new callback interface. Reporting the truth quickly is the fix that
does not invent API.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 79c5580ed1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/developer-guide/Wearables.asciidoc Outdated
shai-almog and others added 2 commits August 15, 2026 08:18
… no companion

`ld: framework 'VisionKit' not found` -- the fourth of these, and the guard I
added last time did not catch it. It scanned IPhoneBuilder for quoted
"X.framework" literals, but PlatformFeatureCatalog names frameworks BARE and
IPhoneBuilder appends the suffix, so VisionKit and Speech had no literal to
find. Both are absent from both watch SDKs; both are now declared.

The test now reads all three sources a framework can arrive from --
IPhoneBuilder, MapsProviderInjector, and the catalog in the other module -- and
matches the catalog's `.iosFrameworks("VisionKit", ...)` form as well as
literals. Verified by removing VisionKit and watching it fail, naming it.

That verification failed the first two times I ran it, and the reason is worth
recording: WATCH_OPTIONAL_FRAMEWORKS is a static final String, so javac inlines
it into the test class. Editing the list without touching the test left a stale
copy compiled in and the test passed on a list it was no longer reading. The
test now reads both constants reflectively. CI builds clean and would never have
seen this, which is exactly why it was worth ruling out.

Also, the guide's "by default the watch app is a companion" is only true on
Apple Watch (#5487 r3787443009). AndroidGradleBuilder builds the Wear app in the
standalone branch only, and the builder already logs that at build time; the
introduction now says so instead of contradicting its own section 391-398 four
hundred lines later.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
windows-latest failed with three 504s from community.chocolatey.org over about
seven minutes, then the verify step correctly refused to continue without
ffmpeg. The retry-and-verify hardening did its job -- it named the cause in the
log instead of letting the smoke test die later with a FileNotFoundError that
mentions neither ffmpeg nor the feed -- but retrying cannot help when the outage
outlasts the retry budget.

So after the retries, download a build from GitHub instead. That is not a second
flaky mirror: if github.com is unreachable the job could not have checked the
repository out, so it is the one download whose failure is never a mystery.

The directory goes on GITHUB_PATH as well as $env:PATH, because the smoke test
runs in a later step and only sees the former.

Chocolatey is still tried first, and still preferred when it answers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

pointerPressedC(xs, ys, 1);

P2 Badge Gate watch touch input until Java is ready

When the user touches the watch immediately after onAppear, cn1_watch_runtime_start() has only launched the VM on a background pthread, so cn1WatchJavaLifecycleReady may still be false. The generated Swift gesture is already active, however, and this call reaches IOSImplementation.pointerPressedCallback(), which dereferences the still-null static instance; drag and release have the same issue. Ignore or queue pointer events until the lifecycle-ready flag is set, as the phase-delivery path already does.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants