Skip to content

B.1: watchOS support for LoopKit, OmniBLE, G7SensorKit (sub-project of standalone watchOS Loop) - #1

Draft
threecee wants to merge 98 commits into
mainfrom
personal
Draft

B.1: watchOS support for LoopKit, OmniBLE, G7SensorKit (sub-project of standalone watchOS Loop)#1
threecee wants to merge 98 commits into
mainfrom
personal

Conversation

@threecee

Copy link
Copy Markdown
Owner

What this is

Sub-project B.1 of a longer program to build a standalone watchOS Loop variant (watch talks BLE directly to DASH + G7 with the phone absent). This PR is a fork-internal review view on threecee/LoopWorkspace from personalmain. It is not intended for upstream submission yet — upstream PRs on each of the ported packages are scheduled to publish only after sub-project B.2 demonstrates real BLE on watchOS.

Full spec: /Users/carl/Projects/loop-and-learn/docs/superpowers/specs/2026-04-20-b1-watchos-port-design.md
Full plan: /Users/carl/Projects/loop-and-learn/docs/superpowers/plans/2026-04-20-b1-watchos-port.md

Scope — what B.1 does

Platform-compile work only. No new behaviour, no new algorithm, no BLE changes. Each ported Swift package builds cleanly for watchOS 10+ with iOS behaviour unchanged.

Three packages ported (the original spec said four; LoopAlgorithm turned out to live inside LoopKit at LoopKit/LoopKit/LoopAlgorithm/ as part of the LoopKit target — see §10 Amendment in the spec):

PackageFork branchCI
LoopKit (+ embedded LoopAlgorithm)threecee/LoopKit@watchos-support
OmniBLEthreecee/OmniBLE@watchos-support
G7SensorKitthreecee/G7SensorKit@watchos-support

CI failures on the first two runs were debugged to configuration issues (hardcoded Xcode version, transient submodule pin) rather than port defects; re-runs are in flight at PR-creation time. All three packages build locally for both iOS and watchOS via LoopWorkspace.xcworkspace.

What's in this PR

11 commits on personal, all submodule-pointer changes in LoopWorkspace:

  • .gitmodules re-points LoopKit, OmniBLE, G7SensorKit at the threecee forks.
  • Three submodule pins advanced to the watchos-support tip of each fork.
  • Interim pin corrections as CI-scope issues were debugged.
  • .gitignore excludes scratch audit artefacts (.b1-*.txt).

Real code changes live in each package fork's watchos-support branch — not in this LoopWorkspace repo.

Methodology (same per package)

  1. Fork LoopKit/<Package>threecee/<Package>.
  2. Branch watchos-support from canonical default (dev for LoopKit/OmniBLE, main for G7SensorKit).
  3. Audit iOS-only surface: imports (UIKit, BackgroundTasks, UserNotifications-iOS-specifics, LocalAuthentication), API call sites (BGTaskScheduler, UIApplication, view controllers, biometrics, keep-alive audio).
  4. Extend xcodeproj build settings on the core framework target: SUPPORTED_PLATFORMS += "watchos watchsimulator", TARGETED_DEVICE_FAMILY = "1,2,4", WATCHOS_DEPLOYMENT_TARGET = 10.0, SDKROOT = auto. Where UI sources must be excluded on watchOS, use EXCLUDED_SOURCE_FILE_NAMES[sdk=watchos*]. Where iOS-only frameworks are linked, use platformFilter = ios.
  5. Gate iOS-only code with #if os(iOS) (matching upstream's existing gate style). Every gated call site that must exist on both platforms gets a TODO(B.3): stub pointing at the watchOS-equivalent API for B.3 to implement.
  6. Verify iOS didn't regress (build + tests green, test count unchanged).
  7. Add CI matrix covering both iOS and watchOS.

Findings worth a reviewer's eye

  • LoopKit had a stale LoopKit-watchOS target from 2019 with WATCHOS_DEPLOYMENT_TARGET = 6.1 and 43 missing sources. Extending the existing target (rather than creating a new one) preserves maintainer-established structure. No iOS-only code in LoopKit's core module needed gating — surprisingly clean.
  • OmniBLE needed more gating: UIKit-based pod-pairing UI, keep-alive audio (AVAudioSession), UIApplication lifecycle observers, background-task scheduling, Face ID. Handled with a combination of EXCLUDED_SOURCE_FILE_NAMES (for whole UI subtree) and per-call-site #if os(iOS) gates with TODO(B.3): stubs. One type (ReservoirLevelHighlightState) lives inside PumpManagerUI — gated out of watchOS; upstream may prefer promotion into the core framework at PR-time.
  • G7SensorKit was the lightest port: one unused import LoopKitUI stripped, no gates needed.
  • No watchOS test bundles were created from scratch. Existing test targets either don't exist (OmniBLE, G7SensorKit) or pull in out-of-scope UI types (LoopKit). Framework-level watchOS build verification satisfies B.1's link-correctness bar.

