Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
333 commits
Select commit Hold shift + click to select a range
aaaea35
ble: persist the hello-deferral run across process restarts
ryanbr Aug 30, 2026
7939bbb
collect: the Swift live-persist catches were silent too
ryanbr Aug 30, 2026
97734cc
staging: build 385/266 for the persisted deferral run and the Swift t…
ryanbr Aug 30, 2026
97881d9
staging: build 387/268 to put testing-latest back on the main line
ryanbr Aug 30, 2026
ac068c1
feat(ble): adopt a stable whoop-<serial> identity for 5.0/MG (#1455)
ryanbr Aug 30, 2026
c6c0c1d
ble: the pairing experiment must not cost a user their history sync
ryanbr Aug 30, 2026
b343f75
staging: build 388/269 for the pairing-experiment sync fix
ryanbr Aug 30, 2026
39692d4
ble: a written hello is not a working handshake
ryanbr Aug 30, 2026
fe7277e
WHOOP 5/MG: probe whether the historical offload actually needs a bon…
ryanbr Aug 30, 2026
7452b9c
staging: build 389 / iOS 270 for the unbonded offload probe
ryanbr Aug 30, 2026
4280efa
WHOOP 5/MG: let the offload probe supersede the handshake, or it neve…
ryanbr Aug 30, 2026
b2a4049
WHOOP 5/MG: a completed CLIENT_HELLO write is not an encrypted link (…
ryanbr Aug 30, 2026
a82e7f6
Last sync belongs to a strap, not to the install (#1745)
ryanbr Aug 30, 2026
7378c97
Sync controls follow what the strap can actually do (#1748)
ryanbr Aug 30, 2026
331eccc
The offload probe has to report when the link is what ends it (#1749)
ryanbr Aug 31, 2026
555778e
Apple parity: last sync belongs to a strap, and sync controls follow …
ryanbr Aug 31, 2026
8cfb2bc
sleep: make debt an actionable next-night target (#1741)
bhelm Aug 31, 2026
c6dcef4
sync: skip the #689 backlog decode on the GET_DATA_RANGE PENDING ack …
Zebsi235 Aug 31, 2026
6bd877a
tooling: i18n_audit keys its baselines on OS-native paths, so it fail…
Zebsi235 Aug 31, 2026
4e75d45
Add a JSON query CLI for local read tools (#1752)
kvnloo Aug 31, 2026
1a8195c
sleep: let the sparse bridge cross one short active run (#1755)
ryanbr Aug 31, 2026
1253ce2
staging: build 395 / iOS 276 for the sleep-bridge work
ryanbr Aug 31, 2026
7960c0c
The unbonded offload probe must not start on top of the DIS chain (#1…
ryanbr Aug 31, 2026
2fe3a5c
Coach: retire the transcript on a new local day (#1542)
dwehrmann Aug 31, 2026
c09838c
Coach: twin the day-boundary retirement on Apple (#1757)
ryanbr Aug 31, 2026
712c020
The unbonded offload probe's silence budget must outlive the process …
ryanbr Aug 31, 2026
6f290d4
Asking Android to pair must not clear the hello give-up every link (#…
ryanbr Aug 31, 2026
9991f7b
staging: build 398 / iOS 279 for the #1635 reconnect-loop work
ryanbr Aug 31, 2026
f1b4eb2
Stop asking Android to pair once the strap has refused the bond (#1635)
ryanbr Aug 31, 2026
9e4fe34
staging: build 399 / iOS 280 for the pairing-request stand-down
ryanbr Aug 31, 2026
c9a4802
The Devices card told the truth once, then went green again (#221)
ryanbr Aug 31, 2026
0504a31
A day you never logged is not a day you answered No (#322)
ryanbr Aug 31, 2026
c40287f
protocol(mg): decode the type-43 REALTIME_RAW_DATA record on both pla…
Zebsi235 Aug 31, 2026
aa5312d
Make standard-HR ingestion timing and durability observable (#1767)
kvnloo Aug 31, 2026
eef14cb
Android standard-HR: stop losing a rejected batch in silence (#1770)
ryanbr Aug 31, 2026
bb0e616
staging: build 400 / iOS 281 for the standard-HR persistence work
ryanbr Aug 31, 2026
bd24995
sleep: say when a night's hypnogram only covers part of its window (#…
pipiche38 Aug 31, 2026
3a27fc9
ios: take the lifecycle flush back out — it is what broke the build
ryanbr Aug 31, 2026
387d644
ios: re-land the standard-HR lifecycle flush on the scene delegate (#…
ryanbr Aug 31, 2026
1499caa
diag: say why Effort is 0, why a night did not stage, and what the st…
ryanbr Aug 31, 2026
963dd17
staging: build 401 / iOS 282 — the first complete set since iOS broke
ryanbr Aug 31, 2026
5254ccc
ble: keep the hello-suppression latch across a user Connect (#1778)
ryanbr Aug 31, 2026
d070512
staging: build 402 / iOS 283 for the suppression-latch fix
ryanbr Aug 31, 2026
907c9cd
ble: spend fewer links learning what the strap already told us (#1779)
ryanbr Aug 31, 2026
820a2b6
staging: build 403 / iOS 284 for the reconnect-cost work
ryanbr Aug 31, 2026
faa1fd4
ble: a timer firing is not the user asking to retry the handshake (#1…
ryanbr Aug 31, 2026
2f32852
staging: build 404 / iOS 285 for the system-initiated connect split
ryanbr Aug 31, 2026
7bbef7a
sleep: let the HR band widen the sparse bridge's active bound, not be…
ryanbr Sep 1, 2026
efc9894
staging: build 405 / iOS 286 for the sparse-bridge in-band bound
ryanbr Sep 1, 2026
7b566f5
fix(oura): list a bonded ring that advertises no local name (#1776)
pipiche38 Sep 1, 2026
708d1f1
doc(oura): record the 0x6A motion_count guard discrepancy vs open_our…
pipiche38 Sep 1, 2026
8c9fca0
Add an opt-in pre-sleep heart-rate feedback contract (#1772)
kvnloo Sep 1, 2026
d4fa26d
test(push): pin the clock, so the suite stops asserting the calendar …
ryanbr Sep 1, 2026
63a0133
analytics: give the pre-sleep HR feedback its Kotlin twin (#1785)
ryanbr Sep 1, 2026
82f8cb9
push: make an omitted clock a compile error, not a green test (#1788)
ryanbr Sep 1, 2026
8b30786
Preserve sensor-contact state from standard BLE heart-rate measuremen…
kvnloo Sep 1, 2026
270a84f
standard-HR: record contact changes, not every reading
ryanbr Sep 1, 2026
d9a5c7a
staging: build 406 / iOS 287 for the standard-HR contact work
ryanbr Sep 1, 2026
4972e51
test(standard-HR): repin the contact auto-flush for change-only recor…
ryanbr Sep 1, 2026
1d9f1f3
staging: build 407 / iOS 288 on a fully verified tree
ryanbr Sep 1, 2026
7ab0588
ble: offer to clear a stale phone pairing, once, after asking first (…
ryanbr Sep 1, 2026
caa9d37
staging: build 408 / iOS 289 for the stale-pairing clear
ryanbr Sep 1, 2026
1a9d6c4
ble: record why the stale-pairing clear stays Android-only (#1791)
ryanbr Sep 1, 2026
8918f53
staging: build 409 / iOS 290 on the parity record
ryanbr Sep 1, 2026
ebcb0cd
docs: describe the CI that actually runs (#1793)
ryanbr Sep 1, 2026
08c0f82
Canonicalize signed zero in effective effort (#1792)
bhelm Sep 1, 2026
73e4831
Split Health Connect import permissions by category (#1509)
kavemang Sep 1, 2026
f481377
staging: build 410 / iOS 291 for the Health Connect permission split
ryanbr Sep 1, 2026
a1470cf
analytics: do not claim a twin with a Swift stdlib method (#1794)
ryanbr Sep 1, 2026
527e797
Release 11.0.0: notes, What's New, and the version bump
ryanbr Sep 1, 2026
d6d84cc
Release 11.0.0: add to AltStore source
ryanbr Sep 1, 2026
1413363
fix(oura): make the nameless-ring label rule one tested twin, not two…
pipiche38 Sep 1, 2026
206a6ff
docs(oura): correct the whitespace-divergence note on ouraAdvertisedL…
ryanbr Sep 1, 2026
dca0576
android: twin standardHRHostReceivedLine and be honest about take(200…
kvnloo Sep 1, 2026
9e5afbf
docs(ble): give standardHrFlushAttemptLine its doc back
ryanbr Sep 1, 2026
208c442
widgets: give the home-screen vitals footer the app's own HR/HRV icon…
ryanbr Sep 2, 2026
b9b04c8
widget: let the home-screen HR and battery line say what it is (#1800)
ryanbr Sep 2, 2026
d2f2c96
sleep: add an HR-only Stage-0 spine for straps that bank no motion (#…
ryanbr Sep 2, 2026
db1b81f
sleep: wire the HR-only spine, on a percentile anchor rather than the…
ryanbr Sep 2, 2026
5cc1e28
Bump to 11.1.0 for a testing build
ryanbr Sep 2, 2026
33ce503
backup: warn at export when a database is past the restore ceiling, a…
ryanbr Sep 2, 2026
1e3e75a
backup: Android half — warn at export, and let a user restore an over…
ryanbr Sep 2, 2026
b6cb5f7
sleep: give the HR-only spine a funnel line, because it shipped silen…
ryanbr Sep 2, 2026
67d7512
Bump build to 413 for a testing build
ryanbr Sep 2, 2026
e4b27fb
sleep: stop the HR-only fallback inheriting an exclusion written to k…
ryanbr Sep 2, 2026
671b265
Bump build to 414 for a testing build
ryanbr Sep 2, 2026
92ce025
live workout: stop the bottom timer sitting under the pause button (#…
ryanbr Sep 2, 2026
b43b8f0
steps: stop the calibration card asking for the half the user already…
ryanbr Sep 2, 2026
a741dbc
Bump build to 415 for a testing build
ryanbr Sep 2, 2026
9d70981
Coach: send sleep stage breakdown and normalised efficiency (#1817)
ryanbr Sep 2, 2026
edb6e99
rtc: stop telling an already-charged strap to charge it (#1819)
ryanbr Sep 2, 2026
7831d34
diag(ble): log a link epitaph on every disconnect (#1820)
ryanbr Sep 2, 2026
13ffb82
Clock format setting: System / 12-hour / 24-hour (#1822)
ryanbr Sep 2, 2026
b254e70
Bump build to 416 for a testing build
ryanbr Sep 2, 2026
b2288ba
diag(clock): read the strap's answer instead of asserting it (#1824)
ryanbr Sep 2, 2026
b11a8a5
Bump build to 417 for a testing build
ryanbr Sep 2, 2026
f685632
ble: report an intentional teardown as intentional in the Android epi…
ryanbr Sep 2, 2026
7df4722
Release 11.1.0: notes, changelog, in-app What's New, build numbers
ryanbr Sep 2, 2026
2787d46
Release 11.1.0: add to AltStore source
ryanbr Sep 2, 2026
ab9420e
ble: light the charging pill on a 5/MG from the pack-attach events (#…
Zebsi235 Sep 2, 2026
7ff64d6
docs(ios): explain the AltServer sign-in failure that stops the insta…
ryanbr Sep 2, 2026
7b1c49c
ble: handle the pack-attach events on Apple too (#1831)
ryanbr Sep 3, 2026
1aeae00
perf(clock): memoise the resolved clock instead of rebuilding it per …
ryanbr Sep 3, 2026
dfa2175
ble: redact a hex-hidden serial on Apple too (#1834)
ryanbr Sep 3, 2026
ddb5a2d
test centre: log every pushed strap EVENT under the Connection domain…
Zebsi235 Sep 3, 2026
21fb712
Bump to 11.1.1 for a testing build
ryanbr Sep 3, 2026
60e5d41
docs(ios): the AltServer sign-in failure is an upstream bug, not your…
ryanbr Sep 3, 2026
52bf64f
android: optional overlay bottom bar so its glass has a backdrop (#1837)
ryanbr Sep 3, 2026
42367d4
Bump build to 420 for a testing build
ryanbr Sep 3, 2026
0839ca9
android: make the overlay bar actually overlay something (#1838)
ryanbr Sep 3, 2026
936902d
Bump build to 421 for a testing build
ryanbr Sep 3, 2026
8fd764e
android: hide the bottom bar while scrolling down (#1840)
ryanbr Sep 3, 2026
b7d8743
Bump build to 422 for a testing build
ryanbr Sep 3, 2026
c7b629f
android: default the overlay bottom bar and auto-hide ON
ryanbr Sep 3, 2026
b6df4d8
ios: hide the tab bar on scroll, from the same setting Android uses (…
ryanbr Sep 3, 2026
b8da10c
today: per-field HRV / Resting HR carry on both platforms, and stop p…
ryanbr Sep 3, 2026
152f4a8
build: staging 423 / iOS 304 for the next testing build
ryanbr Sep 3, 2026
28d57b1
today: lead skin temp with the measured temperature, with a Settings …
ryanbr Sep 3, 2026
5b4bf31
build: staging 424 / iOS 305 for the next testing build
ryanbr Sep 3, 2026
8905f8f
health: say when skin temp could not honour the Settings choice (#1847)
ryanbr Sep 3, 2026
a624071
build: staging 425 / iOS 306 for the next testing build
ryanbr Sep 3, 2026
82d1529
health: apply the skin-temp preference across the window, not just th…
ryanbr Sep 3, 2026
2ec7458
build: staging 426 / iOS 307 for the next testing build
ryanbr Sep 3, 2026
73be481
health: backfill the skin-temp absolutes the 21-night window never re…
ryanbr Sep 3, 2026
4078180
build: staging 427 / iOS 308 for the next testing build
ryanbr Sep 3, 2026
0c88047
health: the absolutes backfill never ran on the installs that need it…
ryanbr Sep 3, 2026
f0a32fa
build: staging 428 / iOS 309 for the next testing build
ryanbr Sep 3, 2026
47bad5d
health: stop predicting which device a night belongs to — read it off…
ryanbr Sep 3, 2026
15060dd
build: staging 429 / iOS 310 for the next testing build
ryanbr Sep 3, 2026
7ec3dbc
Revert the skin-temp absolutes backfill (#1857)
ryanbr Sep 3, 2026
002608f
alarm: per-day wake times for the phone alarm (#1859)
ryanbr Sep 4, 2026
6d25688
ble: re-arm a WHOOP 5/MG realtime stream that has lapsed (#1866)
ryanbr Sep 4, 2026
559f7fd
build: staging 430 / iOS 311 for the next testing build
ryanbr Sep 4, 2026
17a28f0
ble: stop skipping the handshake once the unbonded probe has retired …
ryanbr Sep 4, 2026
ba6eba9
fix(l10n): restore pt-PT Restaurar corrupted by a Rest→Descanso find/…
kvnloo Sep 4, 2026
e3822b7
fix(oura): break the resume-cursor <-> time-anchor deadlock that re-s…
pipiche38 Sep 4, 2026
8789c41
alarm: log what the smart-alarm detector actually saw (#1870)
ryanbr Sep 4, 2026
b24f776
docs(ios): name the on-device AltStore Error Log shape, not just the …
ryanbr Sep 4, 2026
8954b44
coupled: show the optimal strain band as a tube on Android, matching …
ryanbr Sep 4, 2026
e12dff5
build: staging 431 / iOS 312 for the next testing build
ryanbr Sep 4, 2026
2809c16
i18n: add a Russian locale to Android, and backfill Apple's from it (…
ryanbr Sep 4, 2026
6844b5d
i18n(android): park the partial Arabic translation as data, not a loc…
ryanbr Sep 4, 2026
800cb9c
sleep: drag the filled hypnogram to read the clock time under your fi…
ryanbr Sep 4, 2026
14d002c
build: staging 432 / iOS 313 for the next testing build
ryanbr Sep 4, 2026
dbe0834
backup: say WHICH folder backups go to, iCloud Drive included (#1876)
ryanbr Sep 4, 2026
6c1fd6b
android(backup): clear the busy flag in a finally, as the Apple twin …
ryanbr Sep 4, 2026
188bc47
build: staging 433 / iOS 314 for the next testing build
ryanbr Sep 4, 2026
7e817b5
ble: say what an unbonded strap actually costs, not just history sync…
ryanbr Sep 4, 2026
a7f24db
today: say why Recovery Vitals is blank on a night with no motion (#1…
ryanbr Sep 4, 2026
7d7702c
build: staging 434 / iOS 315 for the next testing build
ryanbr Sep 4, 2026
e162428
connection: say which streams actually banked on a finished link (#1880)
ryanbr Sep 4, 2026
9ca9414
build: staging 435 / iOS 316 for the next testing build
ryanbr Sep 4, 2026
ee49d29
Fix daily energy integration for sparse HR (#1692)
fadkeprasad Sep 4, 2026
435842a
connection: split the banked line by PATH, so it says something (#1882)
ryanbr Sep 4, 2026
5bc2cbd
build: staging 436 / iOS 317 for the next testing build
ryanbr Sep 4, 2026
d0875be
sleep: report the HRV an HR-only night actually measures (#1884)
ryanbr Sep 5, 2026
e6450e8
build: staging 437 / iOS 318 for the next testing build
ryanbr Sep 5, 2026
70d350c
ble: connect and attribute the strap the user actually selected (#1881)
ryanbr Sep 5, 2026
ef96051
feat: sleep-aware steps and physiological day cycles (#1572)
bhelm Sep 5, 2026
b3f8333
daycycle: drop a dead parameter and pin the two branches nothing reac…
ryanbr Sep 5, 2026
49bf53d
analytics: retrospective TDEE from logged intake and the weight trend
ryanbr Sep 5, 2026
d479e7e
ble: give the battery-pack decoder a caller, and a sanity check befor…
ryanbr Sep 5, 2026
441d771
ble: stop telling a strap that refuses pairing to try pairing again (…
ryanbr Sep 5, 2026
0ae341f
build: restore the Apple targets, and the app-target tests that never…
ryanbr Sep 5, 2026
205ae63
today: add an optional, default-OFF Coach launcher card (#1862)
ryanbr Sep 5, 2026
cbbbbfd
settings: stop claiming an unpaired strap costs you HRV and resting HR
ryanbr Sep 5, 2026
ad7da24
ble: record what a strap advertised, and keep owner names out of the …
ryanbr Sep 5, 2026
ff4ed6a
ble: give a WHOOP 4.0 the stable serial identity the 5/MG already has…
ryanbr Sep 5, 2026
1d5b009
build: testing build 439 / 320
ryanbr Sep 5, 2026
bbebaa8
ble: mask WHOOP serials that start with a letter
ryanbr Sep 5, 2026
fd18ef9
analytics: cap HR, R-R and gravity reads separately, so a night is no…
ryanbr Sep 5, 2026
64ddc81
build: testing build 440 / 321
ryanbr Sep 5, 2026
2a06c24
today: let the Recovery Vitals rows open their trends, like the cards do
ryanbr Sep 5, 2026
3c3f15d
build: testing build 441 / 322
ryanbr Sep 5, 2026
168cfc1
settings: give the bottom bar its own card, with a size and a transpa…
ryanbr Sep 5, 2026
745df16
build: testing build 442 / 323
ryanbr Sep 5, 2026
2b46dd0
analytics: require one continuous effort for heart-rate recovery elig…
bhelm Sep 6, 2026
f1c9db3
tools: make the capture tools work on non-UTF-8 hosts, and close thei…
bhelm Sep 6, 2026
73f62b8
ci: only run the Python tools workflow when the Python tools change
ryanbr Sep 6, 2026
df1c3cf
settings: centre the bar's transparency default, add notches, and kee…
ryanbr Sep 6, 2026
1e3ee08
build: testing build 443 / 324
ryanbr Sep 6, 2026
2b80c95
analytics: say when a night's stream came back at its read cap
ryanbr Sep 6, 2026
eca2f0f
build: testing build 444 / 325
ryanbr Sep 6, 2026
5536b76
analytics: witness every scored stream in the per-day reuse key, not …
bhelm Sep 6, 2026
cf5f8de
today: stop the Liquid Effort tile calling a load index a percentage
ryanbr Sep 6, 2026
df22dc7
tests: repin the frozen-Rest tests on what the shipping code does
ryanbr Sep 6, 2026
2db7e76
build: testing build 445 / 326
ryanbr Sep 6, 2026
e750953
coach: streaming, persisted history, voice, morning brief, widgets an…
evoveotech Sep 6, 2026
9966d0a
coach: localize the voice controllers on both platforms, pin CRLF on …
ryanbr Sep 6, 2026
ba0f09b
skin-temp: explorer leads with the absolute and honours the Settings …
evoveotech Sep 6, 2026
a6c6de7
units: split body measurements from exercise distance and pace
kavemang Sep 6, 2026
92ca59a
storage: let the Apple footprint name the tables that can actually be…
ryanbr Sep 6, 2026
e4cf076
build: testing build 446 / 327
ryanbr Sep 6, 2026
fae9420
wind-down: pin the per-day nudge to the nudge's own day, not the wake's
evoveotech Sep 6, 2026
c7b0fc5
alarm: per-day wake overrides now drive the strap alarm and backup no…
evoveotech Sep 6, 2026
785a334
storage: estimate bytes per table on both platforms, and stop saying …
ryanbr Sep 6, 2026
b9a9280
import: convert Fahrenheit skin-temp on the way in, not alias it to °C
evoveotech Sep 6, 2026
5d3fb38
steps: the Today tile names the right missing half when no motion is …
evoveotech Sep 6, 2026
5cdb5d7
today: the calibrating ring counts nights, and says when they arrive …
ryanbr Sep 6, 2026
3cdae38
build: testing build 447 / 328
ryanbr Sep 6, 2026
5d8cc42
health: drop the active strap id from Apple Health external UUID keys
evoveotech Sep 6, 2026
9eb744d
skin-temp: backfill absolutes for nights outside the 21-night rescore…
evoveotech Sep 6, 2026
f77fc65
storage: bound ppgWaveformSample with rolling newest-N retention
ryanbr Sep 6, 2026
ab14a95
recovery: close the final HR bin on the window end so an endpoint sam…
bhelm Sep 6, 2026
c709a46
test centre: sweep cmd 151's arguments, and record what an MG answers
Zebsi235 Sep 6, 2026
744a8b9
test centre: keep the pack probe's serial and address out of both its…
ryanbr Sep 6, 2026
ff103fa
devices: show the battery pack's charge, from an event the strap alre…
ryanbr Sep 6, 2026
8c3ddac
build: testing build 448 / 329
ryanbr Sep 6, 2026
460f11a
hrv: withhold RMSSD on a night whose R-R over-counts
ryanbr Sep 6, 2026
ef54b75
de: convert the German copy from formal Sie to informal du
andigandhi Sep 6, 2026
965e5dc
sleep: close the final HR/HRV window on the shipped stager path
bhelm Sep 6, 2026
7ec7e77
ecg: the pure research modules — allow-list, stats and log
Zebsi235 Sep 6, 2026
75dca36
analytics: a zone-based local day-window helper with a Kotlin twin an…
bhelm Sep 6, 2026
212b99a
import: read GPS routes from Apple Health and Health Connect (#1205)
evoveotech Sep 6, 2026
4aff485
test centre: keep the pack's BT address out of the event census
Zebsi235 Sep 6, 2026
582995a
build: testing build 449 / 330
ryanbr Sep 6, 2026
7ee36e1
today: surface Rest as Pending sync while the strap has un-offloaded …
evoveotech Sep 6, 2026
1356da0
build: testing build 450 / 331
ryanbr Sep 6, 2026
09c8818
rhr: measure what an artefact gate would do, before gating anything (…
ryanbr Sep 7, 2026
e2a68f3
charging: stop pack presence from overriding the strap gauge (#1945)
ryanbr Sep 7, 2026
829ee1d
explorer: stop the readings table printing the unit twice (#1947)
ryanbr Sep 7, 2026
8bc0e6d
connection: say why the unbonded probe was skipped, and what we hold …
ryanbr Sep 7, 2026
ad7e625
battery: stop asking a 5/MG for battery it refuses to answer (#1951)
ryanbr Sep 7, 2026
cdf247c
ci: trigger the app build on the app-target tests it runs (#1955)
ryanbr Sep 7, 2026
39cbdcd
widget: heart rate with the recent trace (Android) (#1958)
ryanbr Sep 7, 2026
1106b97
build: testing build 451 / 332
ryanbr Sep 7, 2026
1108050
widget: let the launcher lay out the heart-rate card (#1961)
ryanbr Sep 7, 2026
ed3b7aa
build: testing build 452 / 333
ryanbr Sep 7, 2026
5b01367
widget: heart rate with the recent trace (iOS) (#1964)
ryanbr Sep 7, 2026
ddec88f
build: testing build 453 / 334
ryanbr Sep 7, 2026
6b58ac1
iOS: poll 0x2A19 battery for an unbonded 5/MG on the keep-alive tick …
evoveotech Sep 7, 2026
5428c84
backfill: name the unbonded-offload probe in the deferral diagnostic …
evoveotech Sep 7, 2026
6fd49b0
backfill: distinguish no-flash-cursor from recording-nothing in the e…
evoveotech Sep 7, 2026
0e6845c
build: testing build 454 / 335
ryanbr Sep 7, 2026
c753493
widget: measure what the home-screen widgets cost, and stop sending u…
ryanbr Sep 7, 2026
9e5bc84
build: testing build 455 / 336
ryanbr Sep 7, 2026
3f89554
widget: measure the rates after the startup burst, and give the count…
ryanbr Sep 7, 2026
7d78150
build: testing build 456 / 337
ryanbr Sep 7, 2026
59f75de
diagnostics: three lines that told triage the wrong thing
ryanbr Sep 7, 2026
207fe58
sleep: the night label counted from the newest record, not from today
ryanbr Sep 7, 2026
d2bfb82
build: testing build 457 / 338
ryanbr Sep 7, 2026
e3d799c
ble: add a timing tell for Apple's unverified bond state (#1883)
evoveotech Sep 7, 2026
ef70b09
unbonded-offload probe: a local teardown is inconclusive, not a strap…
evoveotech Sep 7, 2026
95a3e6a
Sleep tab: stamp carried prior-night metric values with their day (#1…
evoveotech Sep 7, 2026
71433ae
sessionRestingHR: gate bins on sample count + plausibility before let…
evoveotech Sep 7, 2026
d6cc588
build: testing build 458 / 339
ryanbr Sep 7, 2026
7e7972a
fix(sleep): drop a stagesJSON write that would make a stored night le…
pipiche38 Sep 7, 2026
e59a641
build: testing build 459 / 340
ryanbr Sep 7, 2026
81da85c
fix(protocol): propagate the full frame integrity verdict to every co…
Sep 8, 2026
bd1ec3a
test(app): call the main-actor Backfiller helper from main-actor tests
Sep 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
16 changes: 16 additions & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,27 @@ on:
paths:
- 'android/**'
- '.github/workflows/android.yml'
# The twin-drift guards read the SWIFT side's fixtures on purpose — DecoderOracleTest compares
# against Packages/.../decoder_oracle.json to catch a one-sided edit, and the schema/protocol
# oracles work the same way. Without these, editing the Swift copy alone leaves the very test
# written to catch that from ever running. Keep this list in step with what the tests read.
- 'Packages/WhoopProtocol/Sources/WhoopProtocol/Resources/**'
- 'Packages/WhoopProtocol/Tests/WhoopProtocolTests/Resources/**'
- 'Packages/WhoopStore/Tests/WhoopStoreTests/Resources/**'
- 'Strand/Resources/Localizable.xcstrings'
push:
branches: [main]
paths:
- 'android/**'
- '.github/workflows/android.yml'
# The twin-drift guards read the SWIFT side's fixtures on purpose — DecoderOracleTest compares
# against Packages/.../decoder_oracle.json to catch a one-sided edit, and the schema/protocol
# oracles work the same way. Without these, editing the Swift copy alone leaves the very test
# written to catch that from ever running. Keep this list in step with what the tests read.
- 'Packages/WhoopProtocol/Sources/WhoopProtocol/Resources/**'
- 'Packages/WhoopProtocol/Tests/WhoopProtocolTests/Resources/**'
- 'Packages/WhoopStore/Tests/WhoopStoreTests/Resources/**'
- 'Strand/Resources/Localizable.xcstrings'
workflow_dispatch:

concurrency:
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/app-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ on:
branches: [main]
paths:
- 'Strand/**'
# The app-target TESTS, not just the sources they cover. The `Test Strand` step below runs
# StrandTests, but a PR touching only those tests matched nothing here, so it built and ran
# nothing: one that failed to compile, or simply failed, merged unverified and went red on the
# next unrelated PR that happened to touch `Strand/**`, mis-attributing the breakage to whoever
# that was.
#
# This is the only test directory that needs naming, and the reason is layout rather than
# oversight: Android's tests sit under `android/**` and the package suites under `Packages/**`,
# both already inside their subject's glob, while StrandTests is a SIBLING of `Strand/` instead
# of part of it. `project.yml` does not cover it either — the target takes
# `sources: [StrandTests]`, a directory, so adding a test file changes no other tracked path.
- 'StrandTests/**'
- 'StrandiOS/**'
- 'StrandiOSShared/**'
- 'StrandiOSWidgets/**'
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/swift-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ on:
- 'Tools/SleepPSG/**'
- 'Tools/Backfill/**'
- '.github/workflows/swift-packages.yml'
# Mirror of the note in android.yml: package tests read the ANDROID oracles to catch a one-sided
# edit, and one reads Strand/Liquid/LiquidCore.swift. Editing only that side would otherwise skip
# the guard written for exactly that case. Keep in step with what the tests read.
- 'android/app/src/test/resources/**'
- 'Strand/Liquid/LiquidCore.swift'
push:
branches: [main]
paths:
Expand All @@ -24,6 +29,11 @@ on:
- 'Tools/SleepPSG/**'
- 'Tools/Backfill/**'
- '.github/workflows/swift-packages.yml'
# Mirror of the note in android.yml: package tests read the ANDROID oracles to catch a one-sided
# edit, and one reads Strand/Liquid/LiquidCore.swift. Editing only that side would otherwise skip
# the guard written for exactly that case. Keep in step with what the tests read.
- 'android/app/src/test/resources/**'
- 'Strand/Liquid/LiquidCore.swift'
workflow_dispatch:

concurrency:
Expand Down
58 changes: 52 additions & 6 deletions .github/workflows/tools-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,35 @@
# one of them only ever executed when a contributor remembered to, which meant a decoder could regress and
# the suite that would have caught it stayed green by never being run.
#
# Cheap to run: pure stdlib `unittest`, no dependencies, no fixtures to fetch, ubuntu rather than macOS.
# Path-filtered so it only fires when the suites it runs change.
# Cheap to run: pure stdlib `unittest`, no dependencies, no fixtures to fetch, ubuntu rather than macOS —
# about 25 seconds end to end.
#
# NOT path-filtered, and that is deliberate. It used to fire only on `Tools/**.py`, on the premise that a
# suite can only break when its own source changes. That is false for `test_home_i18n.py`, which asserts
# on the CONTENTS of app and package files — AppModel.swift, the xcstrings catalogues, ReadinessEngine,
# DayNavBar. A Swift-only commit broke it (#1671 moved a sign and a hardcoded °C out of a localized
# literal) and the filter meant this job never ran, so `main` sat red until a contributor touching
# `Tools/` for something unrelated tripped over it in their own PR (#1691).
#
# Widening the filter to name those paths would just re-create the trap one edit later: the list is
# whatever the assertions happen to read today. Twenty-five seconds on every PR is cheaper than a gate
# that is green because it did not run.
name: Tools Python CI

on:
# Path-filtered like android.yml / swift-packages.yml / app-build.yml. Without this the workflow ran on
# EVERY pull request and every push to main, including the many that touch no Python at all - and with a
# Windows runner now in it, billed at twice a Linux minute, that is a real cost for a job whose inputs
# cannot have changed. The workflow file itself is included so a change to the job still tests itself.
pull_request:
branches: [main]
paths:
- 'Tools/linux-capture/**'
- 'Tools/*.py'
- 'Tools/**'
- '.github/workflows/tools-python.yml'
push:
branches: [main]
paths:
- 'Tools/linux-capture/**'
- 'Tools/*.py'
- 'Tools/**'
- '.github/workflows/tools-python.yml'
workflow_dispatch:

Expand Down Expand Up @@ -76,3 +89,36 @@ jobs:
exit 1
fi
working-directory: Tools

windows-capture:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

# Keep Windows locale defaults and exercise a strict legacy console, including subprocesses.
# UTF-8 mode would hide implicit file encodings; replacement output would hide console bugs.
- name: Run Tools/linux-capture tests with legacy Windows encodings
working-directory: Tools/linux-capture
env:
PYTHONUTF8: '0'
PYTHONIOENCODING: 'cp1252:strict'
PYTHONLEGACYWINDOWSSTDIO: '1'
shell: python
run: |
import sys
import unittest

suite = unittest.defaultTestLoader.discover('.', pattern='test_*.py')
count = suite.countTestCases()
print(f'collected {count} tests', flush=True)
result = unittest.TextTestRunner(verbosity=2).run(suite)
if count < 200 or result.testsRun < 200:
print('::error::expected at least 200 tests; discovery or execution is incomplete')
sys.exit(1)
sys.exit(0 if result.wasSuccessful() else 1)
14 changes: 9 additions & 5 deletions ATTRIBUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,15 @@ This is long-standing practice, not a one-off. Worked examples already in the tr
field order, widths and enum values for `FilteredLabradorPacket` / `RawLabradorPacket`, plus the
`{revision, arg, padding}` command payload shape, re-derived from static analysis of the vendor's iOS
client. The four command NUMBERS were already in our own `CommandNumber` table from the whoomp/goose
work above. Everything ships as an **unvalidated candidate**: no strap has yet been asked whether it
honours these commands, the decode backs no metric, and the fields nobody can attest (the wrist enum's
raw values, the progress "timed out" sentinel, the ECG sample unit, the packet type byte) are carried
raw rather than named. The on-strap rhythm classifier's verdict is decoded as a byte and is never
presented as a finding — NOOP is not a medical device.
work above. Most of it still ships as an **unvalidated candidate**: the decode backs no metric, and the
fields nobody can attest (the wrist enum's raw values, the progress "timed out" sentinel, the ECG
sample unit, the packet type byte) are carried raw rather than named. **One exception is now attested
rather than inferred:** `ControlSignal`'s raw values were also read off the client's enum order, and a
WHOOP MG (`WS50_r00`, fw `50.39.1.0`) contradicts that reading — `1` stops generation, `2` starts it,
`0` is refused. The enum now carries the hardware values with the device and firmware named. A
hardware-observed value is strictly stronger than the inference it replaces, but it is one device.
The on-strap rhythm classifier's verdict is decoded as a byte and is never presented as a finding —
NOOP is not a medical device.

And the line held from the other side:

Expand Down
60 changes: 60 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,66 @@ approximate; downloads are on the [Releases](https://github.com/NoopApp/noop/rel

---

## 11.1.0: A clock you choose, sleep without motion, and logs that report instead of assuming (all platforms)

**Choose how you read times**

- **Clock format: System / 12-hour / 24-hour (#1821).** Times were decided by your device's *region*, not
by you. On Apple that is deliberate — it preserves regional date order and clock style — but building a
locale from a region identifier discards the explicit **24-Hour Time** switch in iOS Settings, so
someone in a 24-hour country who preferred a 12-hour clock had no way to ask for one. Settings →
Appearance now offers the choice, defaulting to System, and System now follows your phone's own switch.
It applies to the Asleep/Woke row, workout and nap times, caffeine and hydration entries, backup
timestamps, sleep-stage timelines and the home-screen widgets. Chart axis ticks stay 24-hour for now:
12-hour labels are wider and the axis spacing needs checking on a device first.

**Sleep**

- **A night can be found from heart rate alone (#1801).** Sleep detection is built on stillness, so a
strap that banks no movement scored no nights at all — the everyday reality of a WHOOP 5/MG that will
not complete pairing. NOOP can now detect and stage such a night from heart rate. It is display-only by
design: an HR-only night never feeds resting heart rate, HRV or SDNN baselines, because it was not
established with the evidence those depend on.
- **The AI coach receives your sleep stages (#1816).** It had been answering that it could not see them,
because deep, REM and light minutes were never included in its context. They are now, with efficiency
normalised so it cannot arrive as a nonsensical percentage.

**Straps, logs and honesty**

- **The 1970/71 clock warning takes battery into account (#1818).** It previously offered one remedy at
any charge level, so people already at 100% were told to charge to 100% and reconnect — advice that was
not backed by any precondition in the code.
- **Every disconnect records what actually happened (#1809, thanks @supremesynergy).** How long the link
held, how many frames and bytes arrived, whether the realtime stream was armed, and how it ended. A
strap log could not previously state whether a strap had transmitted anything; you had to infer it from
every logged line happening to be outgoing, which measures NOOP's logging rather than the strap.
- **The clock exchange quotes the strap's reply (#1823).** NOOP wrote "clock synced" the moment it queued
the write, before any answer existed, so a log could assert the clock was set while the connection
readout said 1970/71. It now says what it *sent* and records the reply frame. On a WHOOP 5/MG the
readout also claimed "RTC reads 1970/71" when no clock had ever been read on that family; it now
reports the evidence it has, which is how the strap dated its records.

**Backup, widgets and interface**

- **Oversize backups (#1807).** Export warns when an archive exceeds the 2 GiB restore ceiling instead of
writing one that cannot be restored, and an oversize restore can be allowed through rather than refused.
- **Home-screen widgets (#1795, thanks @Sneheth; #1799).** Heart-rate and HRV values had swapped icons on
Apple. Both platforms' widgets now also read aloud properly, speaking the value rather than a bare
number.
- **Live workout no longer shows two overlapping timers (#1814),** and the steps calibration card stops
asking for data already provided (#1815).
- **Oura rings that advertise no name (#1797, thanks @pipiche38)** label consistently across platforms,
and Android's standard heart-rate logging is honest about how much it captured (#1796, thanks @kvnloo).

**Still open**

WHOOP 5/MG history remains unsolved. A strap that will not complete the pairing handshake is never given
a clock, and an un-clocked 5/MG does not persist sensor data to flash, so its offloads complete empty.
The diagnostics above exist to separate that from a strap that *was* clocked and still refuses — a
different fault seen on the same firmware.

---

## 9.3.0: Water and caffeine import, honest Effort, and an Oura resting-heart-rate fix (all platforms)

A release about the numbers being right, on top of 9.2.1.
Expand Down
25 changes: 16 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ and [`docs/IOS.md`](docs/IOS.md) covers the iOS target. Read this first; follow

## What NOOP is (and the hard scope limits)

NOOP is a **fully offline, on-device** companion app for WHOOP 4.0 and 5.0/MG straps (with
NOOP is an **offline-by-default, on-device** companion app for WHOOP 4.0 and 5.0/MG straps (with
**experimental** Oura support in the tree — gated behind `ExperimentalBrand`, not a shipped supported
strap). It pairs over Bluetooth, stores everything in on-device SQLite, and computes recovery / strain
/ HRV / sleep locally. There is **no server, no account, no cloud sync, no telemetry**, and the project stays
**anonymous** (iOS/Android ship build-from-source / sideload, not via the App Store).
/ HRV / sleep locally. There is **no NOOP-operated server, no account, no cloud dependency, no
telemetry**, and the project stays **anonymous** (iOS/Android ship build-from-source / sideload, not
via the App Store). Issue #1314 permits one narrow exception: a default-off Experimental client may
export data one way to an HTTP(S) endpoint the user owns and configures. It must remain outside strap
sync, never read data back, and ship no receiver or hosted service in this repository.

These are hard constraints, not preferences. A PR is out of scope if it:
- adds a server, account, cloud sync, or sends any data off-device;
- adds a server, account, cloud dependency, or sends data off-device without the explicit user export
boundary in [`docs/SCOPE.md`](docs/SCOPE.md) (including #1314's one-way self-hosted push);
- adds analytics/telemetry/crash-reporting that phones home;
- adds WHOOP firmware, decompiled app code, logos/assets, or any DRM circumvention. NOOP is
**clean-room interoperability** with hardware the user owns — keep it that way. (That bars
Expand Down Expand Up @@ -115,7 +119,7 @@ xcodegen generate && xcodebuild -project Strand.xcodeproj -scheme Strand \
### What each CI job covers — and the gaps
| Workflow | Covers | Runner | Default state |
|---|---|---|---|
| `swift-packages.yml` | `swift test` for **`Packages/**` only** (WhoopProtocol, WhoopStore, StrandAnalytics, StrandImport, StrandDesign, NoopLocalAccess) | macos-15 | **active** |
| `swift-packages.yml` | TWO jobs. `test`: `swift test` over **`Packages/**`** (WhoopProtocol, WhoopStore, StrandAnalytics, StrandImport, StrandDesign, NoopLocalAccess). `tools`: `swift build` + `swift test` over **`Tools/SleepBench`, `Tools/SleepPSG`, `Tools/Backfill`** — Backfill has no test target, so it is build-only. Path-filtered to those directories. | macos-15 | **active** |
| `app-build.yml` | Builds the **app targets** (`Strand` macOS + `NOOPiOS` iOS) **and runs `StrandTests`** on the macOS/`Strand` leg only — the iOS leg is compile-only. iOS leg needs **macos-26** (iOS 26 SDK / `glassEffect`). | macos-15 / macos-26 | **disabled** (on-demand) |
| `android.yml` | `assembleFullDebug` + `testFullDebugUnitTest` | ubuntu | **active**, path-filtered to `android/**` |
| `source-hygiene.yml` | Doc comments that bind to nothing (`Tools/doc_comment_lint.py`) | ubuntu | **active** |
Expand All @@ -132,10 +136,13 @@ properties are initialized`) will pass every green check and still be broken. If
Swift, you MUST build the app yourself: `xcodebuild … build` locally, or run `app-build.yml` on demand.

### Local walls (things that will *not* build where you expect)
- **On Linux:** only `WhoopProtocol` / `OuraProtocol` (pure) build & test. Every GRDB-linked package —
`WhoopStore`, `StrandImport`, `StrandAnalytics` (via `WhoopStore`), and `NoopLocalAccess` — fails with
`sqlite3.h not found` (GRDB's CSQLite), and `StrandDesign` needs SwiftUI — all need **macOS**. Android
JVM unit tests **do** run on Linux.
- **On Linux:** `WhoopProtocol` / `OuraProtocol` (pure) build & test with a bare toolchain. The
GRDB-linked packages need the snapshot-enabled SQLite build in [`docs/BUILD.md`](docs/BUILD.md) — with
it, all four build AND test: `StrandAnalytics` (1523), `WhoopStore` (439), `StrandImport` (249) and
`NoopLocalAccess` (9). Without those flags they fail with `sqlite3.h not found` (GRDB's CSQLite). `StrandDesign`
needs SwiftUI and is macOS-only. Android JVM unit tests **do** run on Linux.
**None of this is CI-enforced** — `swift-packages.yml` is macOS-only, so Linux support is honour-system
and a change can break it silently.
- **App targets** (`Strand`, `NOOPiOS`) need **Xcode on macOS**; `StrandTests` runs only under
`xcodebuild … test` on macOS — locally, or via `app-build.yml`, which does run it on the `Strand` leg.
Since that workflow is **disabled by default**, app-target tests are only as validated as your last
Expand Down
Loading
Loading