Deferred to B.3 (follow-up sub-project)

Every gated-out iOS path carries a TODO(B.3): marker:

  • OmniBLE: 5 markers in OmniBLEPumpManager.swift — lifecycle observers, pod keep-alive, background tasks, ReservoirLevelHighlightState promotion.
  • LoopKit, G7SensorKit: 0 markers (nothing needed gating).

How to review

If you want to see the actual Swift changes, review each fork's watchos-support branch directly. The commits here are purely submodule-pointer advances.

threecee/LoopKit watchos-support https://github.com/threecee/LoopKit/commits/watchos-support
threecee/OmniBLE watchos-support https://github.com/threecee/OmniBLE/commits/watchos-support
threecee/G7SensorKit watchos-support https://github.com/threecee/G7SensorKit/commits/watchos-support

Status

  • Draft because the upstream-publication milestone (demonstrated real BLE on watchOS via sub-project B.2) hasn't been reached. This PR is for self-review and visibility.
  • Phase 5 (WatchOSSmokeTest target proving the link graph closes on a watchOS Simulator) is next in the B.1 plan and will add more commits here.

threecee-claude

threecee-adskand others added 30 commits April 20, 2026 19:11
Pins LoopKit to 522cba34 on threecee/LoopKit watchos-support:
- LoopKit-watchOS framework target now builds cleanly for watchOS
with WATCHOS_DEPLOYMENT_TARGET=10.0 and all 182 LoopKit sources
(was 139, missing 43 files including all LoopAlgorithm components).
The CI workflow commit (371cbc2f on the local watchos-support branch)
is not yet pushed to origin because the active gh token lacks 'workflow'
scope. Once the scope is refreshed, the 1 additional commit should be
pushed and this submodule pin advanced one more step.
G7SensorKit now builds for both iOS and watchOS via the
LoopWorkspace scheme `G7SensorKit`:
- iOS: xcodebuild build -workspace LoopWorkspace.xcworkspace \
-scheme G7SensorKit -destination "generic/platform=iOS"
BUILD SUCCEEDED.
- watchOS: xcodebuild build -workspace LoopWorkspace.xcworkspace \
-scheme G7SensorKit -destination "generic/platform=watchOS"
BUILD SUCCEEDED.
- iOS tests: 15 tests in 3 suites, 0 failures.
Port content:
- Removes unused `import LoopKitUI` from G7DeviceStatus.swift.
- Adds a shared G7SensorKit.xcscheme.
- Adds explicit LoopKit.framework link (needed once LoopKitUI is
excluded on watchOS — LoopKit was only reaching G7SensorKit
transitively through the iOS-only LoopKitUI link).
- Extends the G7SensorKit target's SUPPORTED_PLATFORMS to include
watchos/watchsimulator, sets WATCHOS_DEPLOYMENT_TARGET = 10.0,
and widens TARGETED_DEVICE_FAMILY to 1,2,4.
- Stamps LoopKitUI.framework link with `platformFilter = ios` so
watchOS builds skip it.
- Adds .github/workflows/build.yml mirroring OmniBLE's CI pattern
(clone LoopWorkspace/personal, repoint submodule, drive xcodebuild
through the workspace).
Out of scope (stay iOS-only): G7SensorKitTests, G7SensorKitUI,
G7SensorPlugin. No watchOS test bundle was created; see
.b1-g7sensorkit-testcount.txt for the rationale.
Creates SmokeTests.swift with four functions that force symbol resolution
of GlucoseTrend (LoopKit), LoopAlgorithmSettings (LoopAlgorithm via
LoopKit), OmniBLEPumpManager (OmniBLE), and G7CGMManager (G7SensorKit).
Updates ContentView.swift to display all four smoke-test results plus a
green "All linked." confirmation. Fixes framework linking so LoopKit,
OmniBLE, and G7SensorKit are linked into the Watch App target (not the
iOS container), enabling the workspace to build them for watchOS when
the Watch App is the destination.
Build verified: generic/platform=watchOS BUILD SUCCEEDED.
Simulator note: watchOS Simulator build fails because OmniBLE's
xcodeproj EXCLUDED_SOURCE_FILE_NAMES uses sdk=watchos* (device-only)
rather than sdk=watch* (device+simulator), so PumpManagerUI files that
import LoopKitUI are not excluded when building for watchsimulator. This
is an OmniBLE porting bug (Phase 3 scope), not a smoke-test issue.
threecee-claude
… App target
The Xcode template placed the Embed Frameworks phase in the container
(watchapp2-container) target, but since the Watch App is marked
WKWatchOnly=YES, the container is vestigial and nothing it embeds
reaches the installed app. Move the phase (and its framework entries)
to the Watch App target so LoopKit, OmniBLE, and G7SensorKit ship
inside the .app bundle.
Verified: simctl install + launch on Apple Watch Series 11 (46mm)
watchOS 26.4 simulator shows 'All linked.' in green.
Picks up:
- OmniBLE (b4-start..5fb011a, 4 commits): payload validity 60s → 600s
+ doc comment (LoopKit#6); PhoneWatchSettingsSync v2 with automaticDosing
fields + protocol version bump (LoopKit#3); shared HandoffState.currentOwner
extension for policy-engine wiring (LoopKit#2).
- Loop (b4-start..b5e94f7a, 4 commits): iOS LoopAppManager wires
settingsSyncProvider closure (closes production gap discovered
during planning) + watch WatchSettingsSnapshot reads dosing flags
from sync (LoopKit#3); HandoffOrchestrator wires HandoffPolicyEngine
markX hooks on both sides via Combine reachability subscription
with 60s debounce (LoopKit#2); review-fix renames + safety comment.
After this, the watch can self-drive when the phone reports
automaticDosingEnabled, the auto-revert path is live (was dead code
since B.3.a), and the cached-pod-state safety gate is active.
BEHAVIOR CHANGE: auto-revert from watch back to phone WILL FIRE in
.automatic and .manualWithAutoRevert modes when the phone reconnects
after being out of range for >=60s. Worth deliberate hardware testing
on the next install.
Verified:
- OmniBLETests targeted (3 suites): 26/26 PASS
- LoopTests targeted (3 suites): 27/27 PASS
- WatchApp ExtensionTests targeted (2 suites): 19/19 PASS
- Release archive: ARCHIVE SUCCEEDED (iOS, generic destination)
threecee-claude
Picks up:
- Loop (b6-start..HEAD, 8 commits): WatchAlgorithmDriver overrides
didRecommend with 5 gates (warmingUp, automaticDosingEnabled,
isAutomaticDosingAllowed, pumpManager non-nil, deliveryIsUncertain)
+ suppression recording + dose dispatch (was structurally absent
before — comment at line 277 said "Phase 6+ will refine"). Bootstrap
wires OmniBLEOwnership.pumpManager through. Race ordering hardened in
HandoffOrchestrator. Driver source extracted to new shared
WatchAlgorithmKit framework (multi-platform iOS+watchOS) so it can
be exercised by OmniBLETests integration tests. Defensive
schedule-copy in driver init catches silent-failure mode where
algorithm reads schedules from doseStore but production never
populated them. ClockKit gate fixed.
- OmniBLE (b6-start..HEAD, 3 commits): new WatchAlgorithmEndToEndTests
integration suite (4 tests, ~5 min total) exercising the full
closed-loop chain against the emulated pod via PodSimulatorTestCase
infrastructure — proves the watch can actually close the loop
end-to-end with real BLE encryption + real pod state machine + real
insulin accounting. Includes happy-path test (pod insulinDelivered
increases or temp basal installs after algorithm iteration) plus
3 gate-suppression sub-tests verifying each gate prevents the dose
through full production plumbing.
After this, the audit gap from B.4 is closed at the integration level:
the watch can actually enact doses, the gates work as designed both at
unit and integration levels, the schedule-copy silent-failure mode is
prevented, and the integration test proves the full chain works under
realistic conditions.
BEHAVIOR: After Build N+1 ships, the watch can self-drive the pod
when handoff state is .watchDriver, settings sync has populated
automaticDosingEnabled+true, and warmup window has cleared. Worth
deliberate hardware testing on the next install (the integration
test passes against the emulated pod, not yet against real hardware).
Verified:
- OmniBLETests targeted (4 suites): 30/30 incl. WatchAlgorithmEndToEndTests 4/4
- LoopTests targeted (3 suites): 27/27 incl. HandoffOrchestratorTests 11/11
- WatchApp ExtensionTests targeted (3 suites): 26/26 incl. WatchAlgorithmDriverTests 7/7
- Release archive: ARCHIVE SUCCEEDED (iOS, generic destination)
threecee-claude
Picks up:
- OmniBLE (b5-start..HEAD, 4 commits): commandsAllowed gate flag on
OmniBLEOwnership + callback-based gate in OmniBLEPumpManager
enactBolus/enactTempBasal (#1); claimedOwner field on
PhoneWatchHeartbeat + PhoneWatchProtocol.currentVersion 2 -> 3 (LoopKit#5);
HandoffStatePersistence + state machine restore-on-init (LoopKit#4);
command-gate integration sub-test in WatchAlgorithmEndToEndTests.
- Loop (b5-start..HEAD, 4 commits): orchestrator effect cases wire
ownership.commandsAllowed flips on both iOS + watch (#1); both
PhoneWatchSessionCoordinators populate claimedOwner in heartbeats
+ watch demotes itself silently on split-brain detection (LoopKit#5);
watch pump manager construction reads from WatchSettingsCache
instead of .watchSideDefault (LoopKit#7); WatchDoseRecoveryStore tripwire
in WatchAlgorithmKit + ExtensionDelegate launch-time stale check;
test pollution fix on iOS + watch HandoffOrchestratorTests +
SettingsSyncEmissionTests + protocol version assertion bump.
After this, the audit gaps from the B.4 critical review are closed:
- Real protection during handoff transitions (no mid-handoff double-
command races)
- Split-brain detection with phone-wins arbitration
- Crash recovery via persistence (HandoffState survives app restart)
+ dose-in-flight tripwire on watch
- Watch pump manager uses real settings, not hardcoded defaults
BEHAVIOR: The new gate and split-brain demotion will fire when expected
(forced handoffs, transient WCSession glitches). Worth observing on
hardware install of next build.
Verified:
- OmniBLETests targeted: all suites green incl. WatchAlgorithmEndToEndTests 5/5
- LoopTests targeted: all green (HandoffOrchestratorTests 13/13,
HandoffPolicyEngineTests 11/11, SettingsSyncEmissionTests 5/5)
- WatchApp ExtensionTests targeted: all green (33/33 across 4 suites)
- Release archive: ARCHIVE SUCCEEDED (iOS, generic destination)
threecee-claude
- WatchSettingsCache.resetForTesting() wrapped in #if DEBUG
- Em-dash -> semicolon in watch split-brain log message
- B.5.1 LoopKit#2 audit confirmed synced settings reach watch algorithm
- Two larger B.5.1 items (timeZone field, gate widening) deferred to B.5.2
threecee-claude
Picks up:
- OmniBLE (b5.2-start..HEAD, 2 commits): commandsAllowedCheck gate
widened to runTemporaryBasalProgram, cancelBolus, suspendDelivery
(#5b); PhoneWatchSettingsSync.timeZone: String? field + protocol
bump 3 -> 4 (LoopKit#3).
- Loop (b5.2-start..HEAD, 1 commit): HandoffBlockedError +
StatusTableViewController handoff-blocked rendering for cancelBolus
(#5b); HandoffOrchestrator + LoopAppManager populate timeZone (LoopKit#3) +
observe NSSystemTimeZoneDidChangeNotification (#3b);
WatchAlgorithmDriver resolves scheduleZone from sync.timeZone and
threads to all 4 dailyItems schedule constructors (LoopKit#3).
After this:
- The 3 newly-gated pump methods short-circuit during the sub-second
handoff window. cancelBolus's iOS UI renders "Handoff in progress,
please try again in a moment" via HandoffBlockedError; suspendDelivery
is OmniBLE-PumpManagerUI-only (out of Loop UI scope).
- Watch interprets all 4 dailyItems schedules in the phone's zone;
phone-side TZ changes propagate to the watch within one WCSession
round-trip via the new NSSystemTimeZoneDidChangeNotification observer.
Verified:
- OmniBLETests targeted: all suites green (104 tests; WatchAlgorithmEndToEndTests
5/5 after retry to clear cold-start noPodsFound flake)
- LoopTests targeted: 31/31 green
- WatchApp ExtensionTests targeted: 36/36 green
- Release archive: ARCHIVE SUCCEEDED (iOS, generic destination)
threecee-claude
- Test for malformed sync.timeZone identifier fallback path
- Docstring fix on notifySettingsChanged() reflecting actual callers
threecee-claude
Picks up:
- Loop (b7-start..HEAD, 3 commits): LoopStateView inner-circle driver
indicator (iOS), StatusTableViewController subscription wiring via
LoopCompletionHUDView forwarding properties, watch HUD driverDot
overlay (both ActionHUDController + ChartHUDController scenes) +
HUDInterfaceController subscription with willActivate/didDeactivate
lifecycle.
After this:
- iPhone main HUD shows a small white dot inside the loop ring when
the phone is driving the pod. Dot disappears when the watch takes
over.
- Watch HUD shows the same dot when the watch is driving. Dot pulses
(~0.8s alpha cycle) on either side during the sub-second handoff
transition.
- No effect on closed-loop semantics or pod control — purely visual.
- OmniBLE submodule untouched (still at b5.2-complete 1135438).
Verified:
- LoopTests targeted: 27/27 (LoopStateViewTests 7/7, HandoffOrchestratorTests
13/13, SettingsSyncEmissionTests 7/7) — all green
- WatchApp ExtensionTests targeted: 27/27 (HandoffOrchestratorTests 15/15,
WatchAlgorithmDriverTests 12/12) — all green
- Release archive: ARCHIVE SUCCEEDED (iOS, generic destination)
threecee-claude
OmniBLE: PumpStatusSnapshot + AlgorithmStateSnapshot payload types,
PhoneWatchMessage.algorithmStateSnapshot enum case.
Loop: AlgorithmStateSnapshotEmitter + LoopDataManager hook + LoopAppManager
wiring + no-op switch arms in all four PhoneWatchMessage consumers + B.7
visual polish (driver-indicator dot size + tint).
threecee-claude
…ing)
Loop changes since Phase 1:
- WatchAlgorithmSnapshotCache (T8) + safety+test cleanup
- Watch-side coordinator dispatch into snapshot cache (T9) + tautology cleanup
- WarmUpDecider 3-gate decision (T10) + boundary-test symmetry
- WatchAlgorithmDriver init derives isWarmingUp from WarmUpDecision (T11)
+ dual-input precedence doc + DEBUG assertion
- WatchAlgorithmBootstrap invokes WarmUpDecider with real freshness inputs (T12)
threecee-claude
OmniBLE: resumeDelivery gated on commandsAllowedCheck (Codex
adversarial review #1 / B.5.2 deferred LoopKit#5). manualRecoveryDismiss
emits .resumeIssuingPodCommands for surviving owner (LoopKit#2). Init
checks .handoffPending deadline against persistence and converts
expired pending to .recovering(.restoredExpiredPending,...) (LoopKit#3).
Mechanical /simplify bucket: HandoffSettings.appGroupDefaults +
UserDefaults+Codable extension applied across both submodules.
Tagged b8.1-complete on all three repos. Build 873 prep.
threecee-claude
OmniBLE: HandoffStateMachine.init always recovers any restored
.handoffPending (M3 — drops deadline check; vacuously closes M2 + M1
dir.origin cleanup). PhoneWatchProtocol.currentVersion bumped 4 → 5.
Loop: live LoopSettings + dosingEnabled observer wires HandoffOrchestrator
into LoopDataManager.loopAlgorithmRunner(_:settingsDidChange:) via the
WatchHandoffNotifying protocol. Phone-side and watch-side settings-sync
dedup. Snapshot delivery via updateApplicationContext (latest-only,
intentionally overwrites) instead of transferUserInfo. 8KB warning
log so B.8.4 inherits size-budget telemetry. Heartbeat encoder
secondsSince1970. WatchSettingsCache.resetForTesting() DEBUG-guard
regression test.
Tagged b8.2-complete on all three repos. Build 874 prep — push gated
on Carl's confirmation that Build 873 (B.8.1) HV-1 passed.
threecee-claude
Loop: WatchSettingsCache adds CurrentValueSubject publisher + persists
last-good to App Group UserDefaults (B.8.1 UserDefaults+Codable
extension). WatchAlgorithmBootstrap and WatchRemoteCommandBootstrap
subscribe; retryIfNeeded() gates on lastHandoffState == .watchDriver.
B.8.2 #if DEBUG writeCount instrumentation removed in favor of
sink-based testing. 5x NSLog -> OSLog (4 in PhoneWatchSessionCoordinator,
1 in WatchAlgorithmBootstrap).
OmniBLE submodule UNTOUCHED -- pointer stays at b8.2-complete.
Tagged b8.3-complete on all three repos. Build 875 prep.
threecee-claude
…nciliation
Loop: AlgorithmStateSnapshotEmitter populates rolling buffers from
runner inputs. WatchAlgorithmDriver.applyAlgorithmStateSnapshot writes
buffers to watch stores via upstream LoopKit batch-insert APIs (with
NewGlucoseSample/NewCarbEntry projections; doses pass through).
WatchAlgorithmDriver's .skipWarmup case hydrates inside the switch
(snapshot is in scope via WarmUpDecision's associated value). Phase 6
caught + fixed a production bug: DoseStore.addDoses calls its
completion handler twice; Phase 3's continuation now NSLock-guarded.
Wire format: file-pointer fallback for oversized snapshots (Phase 1
measured 21.25KB; >8KB threshold). Phone writes to AppGroup/snapshot.json,
sends pointer message via applicationContext. Watch reads file on
pointer receipt, validates monotonic sequence.
LoopAlgorithmReconciliationTests harness ships with placeholder fixtures
+ stub runForReconciliation body. Tests XCTSkip cleanly. Real fixtures
captured manually via Phase 5a's #if DEBUG instrumentation in follow-up.
OmniBLE: AlgorithmStateSnapshot.isFreshEnoughForSkipWarmup helper.
PhoneWatchMessage.algorithmStateSnapshotPointer(sequence:) wire-format
case + PhoneWatchProtocol.currentVersion bumped 5 to 6.
Closes Codex adversarial review Loop LoopKit#3 high. Closes the umbrella
B.8.x adversarial-review-hardening series. algorithm-fidelity-audit
PASS verified at /tmp/algorithm-fidelity-audit-2026-05-03.md.
Tagged b8.4-complete on all three repos. Build 876 prep.
threecee-claude
Activates the previously-stubbed reconciliation test infrastructure:
WatchAlgorithmDriver.runForReconciliation now delegates to
LoopAlgorithm.generatePrediction (Path α — wrapper-only fixture, no
extension needed since LoopPredictionInput is self-contained); 3
real fixtures (steady-state, post-meal-carbs, predicted-hypo)
replace the placeholder {} JSON files via a new programmatic
ReconciliationFixtureGenerator (gated on
GENERATE_RECONCILIATION_FIXTURES=1).
Tests: 3/3 PASSING (no XCTSkip):
-[LoopTests.ReconciliationTests test_postMealCarbs_...] passed
-[LoopTests.ReconciliationTests test_predictedHypo_...] passed
-[LoopTests.ReconciliationTests test_steadyState_...] passed
threecee-claude
Lands Stream B's commit 2b9dbbcf (rebased from 608568f7) on top of
Stream A's reconciliation activation. Closes the pre-existing
ExtensionDelegateApplicationContextTests.testDidReceiveApplicationContextDispatchesToHandler
failure that was carrying forward from B.8.2 → B.8.3 → B.8.4.
Root cause: test fixture used JSONEncoder .iso8601 dateEncodingStrategy
while transport adopted .secondsSince1970 in B.8.2 (mechanical wire-
format migration). Decoder threw on ISO-8601 strings; handleIncomingMessageData
swallowed the error; onIncomingMessage never fired.
Fix: align fixture encoder to .secondsSince1970. One-line behavioral
change + 4-line wire-format-contract comment. WatchApp ExtensionTests
target now fully green.
threecee-claude
…cts cleanup
Closes the two B.7 follow-ups deferred from the 2026-05-02 algorithm-
fidelity audit. Loop submodule consolidates PredictionInputEffect to a
single canonical source in LoopAlgorithmCore (deleting the on-disk
orphan at Loop/Models/PredictionInputEffect.swift) and consolidates
both `enabledEffects` extensions to delegate via a new canonical
`PredictionInputEffect.allEnabled` static-let.
Phase 1 discovery found the dual-target pbxproj wiring confusion
described in the 2026-05-03 known-deviations note had already been
silently resolved — both PBXBuildFile entries already pointed at
LoopAlgorithmCore/PredictionInputEffect.swift, and the iOS Loop app
target had no Sources entry for the type at all (iOS callers use
LoopAlgorithmCore.PredictionInputEffect via existing
`import LoopAlgorithmCore`). So B.8.5's only filesystem cleanup was
deleting the orphan file; no production pbxproj surgery was needed.
Pure structural cleanup. Algorithm-fidelity-audit PASS verified
(/tmp/algorithm-fidelity-audit-2026-05-04.md). Test results:
LoopTests 303/303 (1 skipped), WatchApp ExtensionTests 121/121.
3 new invariant tests in PredictionInputEffectTests assert post-cleanup
invariant: `allEnabled == .all`, iOS `enabledEffects == .allEnabled`,
LoopAlgorithmCore `loopAlgorithmCore_enabledEffects == .allEnabled`.
Tagged b8.5-start + b8.5-complete on Loop + OmniBLE (untouched, both
tags at same SHA 07d5cc7) + LoopWorkspace.
Build 877 prep.
threecee-claude
Loop: slice-tag comment prefix strip across WatchHandoff stack, WatchApp
Extension, and WatchAlgorithmKit (~57 line-comment prefixes; doc comments
preserved). HandoffPolicyEngine tick 1Hz to 5s (efficiency #1, lockstep
across iOS+watch pair). ExtendedRuntimeCoordinator gated on
HandoffOrchestrator.handoffState == .watchDriver via Combine subscription
(efficiency LoopKit#6); 3 new gating tests + 3 existing tests updated.
OmniBLE: slice-tag comment prefix strip in Common/ (state machine,
state persistence, handoff payload, ownership coordinator). No
HandoffPolicyEngine copy on OmniBLE side.
algorithm-fidelity-audit NOT required (no algorithm-adjacent files
touched). LoopTests: 303 pass, 1 skipped. WatchApp ExtensionTests: 124
pass. Tagged b9-complete on all three repos. Build 878 prep.
threecee-claude
Mechanical strip of B.x.y / Phase n / Tn slice-tag prefixes from ///
doc-comments. ~50-70 lines across both submodules. No behavioral change.
Tagged b10.1-complete on all three repos. Build 879 prep.
threecee-claude
Lifts 5 iOS<->watch duplicated handoff-stack files into OmniBLE/Common
parameterized by HandoffRole: ShadowStateScheduler, PhoneWatchTransport,
PhoneWatchSessionCoordinator + HeartbeatScheduler, HandoffPolicyEngine,
HandoffOrchestrator. Adds HandoffStack.assemble(role:) factory that
deduplicates LoopAppManager + ExtensionDelegate bootstrap chains.
~2,675 LOC reduction in Loop submodule (dedup); +1,500 LOC in
OmniBLE/Common (single source of truth, with role-conditional behavior
folded into the lifted types).
Tests deduplicated: 76 -> 51 paired methods, all green. Phase 8 adds 3
HandoffStack factory tests (phone/watch/reentrancy). Drive-by re-add of
WatchApp ExtensionTests/PhoneWatchTransportMock.swift (deleted in
Phase 6 but still referenced by watch-only WatchPumpManagerSettingsTests
- pre-existing watch-test-target build regression that B.10 closes).
algorithm-fidelity-audit PASS (Class A pure structural). Closes
/simplify quality #1 + LoopKit#5. Report:
/tmp/algorithm-fidelity-audit-2026-05-04-b10.md.
Tagged b10-complete on Loop, OmniBLE, and LoopWorkspace. Build 880 prep.
Recommend HV-1 within 2 weeks of ship to verify no subtle handoff
regressions in real-hardware paths.
threecee-claude
Untracked .claude/ dir was causing release.sh to abort on the
working-tree-clean precondition. Mirrors the loop-and-learn repo's
gitignore narrowing pattern.
threecee-claude
Advances Loop and OmniBLE submodules to b11.0-complete. B.11.0 adds:
- aps-environment entitlement to WatchApp Extension target
- WKExtension.shared().registerForRemoteNotifications() on launch
- new PhoneWatchMessage.apnsTokenPublish case (proto v6 -> v7)
- APNsTokenStore App-Group persistence + symmetric exchange
- PhoneWatchTransportQueueing protocol seam
NO Nightscout integration (B.11.1/B.11.2). Class A additive transport.
algorithm-fidelity-audit PASS (zero scope files modified).
Phase 0 (Apple Developer portal APNs enablement + provisioning regen)
remains a Carl-action gate; required before next TestFlight upload
registers for live APNs. Build target: 884.
threecee-claude
B.11.1 RemoteCareUploader role parameterization. New OmniBLE-side
protocol abstracts the Nightscout upload trigger surface; iOS uses
LoopRemoteCareUploader wrapping RDSM; watchOS reuses the same
wrapper (Branch A confirmed in Phase 1 — NightscoutServiceKit links
into watchOS via the static-link path).
HandoffOrchestrator owns the driver-only-writes invariant and
quiesce-on-handoff semantics. 13 iOS callsites + 1 watchOS
bootstrap migrated to proxyUpload(for:). Per-iteration dedup
verified by RemoteCareDedupE2ETests across a simulated phone->watch
handoff window.
algorithm-fidelity-audit verdict: PASS / Class B (additive plumbing).
Special-check 5.5 (LoopDataManager refactor) PASS — proxy is a
pre-RDSM filter, not an interceptor; dosing decision payload bit-
identical when device is current driver.
NightscoutService not touched (Branch A). Tagged b11.1-complete on
all three submodules + LoopWorkspace. Build 885 prep.
threecee-claude
DriverTokenRendezvous Codable type with HMAC-SHA256 signing
(CryptoKit). HandoffOrchestrator gains buildSignedRendezvous()
method that constructs + signs the rendezvous from the App Group
APNs token store (B.11.0), guarded by the existing isCurrentDriver
gate (B.11.1 driver-only-writes invariant). Empty Nightscout API
secret degrades to empty signature with debug-log warning (spec
Risks LoopKit#4). End-to-end token-rotation propagation tests in Loop
exercise the B.11.0 -> B.11.2 producer chain.
Production splice into Nightscout devicestatus
(loop.testingDetails.driverToken — riding LoopStatus.testingDetails
since LoopStatus is in external NightscoutKit and adding a sibling
key would require forking it) is deferred to B.11.2.1 since the
slice scope per Carl's instruction was Loop + OmniBLE submodules
only. Producer side (orchestrator-side build + sign) fully
implemented and tested.
Algorithm-fidelity-audit: PASS (Class A/B — additive metadata).
No algorithm-output path touched.
Tagged b11.2-complete on all three repos. Build 886 prep.
threecee-claude
…splice
NightscoutService: first time touched in B.11. Adds the consumer-side
splice for B.11.2's DriverTokenRendezvous producer — caretaker apps now
find the signed rendezvous at JSON path `loop.testingDetails.driverToken`
in devicestatus uploads. NightscoutService b11.2.1-complete: b39af55.
Loop: wires `driverTokenProvider` on active NightscoutService instances
at boot, plus passes `nightscoutAPISecretProvider` into the handoff
stack so the orchestrator can sign rendezvous payloads with the active
Nightscout API secret. Loop b11.2.1-complete: 7d7fe81.
OmniBLE: UNTOUCHED in this slice. Tag-only no-op (`b11.2.1-start` and
`b11.2.1-complete` both at `b11.2-complete`/1e1b677).
Algorithm-fidelity audit: PASS Class A/B (additive metadata only).
Report: /tmp/algorithm-fidelity-audit-2026-05-04-b11.2.1.md
known-deviations.md update: /tmp/known-deviations-b11.2.1-entry.md
(permission on the source of truth was denied during the slice;
manual merge required).
Tests:
- NightscoutServiceKitTests (Shared scheme, iOS sim): 16/16 pass
(4 new + 12 existing).
- LoopTests (iPhone 17 sim): 379 pass, 1 skip, 6 pre-existing locale
flakes (BolusEntryViewModelTests / SimpleBolusViewModelTests
decimal-comma — unrelated to slice). All 3 B.11.2.1 splice tests
pass; all 3 B.11.2 propagation tests still pass.
- WatchApp ExtensionTests build: TEST BUILD SUCCEEDED.
Carl-action gated: release.sh Build 887.
threecee-claude
B.11.3 ties B.11.0 (APNs entitlements + token publication) + B.11.1
(RemoteCareUploader role-gating) + B.11.2 (DriverTokenRendezvous +
currentDriver field) into a coherent pending-driver handshake.
Sequence:
1. Outgoing driver writes a devicestatus with currentDriver: incoming
(the rendezvous) BEFORE the BLE role flips. Pre-flip publish is
fire-and-forget — Option D (Carl-decided) — because B.11.1 landed
RemoteCareUploader as fire-and-forget without a Result-returning
surface.
2. Caretaker apps polling Nightscout pick up the new currentDriver
within their poll window (LoopCaregiver: 1-5 min, LoopFollow:
configurable).
3. BLE bond moves; HandoffStateMachine flips.
4. New driver's first iteration uploads devicestatus(currentDriver:
self), confirming the rendezvous and re-stamping currentDriver
idempotently. THIS IS THE LOAD-BEARING SAFETY PROPERTY: a transient
pre-flip upload failure is recovered automatically within ~5 min.
If outgoing driver crashes between steps 1 and 4, the incoming driver's
first iteration after BLE detection corrects state idempotently. The
plan's test_role_flip_aborted_on_step1_upload_failure is INTENTIONALLY
DROPPED under Option D — aborting role flip on a transient network
failure would block legitimate handoff when the user walks out of BLE
range, worse than the brief caretaker-visibility gap. Idempotency on
first-iteration is the safety net.
commandsAllowedCheck (B.8.x) continues to short-circuit pump commands
during the rendezvous window. B.11.3 makes no algorithm-scope changes
— algorithm-fidelity-audit verdict: PASS / Class B (additive
structural).
threecee-claude
Sign up for freeto 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

@threecee@threecee-adsk